@rytass/bpm-core-nestjs-module 0.19.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.
Files changed (146) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/package.json +3 -2
  3. package/src/index.d.ts +1 -0
  4. package/src/index.js +1 -0
  5. package/src/index.js.map +1 -1
  6. package/src/lib/bpm/bpm-root-options.d.ts +13 -0
  7. package/src/lib/bpm/bpm-root-options.js.map +1 -1
  8. package/src/lib/bpm/bpm-root.module.d.ts +12 -0
  9. package/src/lib/bpm/bpm-root.module.js +5 -0
  10. package/src/lib/bpm/bpm-root.module.js.map +1 -1
  11. package/src/lib/condition/adapters/cel-legacy.adapter.d.ts +62 -0
  12. package/src/lib/condition/adapters/cel-legacy.adapter.js +141 -0
  13. package/src/lib/condition/adapters/cel-legacy.adapter.js.map +1 -0
  14. package/src/lib/condition/adapters/cel-v2.adapter.d.ts +76 -0
  15. package/src/lib/condition/adapters/cel-v2.adapter.js +371 -0
  16. package/src/lib/condition/adapters/cel-v2.adapter.js.map +1 -0
  17. package/src/lib/condition/condition.service.d.ts +90 -5
  18. package/src/lib/condition/condition.service.js +287 -71
  19. package/src/lib/condition/condition.service.js.map +1 -1
  20. package/src/lib/condition/expression-ast.d.ts +176 -0
  21. package/src/lib/condition/expression-ast.js +1254 -0
  22. package/src/lib/condition/expression-ast.js.map +1 -0
  23. package/src/lib/condition/expression-budget.d.ts +44 -0
  24. package/src/lib/condition/expression-budget.js +61 -0
  25. package/src/lib/condition/expression-budget.js.map +1 -0
  26. package/src/lib/condition/expression-dialect.d.ts +29 -0
  27. package/src/lib/condition/expression-dialect.js +44 -0
  28. package/src/lib/condition/expression-dialect.js.map +1 -0
  29. package/src/lib/condition/expression-differential.corpus.d.ts +34 -0
  30. package/src/lib/condition/expression-differential.corpus.js +995 -0
  31. package/src/lib/condition/expression-differential.corpus.js.map +1 -0
  32. package/src/lib/condition/expression-differential.d.ts +33 -0
  33. package/src/lib/condition/expression-differential.js +48 -0
  34. package/src/lib/condition/expression-differential.js.map +1 -0
  35. package/src/lib/condition/expression-engine-failure.d.ts +15 -0
  36. package/src/lib/condition/expression-engine-failure.js +22 -0
  37. package/src/lib/condition/expression-engine-failure.js.map +1 -0
  38. package/src/lib/condition/expression-errors.d.ts +40 -0
  39. package/src/lib/condition/expression-errors.js +105 -0
  40. package/src/lib/condition/expression-errors.js.map +1 -0
  41. package/src/lib/condition/expression-function-invocation.d.ts +50 -0
  42. package/src/lib/condition/expression-function-invocation.js +290 -0
  43. package/src/lib/condition/expression-function-invocation.js.map +1 -0
  44. package/src/lib/condition/expression-result.walker.d.ts +18 -0
  45. package/src/lib/condition/expression-result.walker.js +152 -0
  46. package/src/lib/condition/expression-result.walker.js.map +1 -0
  47. package/src/lib/condition/expression-runtime.d.ts +70 -0
  48. package/src/lib/condition/expression-runtime.js +134 -0
  49. package/src/lib/condition/expression-runtime.js.map +1 -0
  50. package/src/lib/condition/index.d.ts +4 -0
  51. package/src/lib/condition/index.js +8 -0
  52. package/src/lib/condition/index.js.map +1 -1
  53. package/src/lib/delegation/delegation-rule.entity.d.ts +7 -0
  54. package/src/lib/delegation/delegation-rule.entity.js +10 -0
  55. package/src/lib/delegation/delegation-rule.entity.js.map +1 -1
  56. package/src/lib/delegation/delegation.service.d.ts +7 -1
  57. package/src/lib/delegation/delegation.service.js +22 -5
  58. package/src/lib/delegation/delegation.service.js.map +1 -1
  59. package/src/lib/delegation/dto/delegation-rule.input.d.ts +2 -0
  60. package/src/lib/delegation/dto/delegation-rule.input.js +10 -0
  61. package/src/lib/delegation/dto/delegation-rule.input.js.map +1 -1
  62. package/src/lib/expression-function/expression-function.built-ins.d.ts +9 -0
  63. package/src/lib/expression-function/expression-function.built-ins.js +117 -0
  64. package/src/lib/expression-function/expression-function.built-ins.js.map +1 -0
  65. package/src/lib/expression-function/expression-function.module.d.ts +19 -0
  66. package/src/lib/expression-function/expression-function.module.js +34 -0
  67. package/src/lib/expression-function/expression-function.module.js.map +1 -0
  68. package/src/lib/expression-function/expression-function.provider.d.ts +17 -0
  69. package/src/lib/expression-function/expression-function.provider.js +39 -0
  70. package/src/lib/expression-function/expression-function.provider.js.map +1 -0
  71. package/src/lib/expression-function/expression-function.registry.d.ts +36 -0
  72. package/src/lib/expression-function/expression-function.registry.js +231 -0
  73. package/src/lib/expression-function/expression-function.registry.js.map +1 -0
  74. package/src/lib/expression-function/expression-function.types.d.ts +30 -0
  75. package/src/lib/expression-function/expression-function.types.js +8 -0
  76. package/src/lib/expression-function/expression-function.types.js.map +1 -0
  77. package/src/lib/expression-function/index.d.ts +5 -0
  78. package/src/lib/expression-function/index.js +9 -0
  79. package/src/lib/expression-function/index.js.map +1 -0
  80. package/src/lib/form/form-schema.validator.js +53 -6
  81. package/src/lib/form/form-schema.validator.js.map +1 -1
  82. package/src/lib/form-data-source/form-data-source-value-resolver.service.d.ts +12 -1
  83. package/src/lib/form-data-source/form-data-source-value-resolver.service.js +45 -26
  84. package/src/lib/form-data-source/form-data-source-value-resolver.service.js.map +1 -1
  85. package/src/lib/form-data-source/form-data-source.expression.d.ts +95 -0
  86. package/src/lib/form-data-source/form-data-source.expression.js +267 -0
  87. package/src/lib/form-data-source/form-data-source.expression.js.map +1 -0
  88. package/src/lib/form-data-source/form-data-source.module.js +4 -0
  89. package/src/lib/form-data-source/form-data-source.module.js.map +1 -1
  90. package/src/lib/form-data-source/form-data-source.service.d.ts +22 -3
  91. package/src/lib/form-data-source/form-data-source.service.js +78 -18
  92. package/src/lib/form-data-source/form-data-source.service.js.map +1 -1
  93. package/src/lib/form-data-source/form-data-source.validation.d.ts +47 -4
  94. package/src/lib/form-data-source/form-data-source.validation.js +151 -23
  95. package/src/lib/form-data-source/form-data-source.validation.js.map +1 -1
  96. package/src/lib/migrations/0000000030000-delegation-scope-condition-dialect.d.ts +22 -0
  97. package/src/lib/migrations/0000000030000-delegation-scope-condition-dialect.js +76 -0
  98. package/src/lib/migrations/0000000030000-delegation-scope-condition-dialect.js.map +1 -0
  99. package/src/lib/migrations/index.d.ts +1 -0
  100. package/src/lib/migrations/index.js +3 -0
  101. package/src/lib/migrations/index.js.map +1 -1
  102. package/src/lib/notification/notification.service.d.ts +1 -1
  103. package/src/lib/notification/notification.service.js +7 -3
  104. package/src/lib/notification/notification.service.js.map +1 -1
  105. package/src/lib/template/dto/expression-designer.dto.d.ts +107 -0
  106. package/src/lib/template/dto/expression-designer.dto.js +415 -0
  107. package/src/lib/template/dto/expression-designer.dto.js.map +1 -0
  108. package/src/lib/template/expression-designer.queries.d.ts +16 -0
  109. package/src/lib/template/expression-designer.queries.js +66 -0
  110. package/src/lib/template/expression-designer.queries.js.map +1 -0
  111. package/src/lib/template/expression-designer.service.d.ts +72 -0
  112. package/src/lib/template/expression-designer.service.js +543 -0
  113. package/src/lib/template/expression-designer.service.js.map +1 -0
  114. package/src/lib/template/index.d.ts +2 -0
  115. package/src/lib/template/index.js +2 -0
  116. package/src/lib/template/index.js.map +1 -1
  117. package/src/lib/template/template.module.js +4 -0
  118. package/src/lib/template/template.module.js.map +1 -1
  119. package/src/lib/template/template.service.d.ts +37 -1
  120. package/src/lib/template/template.service.js +317 -29
  121. package/src/lib/template/template.service.js.map +1 -1
  122. package/src/lib/template/workflow-definition.validator.d.ts +7 -0
  123. package/src/lib/template/workflow-definition.validator.js +26 -2
  124. package/src/lib/template/workflow-definition.validator.js.map +1 -1
  125. package/src/lib/workflow-engine/time-budget.d.ts +13 -0
  126. package/src/lib/workflow-engine/time-budget.js +58 -0
  127. package/src/lib/workflow-engine/time-budget.js.map +1 -0
  128. package/src/lib/workflow-engine/workflow-engine.service.d.ts +24 -0
  129. package/src/lib/workflow-engine/workflow-engine.service.js +240 -65
  130. package/src/lib/workflow-engine/workflow-engine.service.js.map +1 -1
  131. package/src/lib/workflow-engine/workflow-service-task-dispatcher.token.js +4 -0
  132. package/src/lib/workflow-engine/workflow-service-task-dispatcher.token.js.map +1 -1
  133. package/src/lib/workflow-webhook/workflow-webhook-delivery.service.d.ts +3 -1
  134. package/src/lib/workflow-webhook/workflow-webhook-delivery.service.js +11 -2
  135. package/src/lib/workflow-webhook/workflow-webhook-delivery.service.js.map +1 -1
  136. package/src/lib/workflow-webhook/workflow-webhook-enqueue.d.ts +8 -1
  137. package/src/lib/workflow-webhook/workflow-webhook-enqueue.js +70 -27
  138. package/src/lib/workflow-webhook/workflow-webhook-enqueue.js.map +1 -1
  139. package/src/lib/workflow-webhook/workflow-webhook-expression.d.ts +57 -0
  140. package/src/lib/workflow-webhook/workflow-webhook-expression.js +123 -0
  141. package/src/lib/workflow-webhook/workflow-webhook-expression.js.map +1 -0
  142. package/src/lib/workflow-webhook/workflow-webhook.module.js +4 -0
  143. package/src/lib/workflow-webhook/workflow-webhook.module.js.map +1 -1
  144. package/src/lib/workflow-webhook/workflow-webhook.validator.d.ts +7 -0
  145. package/src/lib/workflow-webhook/workflow-webhook.validator.js +29 -6
  146. package/src/lib/workflow-webhook/workflow-webhook.validator.js.map +1 -1
