form0-core 0.1.7 → 0.1.9

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.
@@ -3,7 +3,7 @@ import { evaluateVisibility } from './conditions.js';
3
3
  import { evaluateRequirement } from './conditions.js';
4
4
  import { evaluateReadOnly } from './conditions.js';
5
5
  import { validateFields } from './field-validation.js';
6
- import { builtins, eventBuiltins } from '../builtins/registry.js';
6
+ import { calculationBuiltins, eventBuiltins } from '../builtins/registry.js';
7
7
  import { validateSchema } from '../schema/schema-validator.js';
8
8
  import { flattenFields } from '../utilities/field-helpers.js';
9
9
  import { DEFAULT_SECURITY_CONFIG } from '../security/config.js';
@@ -12,6 +12,7 @@ import { FIELD_SPECS } from '../schema/field-specs.js';
12
12
  import { ContextResolver } from './context-resolver.js';
13
13
  import { WarningSystem } from './warning-system.js';
14
14
  import { expandBuildingPlanSchema } from '../schema/building-plan-expander.js';
15
+ import { buildCalculationDependencyPlan } from '../utilities/calculation-dependencies.js';
15
16
 
16
17
  export function createFormEngine({
17
18
  schema,
@@ -90,16 +91,17 @@ export function createFormEngine({
90
91
  const required = {};
91
92
  const read_only = {};
92
93
 
93
- const allHelpers = { ...builtins, ...helpers };
94
+ const allHelpers = { ...calculationBuiltins, ...helpers };
94
95
 
95
96
  // Initialize context resolution system
96
97
  const contextResolver = new ContextResolver(form);
98
+ const calculationPlan = buildCalculationDependencyPlan(form, contextResolver);
97
99
  const sharedWarningSystem = warningSystem || new WarningSystem();
98
100
 
99
101
  // Initialize event system with context resolution
100
102
  const eventManager = new EventManager(form, contextResolver, sharedWarningSystem);
101
103
  eventManager.securityConfig = security; // Pass security config
102
- const eventHelpers = { ...builtins, ...eventBuiltins, ...helpers };
104
+ const eventHelpers = { ...eventBuiltins, ...helpers };
103
105
 
104
106
  // Initialize event code if present
105
107
  if (form.events && form.events.code) {
@@ -128,7 +130,8 @@ export function createFormEngine({
128
130
  security,
129
131
  contextResolver,
130
132
  sharedWarningSystem,
131
- runtimeDiagnostics
133
+ runtimeDiagnostics,
134
+ calculationPlan
132
135
  );
133
136
  evaluateRequirement(form, values, required);
134
137
  evaluateVisibility(form, values, visible);
package/src/index.js CHANGED
@@ -38,6 +38,11 @@ export {
38
38
  createCalculationPreviewSession,
39
39
  simulateCalculationExpression,
40
40
  } from './utilities/calculation-editor.js';
41
+ export {
42
+ getFormEventBuiltinCatalog,
43
+ getFormEventReferenceCatalog,
44
+ analyzeFormEventCode,
45
+ } from './utilities/form-event-editor.js';
41
46
  export {
42
47
  getFormAIMetadata,
43
48
  getFormAIContext,
@@ -1,16 +1,15 @@
1
- import { builtins, eventBuiltins } from '../builtins/registry.js';
1
+ import { calculationBuiltins, eventBuiltins } from '../builtins/registry.js';
2
2
  import { DEFAULT_SECURITY_CONFIG, SAFE_SECURITY_CONFIG, SECURITY_MODES } from './config.js';
3
3
 
4
4
  // Helper function to validate builtin function names in expressions
5
5
  function validateBuiltinNames(expr, includeEventBuiltins = false) {
6
6
  // Always use fresh builtin sets (no caching) to support dynamic builtin registration
7
- const validBuiltinsSet = new Set(Object.keys(builtins));
7
+ const validCalculationBuiltinsSet = new Set(Object.keys(calculationBuiltins));
8
8
  const validEventBuiltinsSet = new Set(Object.keys(eventBuiltins));
9
9
 
10
- // Combine regular and event builtins if in event context
11
10
  const allowedBuiltins = includeEventBuiltins
12
- ? new Set([...validBuiltinsSet, ...validEventBuiltinsSet])
13
- : validBuiltinsSet;
11
+ ? validEventBuiltinsSet
12
+ : validCalculationBuiltinsSet;
14
13
 
15
14
  // Extract function calls from the expression
16
15
  // This regex matches function calls like FUNCTIONNAME( allowing for whitespace
@@ -46,8 +45,8 @@ function validateBuiltinNames(expr, includeEventBuiltins = false) {
46
45
  }
47
46
 
48
47
  // Check for potential typos by finding similar builtin names
49
- const validBuiltins = [...allowedBuiltins]; // Convert back to array for filtering
50
- const similarBuiltins = validBuiltins.filter((builtin) => {
48
+ const validBuiltinNames = [...allowedBuiltins];
49
+ const similarBuiltins = validBuiltinNames.filter((builtin) => {
51
50
  // Simple similarity check - same length or very close
52
51
  const lengthDiff = Math.abs(builtin.length - functionName.length);
53
52
  return lengthDiff <= 2 && builtin.startsWith(functionName.substring(0, 3));
@@ -0,0 +1,311 @@
1
+ import { ContextResolver } from '../engine/context-resolver.js';
2
+ import { flattenFields } from './field-helpers.js';
3
+
4
+ function sortFieldNames(fieldNames, orderByFieldName) {
5
+ return [...fieldNames].sort(
6
+ (left, right) => orderByFieldName.get(left) - orderByFieldName.get(right)
7
+ );
8
+ }
9
+
10
+ function buildStronglyConnectedComponents(nodesByDataName, orderByFieldName) {
11
+ const indices = new Map();
12
+ const lowLinks = new Map();
13
+ const stack = [];
14
+ const onStack = new Set();
15
+ const components = [];
16
+ let currentIndex = 0;
17
+
18
+ function strongConnect(fieldName) {
19
+ indices.set(fieldName, currentIndex);
20
+ lowLinks.set(fieldName, currentIndex);
21
+ currentIndex += 1;
22
+ stack.push(fieldName);
23
+ onStack.add(fieldName);
24
+
25
+ const node = nodesByDataName.get(fieldName);
26
+ if (node) {
27
+ for (const dependency of node.dependencies) {
28
+ if (!indices.has(dependency)) {
29
+ strongConnect(dependency);
30
+ lowLinks.set(fieldName, Math.min(lowLinks.get(fieldName), lowLinks.get(dependency)));
31
+ } else if (onStack.has(dependency)) {
32
+ lowLinks.set(fieldName, Math.min(lowLinks.get(fieldName), indices.get(dependency)));
33
+ }
34
+ }
35
+ }
36
+
37
+ if (lowLinks.get(fieldName) !== indices.get(fieldName)) {
38
+ return;
39
+ }
40
+
41
+ const componentFieldNames = [];
42
+ while (stack.length > 0) {
43
+ const stackFieldName = stack.pop();
44
+ onStack.delete(stackFieldName);
45
+ componentFieldNames.push(stackFieldName);
46
+ if (stackFieldName === fieldName) {
47
+ break;
48
+ }
49
+ }
50
+
51
+ components.push(sortFieldNames(componentFieldNames, orderByFieldName));
52
+ }
53
+
54
+ const sortedFieldNames = sortFieldNames(nodesByDataName.keys(), orderByFieldName);
55
+ sortedFieldNames.forEach((fieldName) => {
56
+ if (!indices.has(fieldName)) {
57
+ strongConnect(fieldName);
58
+ }
59
+ });
60
+
61
+ return components;
62
+ }
63
+
64
+ function topologicallyOrderComponents(componentIds, componentEdges) {
65
+ const inDegree = new Map(componentIds.map((componentId) => [componentId, 0]));
66
+
67
+ componentEdges.forEach((targets) => {
68
+ targets.forEach((targetId) => {
69
+ inDegree.set(targetId, (inDegree.get(targetId) || 0) + 1);
70
+ });
71
+ });
72
+
73
+ const queue = componentIds.filter((componentId) => (inDegree.get(componentId) || 0) === 0);
74
+ const ordered = [];
75
+
76
+ while (queue.length > 0) {
77
+ const componentId = queue.shift();
78
+ ordered.push(componentId);
79
+
80
+ for (const targetId of componentEdges.get(componentId) || []) {
81
+ const nextDegree = (inDegree.get(targetId) || 0) - 1;
82
+ inDegree.set(targetId, nextDegree);
83
+ if (nextDegree === 0) {
84
+ queue.push(targetId);
85
+ }
86
+ }
87
+ }
88
+
89
+ if (ordered.length === componentIds.length) {
90
+ return ordered;
91
+ }
92
+
93
+ return [...componentIds];
94
+ }
95
+
96
+ export function normalizeCalculationFormSchema(schema) {
97
+ if (schema && Array.isArray(schema.elements)) {
98
+ return schema;
99
+ }
100
+
101
+ if (schema && schema.form && Array.isArray(schema.form.elements)) {
102
+ return schema.form;
103
+ }
104
+
105
+ throw new Error('Expected a form schema or a root schema containing form.elements');
106
+ }
107
+
108
+ export function hasDynamicCalculationDependencies(code) {
109
+ return /\bEVAL\s*\(/.test(typeof code === 'string' ? code : '');
110
+ }
111
+
112
+ export function removeEvalContents(code) {
113
+ const source = typeof code === 'string' ? code : '';
114
+ let result = '';
115
+ let i = 0;
116
+
117
+ while (i < source.length) {
118
+ const evalMatch = source.substring(i).match(/^EVAL\s*\(/);
119
+ if (evalMatch) {
120
+ result += 'EVAL()';
121
+ i += evalMatch[0].length;
122
+
123
+ let parenCount = 1;
124
+ let inSingleQuote = false;
125
+ let inDoubleQuote = false;
126
+ let inBacktick = false;
127
+
128
+ while (i < source.length && parenCount > 0) {
129
+ const char = source[i];
130
+
131
+ if (char === "'" && !inDoubleQuote && !inBacktick) {
132
+ inSingleQuote = !inSingleQuote;
133
+ } else if (char === '"' && !inSingleQuote && !inBacktick) {
134
+ inDoubleQuote = !inDoubleQuote;
135
+ } else if (char === '`' && !inSingleQuote && !inDoubleQuote) {
136
+ inBacktick = !inBacktick;
137
+ }
138
+
139
+ if (!inSingleQuote && !inDoubleQuote && !inBacktick) {
140
+ if (char === '(') {
141
+ parenCount += 1;
142
+ } else if (char === ')') {
143
+ parenCount -= 1;
144
+ }
145
+ }
146
+
147
+ i += 1;
148
+ }
149
+ } else {
150
+ result += source[i];
151
+ i += 1;
152
+ }
153
+ }
154
+
155
+ return result;
156
+ }
157
+
158
+ export function extractStaticFieldReferenceMatches(code) {
159
+ const cleanedCode = removeEvalContents(code);
160
+ const fieldRegex = /\$([a-zA-Z_][a-zA-Z0-9_]*)/g;
161
+ const matches = [];
162
+ let match;
163
+
164
+ while ((match = fieldRegex.exec(cleanedCode)) !== null) {
165
+ matches.push({
166
+ fieldName: match[1],
167
+ reference: `$${match[1]}`,
168
+ index: match.index,
169
+ length: match[0].length,
170
+ });
171
+ }
172
+
173
+ return matches;
174
+ }
175
+
176
+ export function extractStaticFieldReferences(code) {
177
+ return new Set(extractStaticFieldReferenceMatches(code).map((match) => match.fieldName));
178
+ }
179
+
180
+ export function buildCalculationExecutionContext(form, fieldDataName, resolver) {
181
+ return {
182
+ type: 'calculation',
183
+ fieldName: fieldDataName,
184
+ parentPath: resolver.getFieldInfo(fieldDataName)?.parentPath || [],
185
+ };
186
+ }
187
+
188
+ export function buildCalculationDependencyPlan(schema, contextResolver = null) {
189
+ const form = normalizeCalculationFormSchema(schema);
190
+ const resolver = contextResolver || new ContextResolver(form);
191
+ const fields = flattenFields(form.elements).filter((field) => field && typeof field === 'object');
192
+ const fieldsByDataName = new Map(
193
+ fields
194
+ .filter((field) => typeof field.data_name === 'string' && field.data_name.length > 0)
195
+ .map((field) => [field.data_name, field])
196
+ );
197
+
198
+ const orderByFieldName = new Map();
199
+ fields.forEach((field, index) => {
200
+ if (typeof field?.data_name === 'string') {
201
+ orderByFieldName.set(field.data_name, index);
202
+ }
203
+ });
204
+
205
+ const nodesByDataName = new Map();
206
+ fields.forEach((field) => {
207
+ if (field.type !== 'CalculatedField' || typeof field.data_name !== 'string') {
208
+ return;
209
+ }
210
+
211
+ const fieldName = field.data_name;
212
+ const executionContext = buildCalculationExecutionContext(form, fieldName, resolver);
213
+ const directReferences = extractStaticFieldReferences(field.calculate || '');
214
+ const dependencies = new Set();
215
+
216
+ directReferences.forEach((referenceFieldName) => {
217
+ const accessInfo = resolver.resolveFieldAccessInfo(executionContext, referenceFieldName);
218
+ if (accessInfo.level !== 'accessible') {
219
+ return;
220
+ }
221
+
222
+ const dependencyField = fieldsByDataName.get(referenceFieldName);
223
+ if (dependencyField?.type === 'CalculatedField') {
224
+ dependencies.add(referenceFieldName);
225
+ }
226
+ });
227
+
228
+ nodesByDataName.set(fieldName, {
229
+ field,
230
+ fieldName,
231
+ dependencies,
232
+ directReferences,
233
+ hasDynamicDependencies: hasDynamicCalculationDependencies(field.calculate || ''),
234
+ });
235
+ });
236
+
237
+ const componentFieldGroups = buildStronglyConnectedComponents(nodesByDataName, orderByFieldName);
238
+ const componentIdByFieldName = new Map();
239
+ const componentsById = new Map();
240
+
241
+ componentFieldGroups.forEach((fieldNames, componentIndex) => {
242
+ const componentId = `component_${componentIndex}`;
243
+ fieldNames.forEach((fieldName) => {
244
+ componentIdByFieldName.set(fieldName, componentId);
245
+ });
246
+ componentsById.set(componentId, {
247
+ id: componentId,
248
+ fieldNames,
249
+ hasDynamicDependencies: false,
250
+ isCyclic: false,
251
+ });
252
+ });
253
+
254
+ nodesByDataName.forEach((node) => {
255
+ node.componentId = componentIdByFieldName.get(node.fieldName);
256
+ });
257
+
258
+ componentsById.forEach((component) => {
259
+ component.hasDynamicDependencies = component.fieldNames.some(
260
+ (fieldName) => nodesByDataName.get(fieldName)?.hasDynamicDependencies
261
+ );
262
+ component.isCyclic =
263
+ component.fieldNames.length > 1 ||
264
+ component.fieldNames.some((fieldName) =>
265
+ nodesByDataName.get(fieldName)?.dependencies.has(fieldName)
266
+ );
267
+ });
268
+
269
+ const componentEdges = new Map(
270
+ Array.from(componentsById.keys(), (componentId) => [componentId, new Set()])
271
+ );
272
+
273
+ nodesByDataName.forEach((node) => {
274
+ const targetComponentId = node.componentId;
275
+ node.dependencies.forEach((dependencyFieldName) => {
276
+ const dependencyComponentId = componentIdByFieldName.get(dependencyFieldName);
277
+ if (!dependencyComponentId || dependencyComponentId === targetComponentId) {
278
+ return;
279
+ }
280
+ componentEdges.get(dependencyComponentId).add(targetComponentId);
281
+ });
282
+ });
283
+
284
+ const orderedComponentIds = topologicallyOrderComponents(
285
+ Array.from(componentsById.keys()),
286
+ componentEdges
287
+ );
288
+ const orderedFieldNames = orderedComponentIds.flatMap(
289
+ (componentId) => componentsById.get(componentId)?.fieldNames || []
290
+ );
291
+ const dynamicFieldNames = orderedFieldNames.filter(
292
+ (fieldName) => nodesByDataName.get(fieldName)?.hasDynamicDependencies
293
+ );
294
+ const cyclicComponentIds = orderedComponentIds.filter(
295
+ (componentId) => componentsById.get(componentId)?.isCyclic
296
+ );
297
+
298
+ return {
299
+ form,
300
+ fieldsByDataName,
301
+ nodesByDataName,
302
+ componentsById,
303
+ componentIdByFieldName,
304
+ orderedComponentIds,
305
+ orderedFieldNames,
306
+ hasDynamicDependencies: dynamicFieldNames.length > 0,
307
+ dynamicFieldNames,
308
+ cyclicComponentIds,
309
+ totalCalculatedFieldCount: orderedFieldNames.length,
310
+ };
311
+ }
@@ -8,6 +8,13 @@ import { createFormEngine } from '../engine/form-engine.js';
8
8
  import { WarningSystem } from '../engine/warning-system.js';
9
9
  import { DEFAULT_SECURITY_CONFIG } from '../security/config.js';
10
10
  import { validateExpression } from '../security/validation.js';
11
+ import {
12
+ buildCalculationDependencyPlan,
13
+ buildCalculationExecutionContext,
14
+ extractStaticFieldReferenceMatches,
15
+ hasDynamicCalculationDependencies,
16
+ normalizeCalculationFormSchema,
17
+ } from './calculation-dependencies.js';
11
18
  import {
12
19
  isMultilineCalculationExpression,
13
20
  normalizeInlineCalculationExpression,
@@ -53,67 +60,10 @@ const CALCULATION_BUILTIN_METADATA_BY_NAME = new Map(
53
60
  CALCULATION_BUILTIN_CATALOG.map((definition) => [definition.name, definition])
54
61
  );
55
62
 
56
- function normalizeFormSchema(schema) {
57
- if (schema && Array.isArray(schema.elements)) {
58
- return schema;
59
- }
60
-
61
- if (schema && schema.form && Array.isArray(schema.form.elements)) {
62
- return schema.form;
63
- }
64
-
65
- throw new Error('Expected a form schema or a root schema containing form.elements');
66
- }
67
-
68
63
  function isBuiltinCallMatchAllowed(functionName) {
69
64
  return COMMON_GLOBAL_FUNCTIONS.has(functionName);
70
65
  }
71
66
 
72
- function removeEvalContents(code) {
73
- let result = '';
74
- let i = 0;
75
-
76
- while (i < code.length) {
77
- const evalMatch = code.substring(i).match(/^EVAL\s*\(/);
78
- if (evalMatch) {
79
- result += 'EVAL()';
80
- i += evalMatch[0].length;
81
-
82
- let parenCount = 1;
83
- let inSingleQuote = false;
84
- let inDoubleQuote = false;
85
- let inBacktick = false;
86
-
87
- while (i < code.length && parenCount > 0) {
88
- const char = code[i];
89
-
90
- if (char === "'" && !inDoubleQuote && !inBacktick) {
91
- inSingleQuote = !inSingleQuote;
92
- } else if (char === '"' && !inSingleQuote && !inBacktick) {
93
- inDoubleQuote = !inDoubleQuote;
94
- } else if (char === '`' && !inSingleQuote && !inDoubleQuote) {
95
- inBacktick = !inBacktick;
96
- }
97
-
98
- if (!inSingleQuote && !inDoubleQuote && !inBacktick) {
99
- if (char === '(') {
100
- parenCount++;
101
- } else if (char === ')') {
102
- parenCount--;
103
- }
104
- }
105
-
106
- i++;
107
- }
108
- } else {
109
- result += code[i];
110
- i++;
111
- }
112
- }
113
-
114
- return result;
115
- }
116
-
117
67
  function extractFunctionCalls(code) {
118
68
  const functionCallPattern = /\b([A-Z_][A-Z0-9_]*)\s*\(/g;
119
69
  const matches = [];
@@ -130,32 +80,6 @@ function extractFunctionCalls(code) {
130
80
  return matches;
131
81
  }
132
82
 
133
- function extractFieldReferenceMatches(code) {
134
- const cleanedCode = removeEvalContents(code);
135
- const fieldRegex = /\$([a-zA-Z_][a-zA-Z0-9_]*)/g;
136
- const matches = [];
137
- let match;
138
-
139
- while ((match = fieldRegex.exec(cleanedCode)) !== null) {
140
- matches.push({
141
- fieldName: match[1],
142
- reference: `$${match[1]}`,
143
- index: match.index,
144
- length: match[0].length,
145
- });
146
- }
147
-
148
- return matches;
149
- }
150
-
151
- function buildExecutionContext(form, fieldDataName, resolver) {
152
- return {
153
- type: 'calculation',
154
- fieldName: fieldDataName,
155
- parentPath: resolver.getFieldInfo(fieldDataName)?.parentPath || [],
156
- };
157
- }
158
-
159
83
  function cloneCatalogDefinition(definition) {
160
84
  return {
161
85
  ...definition,
@@ -254,7 +178,7 @@ function cloneSimulationSchema(schema) {
254
178
  }
255
179
 
256
180
  return {
257
- form: deepClone(normalizeFormSchema(schema)),
181
+ form: deepClone(normalizeCalculationFormSchema(schema)),
258
182
  };
259
183
  }
260
184
 
@@ -383,9 +307,9 @@ export function getCalculationReferenceCatalog({
383
307
  fieldDataName,
384
308
  includeRestricted = false,
385
309
  }) {
386
- const form = normalizeFormSchema(schema);
310
+ const form = normalizeCalculationFormSchema(schema);
387
311
  const resolver = new ContextResolver(form);
388
- const executionContext = buildExecutionContext(form, fieldDataName, resolver);
312
+ const executionContext = buildCalculationExecutionContext(form, fieldDataName, resolver);
389
313
 
390
314
  return flattenFields(form.elements)
391
315
  .filter((field) => field && typeof field === 'object')
@@ -431,9 +355,14 @@ export function analyzeCalculationExpression({
431
355
  const setResultCalls = [];
432
356
 
433
357
  const normalizedExpression = typeof expression === 'string' ? expression : '';
434
- const form = normalizeFormSchema(schema);
358
+ const baseForm = normalizeCalculationFormSchema(schema);
359
+ const form = deepClone(baseForm);
360
+ const targetField = findFieldByDataName(form.elements, fieldDataName);
361
+ if (targetField && targetField.type === 'CalculatedField') {
362
+ targetField.calculate = normalizedExpression;
363
+ }
435
364
  const resolver = new ContextResolver(form);
436
- const executionContext = buildExecutionContext(form, fieldDataName, resolver);
365
+ const executionContext = buildCalculationExecutionContext(form, fieldDataName, resolver);
437
366
 
438
367
  if (normalizedExpression.trim().length === 0) {
439
368
  addIssue(
@@ -590,7 +519,7 @@ export function analyzeCalculationExpression({
590
519
  );
591
520
  }
592
521
 
593
- for (const reference of extractFieldReferenceMatches(normalizedExpression)) {
522
+ for (const reference of extractStaticFieldReferenceMatches(normalizedExpression)) {
594
523
  if (!referencedFieldNames.has(reference.fieldName)) {
595
524
  referencedFieldNames.add(reference.fieldName);
596
525
  referencedFields.push(reference.fieldName);
@@ -633,6 +562,39 @@ export function analyzeCalculationExpression({
633
562
  }
634
563
  }
635
564
 
565
+ if (hasDynamicCalculationDependencies(normalizedExpression)) {
566
+ addIssue(
567
+ issues,
568
+ issueKeys,
569
+ createIssue({
570
+ code: 'dynamic_calculation_dependency',
571
+ severity: 'warning',
572
+ message:
573
+ 'This expression uses dynamic field access through EVAL(). Runtime evaluation will fall back to bounded stabilization.',
574
+ })
575
+ );
576
+ }
577
+
578
+ if (targetField?.type === 'CalculatedField') {
579
+ const dependencyPlan = buildCalculationDependencyPlan(form, resolver);
580
+ const targetNode = dependencyPlan.nodesByDataName.get(fieldDataName);
581
+ const targetComponent = targetNode?.componentId
582
+ ? dependencyPlan.componentsById.get(targetNode.componentId)
583
+ : null;
584
+
585
+ if (targetComponent?.isCyclic) {
586
+ addIssue(
587
+ issues,
588
+ issueKeys,
589
+ createIssue({
590
+ code: 'calculated_field_cycle',
591
+ severity: 'error',
592
+ message: `CalculatedField dependency cycle detected: ${targetComponent.fieldNames.join(' -> ')}.`,
593
+ })
594
+ );
595
+ }
596
+ }
597
+
636
598
  return {
637
599
  valid: !issues.some((issue) => issue.severity === 'error'),
638
600
  issues,
@@ -673,23 +635,20 @@ export function createCalculationPreviewSession({
673
635
  security = DEFAULT_SECURITY_CONFIG,
674
636
  }) {
675
637
  const simulationSchema = cloneSimulationSchema(schema);
676
- const form = normalizeFormSchema(simulationSchema);
638
+ const form = normalizeCalculationFormSchema(simulationSchema);
677
639
  const targetField = findFieldByDataName(form.elements, fieldDataName);
678
640
 
679
641
  if (!targetField) {
680
642
  return {
681
643
  run: () =>
682
- createSimulationErrorResult(
683
- `Field '${fieldDataName}' does not exist in the form schema.`
684
- ),
644
+ createSimulationErrorResult(`Field '${fieldDataName}' does not exist in the form schema.`),
685
645
  dispose: () => {},
686
646
  };
687
647
  }
688
648
 
689
649
  if (targetField.type !== 'CalculatedField') {
690
650
  return {
691
- run: () =>
692
- createSimulationErrorResult(`Field '${fieldDataName}' is not a CalculatedField.`),
651
+ run: () => createSimulationErrorResult(`Field '${fieldDataName}' is not a CalculatedField.`),
693
652
  dispose: () => {},
694
653
  };
695
654
  }
@@ -739,9 +698,13 @@ export function createCalculationPreviewSession({
739
698
 
740
699
  const simulatedResult = engineState.values[fieldDataName];
741
700
  const runtimeError =
742
- runtimeDiagnostics.find((diagnostic) => diagnostic.fieldName === fieldDataName)
701
+ runtimeDiagnostics.find(
702
+ (diagnostic) =>
703
+ diagnostic.fieldName === fieldDataName &&
704
+ (diagnostic.severity || 'error') !== 'warning'
705
+ )?.message ||
706
+ runtimeDiagnostics.find((diagnostic) => (diagnostic.severity || 'error') !== 'warning')
743
707
  ?.message ||
744
- runtimeDiagnostics[0]?.message ||
745
708
  null;
746
709
 
747
710
  return {