bpmnlint-plugin-camunda-compat 2.59.2 → 2.60.1

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.2",
3
+ "version": "2.60.1",
4
4
  "description": "A bpmnlint plug-in for Camunda compatibility",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -15,10 +15,12 @@ const { annotateRule } = require('../helper');
15
15
  * or a part like `toolCallResult.statusCode`), a script task or called decision
16
16
  * `resultVariable`, or a connector `resultVariable`/`resultExpression` header.
17
17
  *
18
- * The rule warns once per tool: on the element that actually miswrote a
19
- * result-shaped variable, when the tool flow writes some but none of them is
20
- * `toolCallResult` (misdirection or wrong casing); on the entry activity when
21
- * the flow writes none at all, since there's no single offending element to
18
+ * A wrong-casing near-miss (`toolcallresult`, `TOOLCALLRESULT`, ...) is always
19
+ * reported on the element that wrote it, even when a sibling write already
20
+ * sets `toolCallResult` correctly case-sensitive engines never read it.
21
+ * Other miswrites are reported individually per row only while no valid
22
+ * `toolCallResult` exists. When the flow writes none at all, the rule reports
23
+ * once on the entry activity, since there's no single offending element to
22
24
  * point to (the agent gets no completion signal and may retry or hallucinate
23
25
  * an outcome). Results written from arbitrary FEEL expressions are not
24
26
  * statically detectable.
@@ -47,27 +49,39 @@ module.exports = skipInNonExecutableProcess(function(config = {}) {
47
49
 
48
50
  const hasResult = channels.some(isToolCallResultChannel);
49
51
  if (!hasResult) {
50
- const casingMismatch = channels.find(isToolCallResultCasingMismatch);
51
52
 
52
- if (casingMismatch) {
53
- reportErrors(casingMismatch.element, reporter, {
54
- message: `Wrong casing "${getCasingMismatchText(casingMismatch)}": use toolCallResult (case-sensitive).`,
53
+ // Every channel here is a miswrite (none matched toolCallResult), so
54
+ // each is reported on the element that wrote it. A wrong-casing
55
+ // near-miss gets the more specific guidance.
56
+ for (const channel of channels) {
57
+ if (isToolCallResultCasingMismatch(channel)) {
58
+ reportErrors(channel.element, reporter, {
59
+ message: `Wrong casing "${getCasingMismatchText(channel)}": use toolCallResult (case-sensitive).`,
60
+ data: { type: ERROR_TYPES.AGENT_TOOL_OUTPUT_KEY_CASING_INVALID },
61
+ path: getChannelPath(channel),
62
+ });
63
+ } else {
64
+ reportErrors(channel.element, reporter, {
65
+ message: '"toolCallResult" output is not mapped.',
66
+ data: { type: ERROR_TYPES.AGENT_TOOL_OUTPUT_KEY_INVALID },
67
+ path: getChannelPath(channel),
68
+ });
69
+ }
70
+ }
71
+ return;
72
+ }
73
+
74
+ // Miscased near-misses are reported even when a sibling already sets
75
+ // toolCallResult correctly. Casing variants never satisfy
76
+ // isToolCallResultChannel, so the overwrite check below ignores them.
77
+ for (const channel of channels) {
78
+ if (isToolCallResultCasingMismatch(channel)) {
79
+ reportErrors(channel.element, reporter, {
80
+ message: `Wrong casing "${getCasingMismatchText(channel)}": use toolCallResult (case-sensitive).`,
55
81
  data: { type: ERROR_TYPES.AGENT_TOOL_OUTPUT_KEY_CASING_INVALID },
56
- path: getChannelPath(casingMismatch),
82
+ path: getChannelPath(channel),
57
83
  });
58
- return;
59
84
  }
60
-
61
- // Every channel here is a miswrite (none matched toolCallResult), so
62
- // the first one is a real misdirected write; report on the element
63
- // that actually wrote it, not the tool's entry.
64
- const misdirected = channels[ 0 ];
65
- reportErrors(misdirected.element, reporter, {
66
- message: '"toolCallResult" output is not mapped.',
67
- data: { type: ERROR_TYPES.AGENT_TOOL_OUTPUT_KEY_INVALID },
68
- path: getChannelPath(misdirected),
69
- });
70
- return;
71
85
  }
72
86
 
73
87
  // toolCallResult is set somewhere, but assigning it more than once
@@ -267,7 +281,7 @@ function isToolCallResultChannel({ kind, value }) {
267
281
  // case-insensitive name matching for fromAi().
268
282
  function isToolCallResultCasingMismatch({ kind, value }) {
269
283
  if (kind === 'resultExpression') {
270
- return /\btoolcallresult\b/i.test(value) && !/\btoolCallResult\b/.test(value);
284
+ return !!getResultExpressionCasingMismatch(value);
271
285
  }
272
286
 
273
287
  const lower = value.toLowerCase();
@@ -276,13 +290,21 @@ function isToolCallResultCasingMismatch({ kind, value }) {
276
290
  return isCasingVariant && !isToolCallResultChannel({ kind, value });
277
291
  }
278
292
 
293
+ // The first miscased toolCallResult token in a FEEL expression, or null when
294
+ // every occurrence is cased correctly. Tokens are judged individually, so a
295
+ // correct occurrence never excuses a miscased sibling in the same expression.
296
+ function getResultExpressionCasingMismatch(value) {
297
+ const tokens = value.match(/\btoolcallresult\b/gi) || [];
298
+
299
+ return tokens.find(token => token !== 'toolCallResult') || null;
300
+ }
301
+
279
302
  // The mismatched word itself, not the whole expression, for resultExpression
280
303
  // channels (a connector header value is a full FEEL expression, e.g.
281
304
  // `={toolcallresult: response.body}`).
282
305
  function getCasingMismatchText({ kind, value }) {
283
306
  if (kind === 'resultExpression') {
284
- const match = value.match(/\btoolcallresult\b/i);
285
- return match ? match[0] : value;
307
+ return getResultExpressionCasingMismatch(value) || value;
286
308
  }
287
309
 
288
310
  return value;