@@ -0,0 +1,1254 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readExpressionFieldType = exports.readsTableInternals = exports.readsFormField = exports.readFormFieldKeys = exports.EXPRESSION_MAX_COMPREHENSION_DEPTH = exports.RESERVED_PATH_SEGMENTS = void 0;
4
+ exports.readExpressionReferences = readExpressionReferences;
5
+ exports.readUnavailableExpressionFunctions = readUnavailableExpressionFunctions;
6
+ exports.lintCelV2Expression = lintCelV2Expression;
7
+ exports.readExpressionHostFunctionNames = readExpressionHostFunctionNames;
8
+ const common_1 = require("@nestjs/common");
9
+ const bpm_core_shared_1 = require("@rytass/bpm-core-shared");
10
+ const form_1 = require("@rytass/bpm-core-shared/form");
11
+ const expression_function_registry_1 = require("../expression-function/expression-function.registry");
12
+ const cel_legacy_adapter_1 = require("./adapters/cel-legacy.adapter");
13
+ const cel_v2_adapter_1 = require("./adapters/cel-v2.adapter");
14
+ const expression_function_invocation_1 = require("./expression-function-invocation");
15
+ const expression_dialect_1 = require("./expression-dialect");
16
+ exports.RESERVED_PATH_SEGMENTS = new Set([
17
+ '__proto__',
18
+ 'constructor',
19
+ 'prototype',
20
+ ]);
21
+ /** Roots the lint refuses to see used as a whole value (ADR 22 §3.4). */
22
+ const WHOLE_REFERENCE_ROOTS = new Set([
23
+ 'form',
24
+ 'formData',
25
+ 'initiator',
26
+ 'row',
27
+ ]);
28
+ const FORM_ROOTS = new Set(['form', 'formData']);
29
+ const COMPREHENSION_MACROS = new Set([
30
+ 'all',
31
+ 'exists',
32
+ 'exists_one',
33
+ 'filter',
34
+ 'map',
35
+ ]);
36
+ /** ADR 22 §3.4 / docs/24 §2.7. */
37
+ exports.EXPRESSION_MAX_COMPREHENSION_DEPTH = 2;
38
+ const createCollector = () => ({
39
+ comprehensionDepth: 0,
40
+ computedKeys: [],
41
+ divisorFieldPaths: [],
42
+ fieldPaths: [],
43
+ functionCalls: [],
44
+ guardedFieldPaths: [],
45
+ hasArguments: [],
46
+ nodeCount: 0,
47
+ orOperandFieldPaths: [],
48
+ receiverPaths: [],
49
+ reservedSegments: new Set(),
50
+ roots: new Set(),
51
+ wholeRootReferences: new Set(),
52
+ });
53
+ const toReferences = (collector, dialect) => ({
54
+ comprehensionDepth: collector.comprehensionDepth,
55
+ computedKeys: [...collector.computedKeys],
56
+ dialect,
57
+ divisorFieldPaths: [...collector.divisorFieldPaths],
58
+ fieldPaths: [...collector.fieldPaths],
59
+ formFieldKeys: (0, exports.readFormFieldKeys)(collector.fieldPaths),
60
+ functionCalls: [...collector.functionCalls],
61
+ guardedFieldPaths: [...collector.guardedFieldPaths],
62
+ hasArguments: [...collector.hasArguments],
63
+ nodeCount: collector.nodeCount,
64
+ orOperandFieldPaths: [...collector.orOperandFieldPaths],
65
+ receiverPaths: [...collector.receiverPaths],
66
+ reservedSegments: [...collector.reservedSegments].sort(),
67
+ roots: [...collector.roots].sort(),
68
+ wholeRootReferences: [...collector.wholeRootReferences].sort(),
69
+ });
70
+ /** Unique top-level keys of the paths rooted at `form` or `formData`, in first-seen order. */
71
+ const readFormFieldKeys = (paths) => [
72
+ ...new Set(paths
73
+ .filter((path) => FORM_ROOTS.has(path.root) && path.segments.length > 0)
74
+ .map((path) => path.segments[0])),
75
+ ];
76
+ exports.readFormFieldKeys = readFormFieldKeys;
77
+ const noteReceiver = (collector, name, path) => {
78
+ if (path !== null) {
79
+ collector.receiverPaths.push({ name, path });
80
+ }
81
+ };
82
+ const noteReserved = (collector, segment) => {
83
+ if (exports.RESERVED_PATH_SEGMENTS.has(segment)) {
84
+ collector.reservedSegments.add(segment);
85
+ }
86
+ };
87
+ /**
88
+ * The roots, field paths, function calls and structure an expression uses,
89
+ * read from its AST without evaluating it. Names a comprehension binds (`x`
90
+ * in `form.items.exists(x, x.qty > 0)`) are only in scope inside it: the same
91
+ * name outside it is a root.
92
+ *
93
+ * Works for both dialects. cel-v2 reads the engine's AST; cel-legacy walks the
94
+ * CST of `cel-js` and answers `UNAVAILABLE` when there is none to walk.
95
+ */
96
+ function readExpressionReferences(expression, dialect = expression_dialect_1.DEFAULT_EXPRESSION_DIALECT) {
97
+ return dialect === 'cel-v2'
98
+ ? readCelV2References(expression)
99
+ : readCelLegacyReferences(expression);
100
+ }
101
+ /**
102
+ * Parses with the shared cel-v2 environment. The engine applies the AST node
103
+ * and depth limits while parsing; breaking one is a `limit_exceeded` error,
104
+ * told apart here so the lint can call it complexity rather than syntax. The
105
+ * error's message is never read: it can echo the expression.
106
+ */
107
+ const parseCelV2Ast = (expression, environment = (0, cel_v2_adapter_1.loadCelV2Environment)()) => {
108
+ try {
109
+ return { ast: environment.parse(expression).ast, status: 'OK' };
110
+ }
111
+ catch (error) {
112
+ return {
113
+ limitExceeded: typeof error === 'object' &&
114
+ error !== null &&
115
+ error.code === 'limit_exceeded',
116
+ status: 'PARSE_FAILED',
117
+ };
118
+ }
119
+ };
120
+ function readCelV2References(expression, environment) {
121
+ const parsed = parseCelV2Ast(expression, environment);
122
+ if (parsed.status !== 'OK') {
123
+ return parsed;
124
+ }
125
+ return {
126
+ references: collectCelV2References(parsed.ast),
127
+ status: 'OK',
128
+ };
129
+ }
130
+ const collectCelV2References = (ast) => {
131
+ const collector = createCollector();
132
+ visitCelV2(ast, new Set(), 0, collector);
133
+ return toReferences(collector, 'cel-v2');
134
+ };
135
+ const readCelV2Chain = (node) => {
136
+ if (node.op === '.' || node.op === '.?') {
137
+ const inner = readCelV2Chain(node.args[0]);
138
+ return {
139
+ base: inner.base,
140
+ steps: [...inner.steps, { kind: 'member', name: node.args[1] }],
141
+ };
142
+ }
143
+ if (node.op === '[]' || node.op === '[?]') {
144
+ const inner = readCelV2Chain(node.args[0]);
145
+ return {
146
+ base: inner.base,
147
+ steps: [...inner.steps, { index: node.args[1], kind: 'index' }],
148
+ };
149
+ }
150
+ return { base: node, steps: [] };
151
+ };
152
+ /** A literal index or key as a path segment, or `null` when it is computed. */
153
+ const readCelV2LiteralKey = (node) => {
154
+ if (node.op !== 'value') {
155
+ return null;
156
+ }
157
+ if (typeof node.args === 'string') {
158
+ return { key: node.args, string: true };
159
+ }
160
+ return typeof node.args === 'bigint' || typeof node.args === 'number'
161
+ ? { key: String(node.args), string: false }
162
+ : null;
163
+ };
164
+ const isCelV2EmptyLiteral = (node) => (node.op === 'value' && (node.args === null || node.args === '')) ||
165
+ (node.op === 'list' && node.args.length === 0);
166
+ const visitCelV2Access = (node, scope, depth, collector) => {
167
+ const { base, steps } = readCelV2Chain(node);
168
+ const rootName = base.op === 'id' && !scope.has(base.args) ? base.args : null;
169
+ collector.nodeCount += steps.length;
170
+ if (base.op === 'id') {
171
+ collector.nodeCount += 1;
172
+ if (rootName !== null) {
173
+ collector.roots.add(rootName);
174
+ }
175
+ }
176
+ else {
177
+ visitCelV2(base, scope, depth, collector);
178
+ }
179
+ const { complete, segments } = steps.reduce((state, step) => {
180
+ if (step.kind === 'member') {
181
+ noteReserved(collector, step.name);
182
+ return state.complete
183
+ ? { complete: true, segments: [...state.segments, step.name] }
184
+ : state;
185
+ }
186
+ const literal = readCelV2LiteralKey(step.index);
187
+ if (literal !== null) {
188
+ collector.nodeCount += 1;
189
+ if (literal.string) {
190
+ noteReserved(collector, literal.key);
191
+ }
192
+ return state.complete
193
+ ? { complete: true, segments: [...state.segments, literal.key] }
194
+ : state;
195
+ }
196
+ collector.computedKeys.push({ kind: 'INDEX', root: rootName });
197
+ visitCelV2(step.index, scope, depth, collector);
198
+ return { complete: false, segments: state.segments };
199
+ }, { complete: true, segments: [] });
200
+ if (rootName === null) {
201
+ return null;
202
+ }
203
+ const path = { complete, root: rootName, segments };
204
+ collector.fieldPaths.push(path);
205
+ return path;
206
+ };
207
+ const visitCelV2Children = (nodes, scope, depth, collector) => {
208
+ nodes.forEach((child) => {
209
+ visitCelV2(child, scope, depth, collector);
210
+ });
211
+ };
212
+ /**
213
+ * Visits one node. Returns the field path when the node is a path from a
214
+ * root, so a caller can tell what `has(...)` or `== null` is about.
215
+ */
216
+ function visitCelV2(node, scope, depth, collector) {
217
+ switch (node.op) {
218
+ case 'value':
219
+ collector.nodeCount += 1;
220
+ return null;
221
+ case 'id':
222
+ collector.nodeCount += 1;
223
+ if (!scope.has(node.args)) {
224
+ collector.roots.add(node.args);
225
+ collector.wholeRootReferences.add(node.args);
226
+ }
227
+ return null;
228
+ case '.':
229
+ case '.?':
230
+ case '[]':
231
+ case '[?]':
232
+ return visitCelV2Access(node, scope, depth, collector);
233
+ case 'call': {
234
+ const [name, args] = node.args;
235
+ collector.nodeCount += 1;
236
+ if (name === 'has' && args.length === 1) {
237
+ const path = visitCelV2(args[0], scope, depth, collector);
238
+ if (path !== null) {
239
+ collector.hasArguments.push(path);
240
+ collector.guardedFieldPaths.push(path);
241
+ }
242
+ return null;
243
+ }
244
+ collector.functionCalls.push({
245
+ argumentCount: args.length,
246
+ name,
247
+ receiver: false,
248
+ });
249
+ visitCelV2Children(args, scope, depth, collector);
250
+ return null;
251
+ }
252
+ case 'rcall': {
253
+ const [name, receiver, args] = node.args;
254
+ const bound = args[0];
255
+ collector.nodeCount += 1;
256
+ if (COMPREHENSION_MACROS.has(name) &&
257
+ bound?.op === 'id' &&
258
+ (args.length === 2 || args.length === 3)) {
259
+ const innerDepth = depth + 1;
260
+ collector.comprehensionDepth = Math.max(collector.comprehensionDepth, innerDepth);
261
+ collector.nodeCount += 1;
262
+ noteReceiver(collector, name, visitCelV2(receiver, scope, depth, collector));
263
+ visitCelV2Children(args.slice(1), new Set([...scope, bound.args]), innerDepth, collector);
264
+ return null;
265
+ }
266
+ // `cel.bind(v, init, body)` is deliberately not a scope here: the
267
+ // evaluation guard (`ConditionService`) refuses `cel` as a root, so the
268
+ // lint reports `cel` and `v` as roots too rather than pass what every
269
+ // case would then fail on.
270
+ collector.functionCalls.push({
271
+ argumentCount: args.length,
272
+ name,
273
+ receiver: true,
274
+ });
275
+ noteReceiver(collector, name, visitCelV2(receiver, scope, depth, collector));
276
+ visitCelV2Children(args, scope, depth, collector);
277
+ return null;
278
+ }
279
+ case 'list':
280
+ collector.nodeCount += 1;
281
+ visitCelV2Children(node.args, scope, depth, collector);
282
+ return null;
283
+ case 'map':
284
+ collector.nodeCount += 1;
285
+ node.args.forEach(([key, value]) => {
286
+ if (key.op === 'value') {
287
+ collector.nodeCount += 1;
288
+ if (typeof key.args === 'string') {
289
+ noteReserved(collector, key.args);
290
+ }
291
+ }
292
+ else {
293
+ collector.computedKeys.push({ kind: 'MAP_KEY', root: null });
294
+ visitCelV2(key, scope, depth, collector);
295
+ }
296
+ visitCelV2(value, scope, depth, collector);
297
+ });
298
+ return null;
299
+ case '||': {
300
+ const start = collector.fieldPaths.length;
301
+ collector.nodeCount += 1;
302
+ visitCelV2Children(node.args, scope, depth, collector);
303
+ collector.orOperandFieldPaths.push(...collector.fieldPaths.slice(start));
304
+ return null;
305
+ }
306
+ case '/': {
307
+ collector.nodeCount += 1;
308
+ visitCelV2(node.args[0], scope, depth, collector);
309
+ const start = collector.fieldPaths.length;
310
+ visitCelV2(node.args[1], scope, depth, collector);
311
+ collector.divisorFieldPaths.push(...collector.fieldPaths.slice(start));
312
+ return null;
313
+ }
314
+ case '==':
315
+ case '!=': {
316
+ const [left, right] = node.args;
317
+ collector.nodeCount += 1;
318
+ const leftPath = visitCelV2(left, scope, depth, collector);
319
+ const rightPath = visitCelV2(right, scope, depth, collector);
320
+ if (leftPath !== null && isCelV2EmptyLiteral(right)) {
321
+ collector.guardedFieldPaths.push(leftPath);
322
+ }
323
+ if (rightPath !== null && isCelV2EmptyLiteral(left)) {
324
+ collector.guardedFieldPaths.push(rightPath);
325
+ }
326
+ return null;
327
+ }
328
+ case '!_':
329
+ case '-_':
330
+ collector.nodeCount += 1;
331
+ visitCelV2(node.args, scope, depth, collector);
332
+ return null;
333
+ case '?:':
334
+ collector.nodeCount += 1;
335
+ visitCelV2Children(node.args, scope, depth, collector);
336
+ return null;
337
+ default:
338
+ // The remaining operators are binary: `&&`, `in`, `+ - * %`, `< <= > >=`.
339
+ collector.nodeCount += 1;
340
+ visitCelV2Children(node.args, scope, depth, collector);
341
+ return null;
342
+ }
343
+ }
344
+ const isLegacyCstNode = (value) => typeof value === 'object' &&
345
+ value !== null &&
346
+ typeof value.name === 'string' &&
347
+ typeof value.children === 'object' &&
348
+ value.children !== null;
349
+ const isLegacyToken = (value) => typeof value.image === 'string';
350
+ const readLegacyNodes = (node, key) => (node.children[key] ?? []).filter(isLegacyCstNode);
351
+ const readLegacyTokens = (node, key) => (node.children[key] ?? []).filter(isLegacyToken);
352
+ const readLegacyElements = (node) => Object.values(node.children).flat();
353
+ function readCelLegacyReferences(expression) {
354
+ const handle = (0, cel_legacy_adapter_1.parseCelLegacy)(expression);
355
+ if (handle === null) {
356
+ return { limitExceeded: false, status: 'PARSE_FAILED' };
357
+ }
358
+ if (!isLegacyCstNode(handle.native)) {
359
+ return { status: 'UNAVAILABLE' };
360
+ }
361
+ const collector = createCollector();
362
+ visitLegacy(handle.native, new Set(), 0, collector);
363
+ return { references: toReferences(collector, 'cel-legacy'), status: 'OK' };
364
+ }
365
+ /** The one wrapped atom of a trivial chain (`expr` → … → `atomicExpression`). */
366
+ const unwrapLegacyAtom = (node) => {
367
+ const elements = readLegacyElements(node);
368
+ if (node.name === 'atomicExpression') {
369
+ return elements.length === 1 ? elements[0] : null;
370
+ }
371
+ if (node.name === 'parenthesisExpression') {
372
+ const inner = readLegacyNodes(node, 'expr')[0];
373
+ return inner ? unwrapLegacyAtom(inner) : null;
374
+ }
375
+ if (elements.length !== 1) {
376
+ return null;
377
+ }
378
+ const [only] = elements;
379
+ return isLegacyCstNode(only) ? unwrapLegacyAtom(only) : null;
380
+ };
381
+ /** cel-legacy does not decode escapes (P1 G25), so only plain literals count. */
382
+ const readLegacyStringLiteral = (token) => {
383
+ if (token.tokenType.name !== 'StringLiteral') {
384
+ return null;
385
+ }
386
+ const match = /^"([^"\\]*)"$|^'([^'\\]*)'$/u.exec(token.image);
387
+ return match ? (match[1] ?? match[2] ?? '') : null;
388
+ };
389
+ const readLegacyLiteralKey = (expression) => {
390
+ const atom = unwrapLegacyAtom(expression);
391
+ if (atom === null || !isLegacyToken(atom)) {
392
+ return null;
393
+ }
394
+ const text = readLegacyStringLiteral(atom);
395
+ if (text !== null) {
396
+ return { key: text, string: true };
397
+ }
398
+ return atom.tokenType.name === 'Integer' ||
399
+ atom.tokenType.name === 'UnsignedInteger'
400
+ ? { key: atom.image.replace(/[uU]$/u, ''), string: false }
401
+ : null;
402
+ };
403
+ const isLegacyEmptyLiteral = (node) => {
404
+ const atom = unwrapLegacyAtom(node);
405
+ if (atom === null) {
406
+ return false;
407
+ }
408
+ if (isLegacyToken(atom)) {
409
+ return (atom.tokenType.name === 'Null' || readLegacyStringLiteral(atom) === '');
410
+ }
411
+ return (atom.name === 'listExpression' && readLegacyNodes(atom, 'lhs').length === 0);
412
+ };
413
+ /** The bare identifier an `expr` is, if it is nothing else. */
414
+ const readLegacyBareIdentifier = (node) => {
415
+ const atom = unwrapLegacyAtom(node);
416
+ if (atom === null ||
417
+ isLegacyToken(atom) ||
418
+ atom.name !== 'identifierExpression' ||
419
+ readLegacyElements(atom).length !== 1) {
420
+ return null;
421
+ }
422
+ return readLegacyTokens(atom, 'Identifier')[0]?.image ?? null;
423
+ };
424
+ const readLegacyCallArguments = (node) => [
425
+ ...readLegacyNodes(node, 'arg'),
426
+ ...readLegacyNodes(node, 'args'),
427
+ ];
428
+ const visitLegacyAll = (nodes, scope, depth, collector) => {
429
+ nodes.forEach((child) => {
430
+ visitLegacy(child, scope, depth, collector);
431
+ });
432
+ };
433
+ /** `lhs` followed by any number of `rhs`: an n-ary operator chain. */
434
+ const visitLegacyChain = (node, scope, depth, collector) => {
435
+ const lhs = readLegacyNodes(node, 'lhs');
436
+ const rhs = readLegacyNodes(node, 'rhs');
437
+ if (rhs.length === 0) {
438
+ return lhs[0] ? visitLegacy(lhs[0], scope, depth, collector) : null;
439
+ }
440
+ collector.nodeCount += rhs.length;
441
+ visitLegacyAll([...lhs, ...rhs], scope, depth, collector);
442
+ return null;
443
+ };
444
+ /** The member and index steps after an identifier, in source order. */
445
+ const readLegacySteps = (node) => {
446
+ const offsetOf = (step) => {
447
+ const first = readLegacyElements(step).find(isLegacyToken);
448
+ return first?.startOffset ?? 0;
449
+ };
450
+ return [
451
+ ...readLegacyNodes(node, 'identifierDotExpression'),
452
+ ...readLegacyNodes(node, 'identifierIndexExpression'),
453
+ ...readLegacyNodes(node, 'indexExpression'),
454
+ ...readLegacyNodes(node, 'Index'),
455
+ ].sort((left, right) => offsetOf(left) - offsetOf(right));
456
+ };
457
+ const visitLegacyStep = (step, state, rootName, scope, depth, collector) => {
458
+ collector.nodeCount += 1;
459
+ if (step.name !== 'identifierDotExpression') {
460
+ const index = readLegacyNodes(step, 'expr')[0];
461
+ const literal = index ? readLegacyLiteralKey(index) : null;
462
+ if (literal !== null) {
463
+ if (literal.string) {
464
+ noteReserved(collector, literal.key);
465
+ }
466
+ return state.complete
467
+ ? { complete: true, segments: [...state.segments, literal.key] }
468
+ : state;
469
+ }
470
+ collector.computedKeys.push({ kind: 'INDEX', root: rootName });
471
+ if (index) {
472
+ visitLegacy(index, scope, depth, collector);
473
+ }
474
+ return { complete: false, segments: state.segments };
475
+ }
476
+ const name = readLegacyTokens(step, 'Identifier')[0]?.image ?? '';
477
+ if (readLegacyTokens(step, 'OpenParenthesis').length === 0) {
478
+ noteReserved(collector, name);
479
+ return state.complete
480
+ ? { complete: true, segments: [...state.segments, name] }
481
+ : state;
482
+ }
483
+ const args = readLegacyCallArguments(step);
484
+ const bound = args[0] ? readLegacyBareIdentifier(args[0]) : null;
485
+ if (rootName !== null && state.complete) {
486
+ // A method called on the root itself uses the root as a value.
487
+ if (state.segments.length === 0) {
488
+ collector.wholeRootReferences.add(rootName);
489
+ }
490
+ noteReceiver(collector, name, {
491
+ complete: true,
492
+ root: rootName,
493
+ segments: state.segments,
494
+ });
495
+ }
496
+ if (COMPREHENSION_MACROS.has(name) && bound !== null && args.length >= 2) {
497
+ const innerDepth = depth + 1;
498
+ collector.comprehensionDepth = Math.max(collector.comprehensionDepth, innerDepth);
499
+ visitLegacyAll(args.slice(1), new Set([...scope, bound]), innerDepth, collector);
500
+ }
501
+ else {
502
+ collector.functionCalls.push({
503
+ argumentCount: args.length,
504
+ name,
505
+ receiver: true,
506
+ });
507
+ visitLegacyAll(args, scope, depth, collector);
508
+ }
509
+ return { complete: false, segments: state.segments };
510
+ };
511
+ const visitLegacySteps = (steps, rootName, scope, depth, collector) => steps.reduce((state, step) => visitLegacyStep(step, state, rootName, scope, depth, collector), { complete: true, segments: [] });
512
+ function visitLegacy(node, scope, depth, collector) {
513
+ switch (node.name) {
514
+ case 'expr': {
515
+ const condition = readLegacyNodes(node, 'conditionalOr')[0];
516
+ const branches = [
517
+ ...readLegacyNodes(node, 'lhs'),
518
+ ...readLegacyNodes(node, 'rhs'),
519
+ ];
520
+ const path = condition
521
+ ? visitLegacy(condition, scope, depth, collector)
522
+ : null;
523
+ if (branches.length === 0) {
524
+ return path;
525
+ }
526
+ collector.nodeCount += 1;
527
+ visitLegacyAll(branches, scope, depth, collector);
528
+ return null;
529
+ }
530
+ case 'conditionalOr': {
531
+ if (readLegacyNodes(node, 'rhs').length === 0) {
532
+ return visitLegacyChain(node, scope, depth, collector);
533
+ }
534
+ const start = collector.fieldPaths.length;
535
+ visitLegacyChain(node, scope, depth, collector);
536
+ collector.orOperandFieldPaths.push(...collector.fieldPaths.slice(start));
537
+ return null;
538
+ }
539
+ case 'conditionalAnd':
540
+ case 'addition':
541
+ return visitLegacyChain(node, scope, depth, collector);
542
+ case 'relation': {
543
+ const [left] = readLegacyNodes(node, 'lhs');
544
+ const [right] = readLegacyNodes(node, 'rhs');
545
+ const [operator] = readLegacyTokens(node, 'ComparisonOperator');
546
+ if (!right || !left) {
547
+ return left ? visitLegacy(left, scope, depth, collector) : null;
548
+ }
549
+ collector.nodeCount += 1;
550
+ const leftPath = visitLegacy(left, scope, depth, collector);
551
+ const rightPath = visitLegacy(right, scope, depth, collector);
552
+ if (operator?.image === '==' || operator?.image === '!=') {
553
+ if (leftPath !== null && isLegacyEmptyLiteral(right)) {
554
+ collector.guardedFieldPaths.push(leftPath);
555
+ }
556
+ if (rightPath !== null && isLegacyEmptyLiteral(left)) {
557
+ collector.guardedFieldPaths.push(rightPath);
558
+ }
559
+ }
560
+ return null;
561
+ }
562
+ case 'multiplication': {
563
+ const [left] = readLegacyNodes(node, 'lhs');
564
+ const rhs = readLegacyNodes(node, 'rhs');
565
+ const operators = readLegacyTokens(node, 'MultiplicationOperator');
566
+ if (rhs.length === 0) {
567
+ return left ? visitLegacy(left, scope, depth, collector) : null;
568
+ }
569
+ collector.nodeCount += rhs.length;
570
+ if (left) {
571
+ visitLegacy(left, scope, depth, collector);
572
+ }
573
+ rhs.forEach((operand, index) => {
574
+ const start = collector.fieldPaths.length;
575
+ visitLegacy(operand, scope, depth, collector);
576
+ if (operators[index]?.image === '/') {
577
+ collector.divisorFieldPaths.push(...collector.fieldPaths.slice(start));
578
+ }
579
+ });
580
+ return null;
581
+ }
582
+ case 'unaryExpression': {
583
+ const [atom] = readLegacyNodes(node, 'atomicExpression');
584
+ if (!atom) {
585
+ return null;
586
+ }
587
+ if (readLegacyTokens(node, 'UnaryOperator').length === 0) {
588
+ return visitLegacy(atom, scope, depth, collector);
589
+ }
590
+ collector.nodeCount += 1;
591
+ visitLegacy(atom, scope, depth, collector);
592
+ return null;
593
+ }
594
+ case 'atomicExpression': {
595
+ const [child] = readLegacyElements(node);
596
+ if (!child) {
597
+ return null;
598
+ }
599
+ if (isLegacyToken(child)) {
600
+ collector.nodeCount += 1;
601
+ return null;
602
+ }
603
+ return visitLegacy(child, scope, depth, collector);
604
+ }
605
+ case 'parenthesisExpression': {
606
+ const [inner] = readLegacyNodes(node, 'expr');
607
+ return inner ? visitLegacy(inner, scope, depth, collector) : null;
608
+ }
609
+ case 'listExpression': {
610
+ collector.nodeCount += 1;
611
+ visitLegacyAll([...readLegacyNodes(node, 'lhs'), ...readLegacyNodes(node, 'rhs')], scope, depth, collector);
612
+ visitLegacySteps(readLegacySteps(node), null, scope, depth, collector);
613
+ return null;
614
+ }
615
+ case 'mapExpression': {
616
+ collector.nodeCount += 1;
617
+ readLegacyNodes(node, 'keyValues').forEach((entry) => {
618
+ const [key] = readLegacyNodes(entry, 'key');
619
+ const [value] = readLegacyNodes(entry, 'value');
620
+ const literal = key ? readLegacyLiteralKey(key) : null;
621
+ collector.nodeCount += 1;
622
+ const atom = key ? unwrapLegacyAtom(key) : null;
623
+ if (literal !== null) {
624
+ if (literal.string) {
625
+ noteReserved(collector, literal.key);
626
+ }
627
+ }
628
+ else if (key && (atom === null || !isLegacyToken(atom))) {
629
+ // Any literal token (`true`, `1.5`) is still a literal key.
630
+ collector.computedKeys.push({ kind: 'MAP_KEY', root: null });
631
+ visitLegacy(key, scope, depth, collector);
632
+ }
633
+ if (value) {
634
+ visitLegacy(value, scope, depth, collector);
635
+ }
636
+ });
637
+ visitLegacySteps(readLegacySteps(node), null, scope, depth, collector);
638
+ return null;
639
+ }
640
+ case 'macrosExpression': {
641
+ const name = readLegacyTokens(node, 'Identifier')[0]?.image ?? '';
642
+ const args = readLegacyCallArguments(node);
643
+ collector.nodeCount += 1;
644
+ if (name === 'has' && args.length === 1) {
645
+ const path = visitLegacy(args[0], scope, depth, collector);
646
+ if (path !== null) {
647
+ collector.hasArguments.push(path);
648
+ collector.guardedFieldPaths.push(path);
649
+ }
650
+ return null;
651
+ }
652
+ collector.functionCalls.push({
653
+ argumentCount: args.length,
654
+ name,
655
+ receiver: false,
656
+ });
657
+ visitLegacyAll(args, scope, depth, collector);
658
+ return null;
659
+ }
660
+ case 'identifierExpression': {
661
+ const name = readLegacyTokens(node, 'Identifier')[0]?.image ?? '';
662
+ const rootName = scope.has(name) ? null : name;
663
+ const steps = readLegacySteps(node);
664
+ collector.nodeCount += 1;
665
+ if (rootName !== null) {
666
+ collector.roots.add(rootName);
667
+ }
668
+ if (steps.length === 0) {
669
+ if (rootName !== null) {
670
+ collector.wholeRootReferences.add(rootName);
671
+ }
672
+ return null;
673
+ }
674
+ const { complete, segments } = visitLegacySteps(steps, rootName, scope, depth, collector);
675
+ if (rootName === null) {
676
+ return null;
677
+ }
678
+ const path = { complete, root: rootName, segments };
679
+ collector.fieldPaths.push(path);
680
+ return path;
681
+ }
682
+ default:
683
+ visitLegacyAll(readLegacyElements(node).filter(isLegacyCstNode), scope, depth, collector);
684
+ return null;
685
+ }
686
+ }
687
+ /** Whether the expression reads the top-level form field `fieldKey`. */
688
+ const readsFormField = (references, fieldKey) => references.formFieldKeys.includes(fieldKey);
689
+ exports.readsFormField = readsFormField;
690
+ /**
691
+ * Whether the expression steps inside the table field `tableKey`
692
+ * (`form.items.qty`, `form.items[0]`, `form.items.exists(r, ...)`), the AST
693
+ * twin of `referencesTableInternals` (ADR 16 §3.8). The table as a whole
694
+ * (`size(form.items)`, `form.items.size()`, `form.items == null`) is not.
695
+ */
696
+ const readsTableInternals = (references, tableKey) => references.fieldPaths.some((path) => FORM_ROOTS.has(path.root) &&
697
+ path.segments[0] === tableKey &&
698
+ (path.segments.length > 1 || !path.complete)) ||
699
+ references.receiverPaths.some(({ name, path }) => name !== 'size' &&
700
+ FORM_ROOTS.has(path.root) &&
701
+ path.segments.length === 1 &&
702
+ path.segments[0] === tableKey);
703
+ exports.readsTableInternals = readsTableInternals;
704
+ /**
705
+ * The CEL types a form field is declared with for the lint's type check
706
+ * (docs/24-design/p2-p3 §2.2). Only names the engine accepts are produced;
707
+ * anything else is `dyn`. Never `optional(...)`: the engine cannot convert it.
708
+ */
709
+ const readExpressionFieldType = (field) => {
710
+ switch (field.type) {
711
+ case 'text':
712
+ case 'textarea':
713
+ case 'date':
714
+ case 'datetime':
715
+ case 'org_unit':
716
+ return 'string';
717
+ case 'number':
718
+ case 'money':
719
+ return 'double';
720
+ case 'boolean':
721
+ return 'bool';
722
+ case 'member':
723
+ return field.multiple === true ? 'list<string>' : 'string';
724
+ case 'select':
725
+ case 'autocomplete':
726
+ case 'radio':
727
+ case 'checkbox':
728
+ return (0, form_1.isFormOptionFieldDefinition)(field) &&
729
+ (0, form_1.readFormFieldSelectionMode)(field) === 'multiple'
730
+ ? 'list<string>'
731
+ : 'string';
732
+ case 'table':
733
+ return 'list<dyn>';
734
+ default:
735
+ return 'dyn';
736
+ }
737
+ };
738
+ exports.readExpressionFieldType = readExpressionFieldType;
739
+ const readSchemaFields = (schema) => Array.isArray(schema?.fields)
740
+ ? schema.fields.filter((field) => typeof field?.fieldKey === 'string' &&
741
+ !exports.RESERVED_PATH_SEGMENTS.has(field.fieldKey))
742
+ : [];
743
+ const readDefinedFunctionNames = (environment) => {
744
+ const functions = environment.getDefinitions().functions;
745
+ return {
746
+ globals: new Set(functions
747
+ .filter((definition) => !definition.receiverType)
748
+ .map((definition) => definition.name)),
749
+ methods: new Set(functions
750
+ .filter((definition) => Boolean(definition.receiverType))
751
+ .map((definition) => definition.name)),
752
+ };
753
+ };
754
+ let pristineFunctionNames = null;
755
+ /**
756
+ * The functions cel-v2 itself provides (`size`, `int`, `contains`, …), read
757
+ * from a fresh environment of the same class so that host functions a later
758
+ * environment may register are never mistaken for the engine's own.
759
+ */
760
+ const readEngineFunctionNames = () => {
761
+ if (pristineFunctionNames === null) {
762
+ const EnvironmentClass = (0, cel_v2_adapter_1.loadCelV2Environment)()
763
+ .constructor;
764
+ pristineFunctionNames = readDefinedFunctionNames(new EnvironmentClass());
765
+ }
766
+ return pristineFunctionNames;
767
+ };
768
+ const knownFunctionNamesByEnvironment = new WeakMap();
769
+ /** Global functions the shared environment can type-check today. */
770
+ const readCheckableGlobalNames = (environment) => {
771
+ const cached = knownFunctionNamesByEnvironment.get(environment);
772
+ if (cached) {
773
+ return cached;
774
+ }
775
+ const names = readDefinedFunctionNames(environment).globals;
776
+ knownFunctionNamesByEnvironment.set(environment, names);
777
+ return names;
778
+ };
779
+ const issue = (code, label, subject) => ({
780
+ code,
781
+ label,
782
+ severity: (0, bpm_core_shared_1.readExpressionLintSeverity)(code),
783
+ ...(subject === undefined ? {} : { subject }),
784
+ });
785
+ /**
786
+ * A use point that is set but is not one (a client may send any string, a
787
+ * prototype key such as `toString` included). It is never used as an index:
788
+ * such a context allows no root and no host function, so the lint fails
789
+ * closed instead of throwing.
790
+ */
791
+ const hasUnknownUsagePoint = (context) => context.usagePoint !== undefined &&
792
+ context.usagePoint !== null &&
793
+ !(0, bpm_core_shared_1.isExpressionUsagePoint)(context.usagePoint);
794
+ const readAllowedRoots = (context) => {
795
+ if (context.allowedRootIdentifiers?.length) {
796
+ return context.allowedRootIdentifiers;
797
+ }
798
+ if (hasUnknownUsagePoint(context)) {
799
+ return [];
800
+ }
801
+ return context.usagePoint
802
+ ? bpm_core_shared_1.EXPRESSION_ROOT_IDENTIFIERS_BY_USAGE_POINT[context.usagePoint]
803
+ : null;
804
+ };
805
+ const lintStructure = (references, label, context) => {
806
+ const allowedRoots = readAllowedRoots(context);
807
+ const allowed = new Set(allowedRoots ?? []);
808
+ const rootIssues = allowedRoots === null
809
+ ? []
810
+ : references.roots
811
+ .filter((root) => !allowed.has(root))
812
+ .map((root) => issue('EXPR_ROOT_NOT_ALLOWED', label, root));
813
+ const wholeIssues = references.wholeRootReferences
814
+ .filter((root) => WHOLE_REFERENCE_ROOTS.has(root) &&
815
+ (allowedRoots === null || allowed.has(root)))
816
+ .map((root) => issue('EXPR_WHOLE_FORM_REFERENCE', label, root));
817
+ return [
818
+ ...rootIssues,
819
+ ...wholeIssues,
820
+ ...(references.computedKeys.length > 0
821
+ ? [issue('EXPR_COMPUTED_KEY', label)]
822
+ : []),
823
+ ...references.reservedSegments.map((segment) => issue('EXPR_RESERVED_PATH_SEGMENT', label, segment)),
824
+ ...(references.comprehensionDepth > exports.EXPRESSION_MAX_COMPREHENSION_DEPTH
825
+ ? [issue('EXPR_COMPREHENSION_NESTING', label)]
826
+ : []),
827
+ ];
828
+ };
829
+ const lintFields = (references, label, schema) => {
830
+ if (!schema) {
831
+ return [];
832
+ }
833
+ const known = new Set(readSchemaFields(schema).map((field) => field.fieldKey));
834
+ return references.formFieldKeys
835
+ .filter((fieldKey) => !known.has(fieldKey))
836
+ .map((fieldKey) => issue('EXPR_FIELD_UNKNOWN', label, fieldKey));
837
+ };
838
+ const lintLogger = new common_1.Logger('ExpressionLint');
839
+ /**
840
+ * Logs that the host's function registry failed the lint. Only a fixed
841
+ * reason: the registry's own message is host text and is never kept
842
+ * (docs/24 §2.6).
843
+ */
844
+ const logRegistryFailure = (reason) => {
845
+ lintLogger.warn(`code=EXPR_FUNCTION_NOT_REGISTERED reason=${reason}`);
846
+ };
847
+ const isStringList = (value) => Array.isArray(value) && value.every((entry) => typeof entry === 'string');
848
+ /**
849
+ * The called function names the registry cannot vouch for, in first-seen
850
+ * order and without duplicates (ADR 22 §5.3; publish and rollback share it).
851
+ *
852
+ * Only names {@link isRegistryFunctionName} accepts — `…_vN` and the nine
853
+ * built-ins — are put to `assertPublishable`; any other name can never be
854
+ * registered and is always reported. Without a registry only the built-ins
855
+ * exist.
856
+ *
857
+ * The registry is host code and is isolated: an `assertPublishable` that
858
+ * throws or answers something other than a list of strings makes every name
859
+ * asked about unavailable. The result only ever holds names the expression
860
+ * itself called, never a value the registry returned.
861
+ */
862
+ function readUnavailableExpressionFunctions(names, registry) {
863
+ const unique = [...new Set(names)];
864
+ const asked = unique.filter(expression_function_invocation_1.isRegistryFunctionName);
865
+ const builtIns = new Set(bpm_core_shared_1.BPM_BUILT_IN_EXPRESSION_FUNCTION_NAMES);
866
+ const answered = (() => {
867
+ if (!registry) {
868
+ return new Set(asked.filter((name) => !builtIns.has(name)));
869
+ }
870
+ if (asked.length === 0) {
871
+ return new Set();
872
+ }
873
+ try {
874
+ const result = registry.assertPublishable(asked);
875
+ if (isStringList(result)) {
876
+ return new Set(result);
877
+ }
878
+ logRegistryFailure('assert-publishable-shape');
879
+ }
880
+ catch {
881
+ logRegistryFailure('assert-publishable-threw');
882
+ }
883
+ return new Set(asked);
884
+ })();
885
+ return unique.filter((name) => !(0, expression_function_invocation_1.isRegistryFunctionName)(name) || answered.has(name));
886
+ }
887
+ /**
888
+ * The parameter count the registry declares for `name`: `ABSENT` when `get`
889
+ * answers `null` (`assertPublishable` already decided availability), `BROKEN`
890
+ * when it throws or returns something without a parameter list. Isolated like
891
+ * {@link readUnavailableExpressionFunctions}.
892
+ */
893
+ const readRegisteredParameterCount = (registry, name) => {
894
+ try {
895
+ const registered = registry.get(name);
896
+ if (registered === null) {
897
+ return 'ABSENT';
898
+ }
899
+ const parameters = typeof registered === 'object'
900
+ ? registered.parameters
901
+ : undefined;
902
+ if (Array.isArray(parameters)) {
903
+ return parameters.length;
904
+ }
905
+ logRegistryFailure('get-shape');
906
+ }
907
+ catch {
908
+ logRegistryFailure('get-threw');
909
+ }
910
+ return 'BROKEN';
911
+ };
912
+ const readLintEnvironment = (registry) => {
913
+ if (!registry || registry === expression_function_registry_1.BUILT_IN_EXPRESSION_FUNCTION_REGISTRY) {
914
+ return { environment: (0, cel_v2_adapter_1.loadCelV2Environment)(), failed: false };
915
+ }
916
+ // Loaded first and outside the `try`: a failure to load the engine is a
917
+ // configuration error, not the registry's.
918
+ const builtInEnvironment = (0, cel_v2_adapter_1.loadCelV2Environment)();
919
+ try {
920
+ return { environment: (0, cel_v2_adapter_1.loadCelV2Environment)(registry), failed: false };
921
+ }
922
+ catch {
923
+ logRegistryFailure('environment');
924
+ return { environment: builtInEnvironment, failed: true };
925
+ }
926
+ };
927
+ const lintFunctions = (references, label, context, lintEnvironment) => {
928
+ const engine = readEngineFunctionNames();
929
+ const unknownMethods = [
930
+ ...new Set(references.functionCalls
931
+ .filter((call) => call.receiver && !engine.methods.has(call.name))
932
+ .map((call) => call.name)),
933
+ ];
934
+ const hostCalls = references.functionCalls.filter((call) => !call.receiver && !engine.globals.has(call.name));
935
+ const hostNames = [...new Set(hostCalls.map((call) => call.name))];
936
+ const registry = context.functionRegistry ?? null;
937
+ const builtIns = new Set(bpm_core_shared_1.BPM_BUILT_IN_EXPRESSION_FUNCTION_NAMES);
938
+ const missing = new Set(lintEnvironment.failed
939
+ ? hostNames
940
+ : readUnavailableExpressionFunctions(hostNames, registry));
941
+ // A call whose argument count matches no registered parameter list names
942
+ // no registered function either (ADR 22 §5.3: "the function and version").
943
+ const wrongArity = registry
944
+ ? hostCalls
945
+ .filter((call) => !missing.has(call.name))
946
+ .filter((call) => {
947
+ const count = readRegisteredParameterCount(registry, call.name);
948
+ return count !== 'ABSENT' && count !== call.argumentCount;
949
+ })
950
+ .map((call) => call.name)
951
+ : [];
952
+ const notRegistered = [
953
+ ...new Set([...unknownMethods, ...missing, ...wrongArity]),
954
+ ];
955
+ const hostFunctionsAllowed = hasUnknownUsagePoint(context)
956
+ ? false
957
+ : !context.usagePoint || (0, bpm_core_shared_1.allowsHostExpressionFunctions)(context.usagePoint);
958
+ const notAllowed = hostFunctionsAllowed
959
+ ? []
960
+ : hostNames.filter((name) => !missing.has(name) && !builtIns.has(name));
961
+ const checkable = readCheckableGlobalNames(lintEnvironment.environment);
962
+ return {
963
+ issues: [
964
+ ...notRegistered.map((name) => issue('EXPR_FUNCTION_NOT_REGISTERED', label, name)),
965
+ // A registry that cannot be read refuses even an expression that
966
+ // calls nothing: evaluation builds its environment from the same list.
967
+ ...(lintEnvironment.failed && notRegistered.length === 0
968
+ ? [issue('EXPR_FUNCTION_NOT_REGISTERED', label)]
969
+ : []),
970
+ ...notAllowed.map((name) => issue('EXPR_FUNCTION_NOT_ALLOWED', label, name)),
971
+ ],
972
+ uncheckable: lintEnvironment.failed ||
973
+ unknownMethods.length > 0 ||
974
+ hostNames.some((name) => !checkable.has(name)),
975
+ };
976
+ };
977
+ /** Base CEL types a use point may produce; `null` means anything. */
978
+ const readExpectedResultTypes = (context) => {
979
+ switch (context.usagePoint) {
980
+ case 'INITIATOR_POLICY':
981
+ case 'ENTRY_CONDITION':
982
+ case 'FLOW_CONDITION':
983
+ case 'DELEGATION_CONDITION':
984
+ return ['bool'];
985
+ case 'APPROVER_RESOLVER':
986
+ return ['string', 'list'];
987
+ case 'DATA_SOURCE_BINDING':
988
+ case 'NOTIFY_WEBHOOK_BINDING':
989
+ return ['bool', 'double', 'int', 'null', 'string', 'uint'];
990
+ case 'SERVICE_FORM_FIELD_VALUE': {
991
+ if (!context.targetField) {
992
+ return null;
993
+ }
994
+ const declared = (0, exports.readExpressionFieldType)(context.targetField);
995
+ const base = declared.split('<')[0];
996
+ if (base === 'dyn') {
997
+ return null;
998
+ }
999
+ return base === 'double'
1000
+ ? ['double', 'int', 'uint', 'null']
1001
+ : [base, 'null'];
1002
+ }
1003
+ default:
1004
+ return null;
1005
+ }
1006
+ };
1007
+ const isResultTypeAccepted = (type, expected) => {
1008
+ if (expected === null || type === undefined) {
1009
+ return true;
1010
+ }
1011
+ const base = type.split('<')[0].trim();
1012
+ return base === 'dyn' || base === 'null_type' || expected.includes(base);
1013
+ };
1014
+ const readCheckErrorNode = (error) => {
1015
+ const node = error?.node;
1016
+ return typeof node === 'object' &&
1017
+ node !== null &&
1018
+ typeof node.op === 'string'
1019
+ ? node
1020
+ : null;
1021
+ };
1022
+ /**
1023
+ * Comparisons a typed check refuses that the runtime (where every field is
1024
+ * `dyn`) accepts and the designer writes on purpose: `form.x != null`,
1025
+ * `form.amount != ""`, `form.member != []` (`readConditionExpression`) and
1026
+ * `form.qty in [1, 2, 3]` (P1 P03 routes the same in both dialects).
1027
+ */
1028
+ const isTolerated = (node) => node.op === 'in' ||
1029
+ ((node.op === '==' || node.op === '!=') &&
1030
+ node.args.some((operand) => isCelV2EmptyLiteral(operand)));
1031
+ /**
1032
+ * The schema type (its base, `list<string>` is `list`) of an argument that is
1033
+ * exactly `form.<key>`, `formData.<key>` or `form["<key>"]` for a field
1034
+ * `runTypeCheck` demoted to `dyn`; `null` for anything else, which keeps the
1035
+ * type the check gave it.
1036
+ */
1037
+ const readDemotedFieldType = (argument, schemaTypes, checkedTypes) => {
1038
+ if (schemaTypes === null || checkedTypes === null) {
1039
+ return null;
1040
+ }
1041
+ const { base, steps } = readCelV2Chain(argument);
1042
+ const step = steps.length === 1 ? steps[0] : null;
1043
+ const literal = step?.kind === 'index' ? readCelV2LiteralKey(step.index) : null;
1044
+ const fieldKey = step?.kind === 'member'
1045
+ ? step.name
1046
+ : literal?.string === true
1047
+ ? literal.key
1048
+ : null;
1049
+ if (fieldKey === null ||
1050
+ base.op !== 'id' ||
1051
+ !FORM_ROOTS.has(base.args) ||
1052
+ argument.op === '.?' ||
1053
+ argument.op === '[?]' ||
1054
+ checkedTypes.get(fieldKey) !== 'dyn') {
1055
+ return null;
1056
+ }
1057
+ const schemaType = schemaTypes.get(fieldKey);
1058
+ return schemaType === undefined || schemaType === 'dyn'
1059
+ ? null
1060
+ : schemaType.split('<')[0].trim();
1061
+ };
1062
+ const createLintEnvironment = (fieldTypes, base) => {
1063
+ // `clone` copies the registry environment's registrations (the mixed
1064
+ // int/double operators, the built-ins and the host's functions) without
1065
+ // touching it. That environment is frozen when it is built (cel-v2
1066
+ // adapter), before anyone can clone it, so cloning here never stops a
1067
+ // registration that is still to come, whichever of boot, lint and
1068
+ // evaluation runs first.
1069
+ const environment = base.clone();
1070
+ if (fieldTypes !== null) {
1071
+ const schema = Object.fromEntries(fieldTypes);
1072
+ environment.registerVariable({ name: 'form', schema });
1073
+ environment.registerVariable({ name: 'formData', schema });
1074
+ }
1075
+ return environment;
1076
+ };
1077
+ /**
1078
+ * `env.check()` with each form field declared at its schema type. A refused
1079
+ * comparison the runtime accepts (see `isTolerated`) demotes the fields in it
1080
+ * to `dyn` and checks again; anything else is the finding. Bounded by the
1081
+ * field count, since every round demotes at least one field.
1082
+ */
1083
+ const runTypeCheck = (expression, fieldTypes, base) => {
1084
+ const check = (types, rounds) => {
1085
+ const parsed = createLintEnvironment(types, base).parse(expression);
1086
+ const result = parsed.check();
1087
+ if (result.valid) {
1088
+ // The engine checks host and built-in parameters through their
1089
+ // signatures, except numbers, which are registered as `dyn` there:
1090
+ // compare each argument's checked type with the declared one here, or
1091
+ // `demo_costBand_v1(form.title)` publishes and fails every case.
1092
+ // A field demoted to `dyn` for a tolerated comparison is demoted in the
1093
+ // whole expression, so an argument that is that field is compared at
1094
+ // its schema type: `form.title != null && f(form.title)` still fails
1095
+ // every case with a text title.
1096
+ const mismatch = (0, cel_v2_adapter_1.findCelV2ArgumentTypeMismatch)(parsed.ast, base, (argument) => readDemotedFieldType(argument, fieldTypes, types));
1097
+ if (mismatch === null) {
1098
+ return { status: 'OK', type: result.type };
1099
+ }
1100
+ const argumentFieldKeys = collectCelV2References(mismatch.argument).formFieldKeys;
1101
+ return {
1102
+ code: 'EXPR_FIELD_TYPE_MISMATCH',
1103
+ fieldKey: argumentFieldKeys.length === 1 ? argumentFieldKeys[0] : null,
1104
+ status: 'FAILED',
1105
+ };
1106
+ }
1107
+ const node = readCheckErrorNode(result.error);
1108
+ const fieldKeys = node ? collectCelV2References(node).formFieldKeys : [];
1109
+ const code = result.error?.code;
1110
+ if (code === 'no_such_key') {
1111
+ return {
1112
+ code: 'EXPR_FIELD_UNKNOWN',
1113
+ fieldKey: fieldKeys.length === 1 ? fieldKeys[0] : null,
1114
+ status: 'FAILED',
1115
+ };
1116
+ }
1117
+ const demotable = types === null || node === null || !isTolerated(node)
1118
+ ? []
1119
+ : fieldKeys.filter((fieldKey) => {
1120
+ const type = types.get(fieldKey);
1121
+ return type !== undefined && type !== 'dyn';
1122
+ });
1123
+ if (types === null || demotable.length === 0 || rounds <= 0) {
1124
+ return {
1125
+ code: 'EXPR_FIELD_TYPE_MISMATCH',
1126
+ fieldKey: fieldKeys.length === 1 ? fieldKeys[0] : null,
1127
+ status: 'FAILED',
1128
+ };
1129
+ }
1130
+ return check(new Map([...types].map(([fieldKey, type]) => [
1131
+ fieldKey,
1132
+ demotable.includes(fieldKey) ? 'dyn' : type,
1133
+ ])), rounds - 1);
1134
+ };
1135
+ return check(fieldTypes, fieldTypes?.size ?? 0);
1136
+ };
1137
+ const lintTypes = (expression, label, context, base) => {
1138
+ const fields = readSchemaFields(context.formSchema);
1139
+ const fieldTypes = context.formSchema
1140
+ ? new Map(fields.map((field) => [
1141
+ field.fieldKey,
1142
+ (0, exports.readExpressionFieldType)(field),
1143
+ ]))
1144
+ : null;
1145
+ const outcome = runTypeCheck(expression, fieldTypes, base);
1146
+ if (outcome.status === 'FAILED') {
1147
+ return [issue(outcome.code, label, outcome.fieldKey ?? undefined)];
1148
+ }
1149
+ return isResultTypeAccepted(outcome.type, readExpectedResultTypes(context))
1150
+ ? []
1151
+ : [issue('EXPR_RESULT_TYPE_MISMATCH', label)];
1152
+ };
1153
+ const readPathKey = (path) => [path.root, ...path.segments].join('.');
1154
+ const readGuardedFormFieldKeys = (references) => new Set((0, exports.readFormFieldKeys)(references.guardedFieldPaths));
1155
+ const lintWarnings = (references, label, context) => {
1156
+ const fields = readSchemaFields(context.formSchema);
1157
+ const fieldsByKey = new Map(fields.map((field) => [field.fieldKey, field]));
1158
+ const guarded = readGuardedFormFieldKeys(references);
1159
+ const optionalIssues = context.formSchema
1160
+ ? references.formFieldKeys
1161
+ .filter((fieldKey) => fieldsByKey.get(fieldKey)?.required === false)
1162
+ .filter((fieldKey) => !guarded.has(fieldKey))
1163
+ .map((fieldKey) => issue('EXPR_OPTIONAL_FIELD_WITHOUT_HAS', label, fieldKey))
1164
+ : [];
1165
+ const orAbsorbs = (0, exports.readFormFieldKeys)(references.orOperandFieldPaths).some((fieldKey) => !guarded.has(fieldKey));
1166
+ const hasTested = new Set(references.hasArguments.map(readPathKey));
1167
+ const hasNestedParent = references.hasArguments.some((path) => path.segments.length >= 2 &&
1168
+ !hasTested.has(readPathKey({ ...path, segments: path.segments.slice(0, -1) })));
1169
+ const readsForm = references.roots.some((root) => FORM_ROOTS.has(root));
1170
+ const target = context.targetField;
1171
+ const hiddenCopies = context.usagePoint === 'SERVICE_FORM_FIELD_VALUE' &&
1172
+ target &&
1173
+ target.hidden !== true
1174
+ ? references.formFieldKeys.filter((fieldKey) => fieldKey !== target.fieldKey &&
1175
+ fieldsByKey.get(fieldKey)?.hidden === true)
1176
+ : [];
1177
+ return [
1178
+ ...optionalIssues,
1179
+ ...(orAbsorbs ? [issue('EXPR_OR_ABSORBS_ERROR', label)] : []),
1180
+ ...(hasNestedParent ? [issue('EXPR_HAS_NESTED_PARENT', label)] : []),
1181
+ ...(context.usagePoint === 'APPROVER_RESOLVER' && readsForm
1182
+ ? [issue('EXPR_APPROVER_READS_FORM', label)]
1183
+ : []),
1184
+ ...hiddenCopies.map((fieldKey) => issue('EXPR_HIDDEN_FIELD_COPIED', label, fieldKey)),
1185
+ ...(references.divisorFieldPaths.length > 0
1186
+ ? [issue('EXPR_DIVISOR_IS_FIELD', label)]
1187
+ : []),
1188
+ ];
1189
+ };
1190
+ /**
1191
+ * The cel-v2 lint of one expression (ADR 22 §3.4, docs/24 §2.2): errors that
1192
+ * refuse a publish and warnings that do not, in that order. The expression is
1193
+ * expected trimmed and within the length limit (`ConditionService` checks
1194
+ * both first).
1195
+ *
1196
+ * `environmentRegistry` is the registry evaluation will run with (the one
1197
+ * `ConditionService` was given): the expression is parsed and type-checked in
1198
+ * its cel-v2 environment, so the built-ins and the host's functions are
1199
+ * type-checked like the engine's own. Absent means the built-ins only. It is
1200
+ * separate from `context.functionRegistry`, which only answers availability
1201
+ * (`assertPublishable`, `get`) and need not be able to build an environment.
1202
+ *
1203
+ * The type check (`env.check()`) runs only when nothing structural is wrong
1204
+ * and every function called is one that environment knows; a call to one it
1205
+ * does not know is covered by `EXPR_FUNCTION_NOT_REGISTERED` instead.
1206
+ */
1207
+ function lintCelV2Expression(expression, label, context = {}, environmentRegistry) {
1208
+ const lintEnvironment = readLintEnvironment(environmentRegistry);
1209
+ const parsed = readCelV2References(expression, lintEnvironment.environment);
1210
+ if (parsed.status !== 'OK') {
1211
+ return [
1212
+ issue(parsed.status === 'PARSE_FAILED' && parsed.limitExceeded
1213
+ ? 'EXPR_COMPLEXITY_EXCEEDED'
1214
+ : 'EXPR_PARSE_FAILED', label),
1215
+ ];
1216
+ }
1217
+ const { references } = parsed;
1218
+ const structural = lintStructure(references, label, context);
1219
+ const fieldIssues = lintFields(references, label, context.formSchema);
1220
+ const functions = lintFunctions(references, label, context, lintEnvironment);
1221
+ const errors = [...structural, ...fieldIssues, ...functions.issues];
1222
+ const typeIssues = errors.length === 0 && !functions.uncheckable
1223
+ ? lintTypes(expression, label, context, lintEnvironment.environment)
1224
+ : [];
1225
+ return [
1226
+ ...errors,
1227
+ ...typeIssues,
1228
+ ...lintWarnings(references, label, context),
1229
+ ];
1230
+ }
1231
+ /**
1232
+ * The global function names an expression calls that cel-v2 does not provide
1233
+ * itself: the built-ins and host functions a rollback must find registered
1234
+ * (docs/24 §1). `null` when the expression cannot be read. Hand the result to
1235
+ * {@link readUnavailableExpressionFunctions}, which applies the same name
1236
+ * rule and registry isolation as the publish lint.
1237
+ */
1238
+ function readExpressionHostFunctionNames(expression, dialect = expression_dialect_1.DEFAULT_EXPRESSION_DIALECT) {
1239
+ // cel-legacy has no function registration at all: nothing to look up.
1240
+ if (dialect !== 'cel-v2') {
1241
+ return [];
1242
+ }
1243
+ const parsed = readCelV2References(expression);
1244
+ if (parsed.status !== 'OK') {
1245
+ return null;
1246
+ }
1247
+ const engine = readEngineFunctionNames().globals;
1248
+ return [
1249
+ ...new Set(parsed.references.functionCalls
1250
+ .filter((call) => !call.receiver && !engine.has(call.name))
1251
+ .map((call) => call.name)),
1252
+ ];
1253
+ }
1254
+ //# sourceMappingURL=expression-ast.js.map