form0-core 0.1.5 → 0.1.6

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": "form0-core",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org/"
6
6
  },
@@ -9,7 +9,8 @@ export function evaluateCalculatedFields(
9
9
  helpers,
10
10
  securityConfig,
11
11
  contextResolver = null,
12
- warningSystem = null
12
+ warningSystem = null,
13
+ runtimeDiagnostics = null
13
14
  ) {
14
15
  const fields = flattenFields(schema.elements);
15
16
 
@@ -40,10 +41,37 @@ export function evaluateCalculatedFields(
40
41
  context,
41
42
  securityConfig,
42
43
  false,
43
- schema
44
+ schema,
45
+ {
46
+ suppressConsoleWarning: Array.isArray(runtimeDiagnostics),
47
+ onError: (error) => {
48
+ if (!Array.isArray(runtimeDiagnostics)) {
49
+ return;
50
+ }
51
+
52
+ runtimeDiagnostics.push({
53
+ fieldName: field.data_name,
54
+ message:
55
+ error instanceof Error && error.message
56
+ ? error.message
57
+ : 'Unknown calculation runtime error.',
58
+ });
59
+ },
60
+ }
44
61
  );
45
62
  } catch (e) {
46
- console.warn(`Calculation failed for ${field.data_name}:`, e.message);
63
+ if (Array.isArray(runtimeDiagnostics)) {
64
+ runtimeDiagnostics.push({
65
+ fieldName: field.data_name,
66
+ message:
67
+ e instanceof Error && e.message
68
+ ? e.message
69
+ : 'Unknown calculation runtime error.',
70
+ });
71
+ }
72
+ if (!Array.isArray(runtimeDiagnostics)) {
73
+ console.warn(`Calculation failed for ${field.data_name}:`, e.message);
74
+ }
47
75
  }
48
76
  }
49
77
  });
@@ -13,13 +13,16 @@ export function runExpression(
13
13
  context = {},
14
14
  securityConfig = DEFAULT_SECURITY_CONFIG,
15
15
  includeEventBuiltins = false,
16
- schema = null
16
+ schema = null,
17
+ options = {}
17
18
  ) {
18
19
  try {
19
20
  // Validate expression based on security mode
20
21
  const validation = validateExpression(expr, securityConfig, includeEventBuiltins);
21
22
  if (!validation.valid) {
22
- console.warn('[form0] Expression validation failed:', validation.reason);
23
+ if (!options.suppressConsoleWarning) {
24
+ console.warn('[form0] Expression validation failed:', validation.reason);
25
+ }
23
26
  return null;
24
27
  }
25
28
 
@@ -73,7 +76,12 @@ export function runExpression(
73
76
 
74
77
  return executeExpression();
75
78
  } catch (e) {
76
- console.warn('[form0] Expression evaluation failed:', e.message);
79
+ if (typeof options.onError === 'function') {
80
+ options.onError(e);
81
+ }
82
+ if (!options.suppressConsoleWarning) {
83
+ console.warn('[form0] Expression evaluation failed:', e.message);
84
+ }
77
85
  return null;
78
86
  }
79
87
  }
@@ -19,6 +19,7 @@ export function createFormEngine({
19
19
  helpers = {},
20
20
  security = DEFAULT_SECURITY_CONFIG,
21
21
  warningSystem = null,
22
+ runtimeDiagnostics = null,
22
23
  }) {
23
24
  const { schema: preparedSchema, buildingPlanMeta } = expandBuildingPlanSchema(schema);
24
25
 
@@ -126,7 +127,8 @@ export function createFormEngine({
126
127
  allHelpers,
127
128
  security,
128
129
  contextResolver,
129
- sharedWarningSystem
130
+ sharedWarningSystem,
131
+ runtimeDiagnostics
130
132
  );
131
133
  evaluateRequirement(form, values, required);
132
134
  evaluateVisibility(form, values, visible);
package/src/index.js CHANGED
@@ -35,6 +35,7 @@ export {
35
35
  getCalculationBuiltinCatalog,
36
36
  getCalculationReferenceCatalog,
37
37
  analyzeCalculationExpression,
38
+ simulateCalculationExpression,
38
39
  } from './utilities/calculation-editor.js';
39
40
  export {
40
41
  getFormAIMetadata,
@@ -4,6 +4,8 @@ import {
4
4
  CALCULATION_BUILTIN_DEFINITIONS,
5
5
  } from '../builtins/registry.js';
6
6
  import { ContextResolver } from '../engine/context-resolver.js';
7
+ import { createFormEngine } from '../engine/form-engine.js';
8
+ import { WarningSystem } from '../engine/warning-system.js';
7
9
  import { DEFAULT_SECURITY_CONFIG } from '../security/config.js';
8
10
  import { validateExpression } from '../security/validation.js';
9
11
  import {
@@ -238,6 +240,62 @@ function createSchemaFilteredSecurityConfig(securityConfig) {
238
240
  };
239
241
  }
240
242
 
243
+ function deepClone(value) {
244
+ if (typeof structuredClone === 'function') {
245
+ return structuredClone(value);
246
+ }
247
+
248
+ return JSON.parse(JSON.stringify(value));
249
+ }
250
+
251
+ function cloneSimulationSchema(schema) {
252
+ if (schema && schema.form && Array.isArray(schema.form.elements)) {
253
+ return deepClone(schema);
254
+ }
255
+
256
+ return {
257
+ form: deepClone(normalizeFormSchema(schema)),
258
+ };
259
+ }
260
+
261
+ function findFieldByDataName(elements, fieldDataName) {
262
+ if (!Array.isArray(elements)) {
263
+ return null;
264
+ }
265
+
266
+ for (const element of elements) {
267
+ if (!element || typeof element !== 'object') {
268
+ continue;
269
+ }
270
+
271
+ if (element.data_name === fieldDataName) {
272
+ return element;
273
+ }
274
+
275
+ if (Array.isArray(element.elements)) {
276
+ const nestedMatch = findFieldByDataName(element.elements, fieldDataName);
277
+ if (nestedMatch) {
278
+ return nestedMatch;
279
+ }
280
+ }
281
+ }
282
+
283
+ return null;
284
+ }
285
+
286
+ function sanitizeCollectedWarnings(warnings) {
287
+ if (!Array.isArray(warnings)) {
288
+ return [];
289
+ }
290
+
291
+ return warnings.map((warning) => ({
292
+ message: warning?.message || '',
293
+ suggestion: warning?.suggestion || null,
294
+ context: warning?.context || null,
295
+ fieldContext: warning?.fieldContext || null,
296
+ }));
297
+ }
298
+
241
299
  /**
242
300
  * @typedef {'stable' | 'advanced' | 'unavailable'} CalculationBuiltinStatus
243
301
  *
@@ -555,3 +613,94 @@ export function analyzeCalculationExpression({
555
613
  referencedFields,
556
614
  };
557
615
  }
616
+
617
+ /**
618
+ * Simulate a CalculatedField expression against form0-core's real evaluation engine.
619
+ *
620
+ * @param {Object} options
621
+ * @param {Object} options.schema
622
+ * @param {string} options.fieldDataName
623
+ * @param {string} options.expression
624
+ * @param {Object} [options.values]
625
+ * @param {Object} [options.security]
626
+ * @returns {{
627
+ * result: unknown,
628
+ * runtimeError: string | null,
629
+ * warnings: Array<{
630
+ * message: string,
631
+ * suggestion: string | null,
632
+ * context: Object | null,
633
+ * fieldContext: Object | null,
634
+ * }>,
635
+ * }}
636
+ */
637
+ export function simulateCalculationExpression({
638
+ schema,
639
+ fieldDataName,
640
+ expression,
641
+ values = {},
642
+ security = DEFAULT_SECURITY_CONFIG,
643
+ }) {
644
+ const simulationSchema = cloneSimulationSchema(schema);
645
+ const form = normalizeFormSchema(simulationSchema);
646
+ const targetField = findFieldByDataName(form.elements, fieldDataName);
647
+
648
+ if (!targetField) {
649
+ return {
650
+ result: null,
651
+ runtimeError: `Field '${fieldDataName}' does not exist in the form schema.`,
652
+ warnings: [],
653
+ };
654
+ }
655
+
656
+ if (targetField.type !== 'CalculatedField') {
657
+ return {
658
+ result: null,
659
+ runtimeError: `Field '${fieldDataName}' is not a CalculatedField.`,
660
+ warnings: [],
661
+ };
662
+ }
663
+
664
+ targetField.calculate = typeof expression === 'string' ? expression : '';
665
+
666
+ const warningSystem = new WarningSystem({
667
+ enableCollection: true,
668
+ enableConsoleWarnings: false,
669
+ });
670
+ const runtimeDiagnostics = [];
671
+
672
+ try {
673
+ const engine = createFormEngine({
674
+ schema: simulationSchema,
675
+ initialValues:
676
+ values && typeof values === 'object' && !Array.isArray(values) ? values : {},
677
+ security,
678
+ warningSystem,
679
+ runtimeDiagnostics,
680
+ });
681
+
682
+ engine.eval();
683
+
684
+ const engineState = engine.getState();
685
+ const simulatedResult = engineState.values[fieldDataName];
686
+ const runtimeError =
687
+ runtimeDiagnostics.find((diagnostic) => diagnostic.fieldName === fieldDataName)?.message ||
688
+ runtimeDiagnostics[0]?.message ||
689
+ null;
690
+
691
+ return {
692
+ result: simulatedResult === undefined ? null : simulatedResult,
693
+ runtimeError,
694
+ warnings: sanitizeCollectedWarnings(warningSystem.getCollectedWarnings()),
695
+ };
696
+ } catch (error) {
697
+ return {
698
+ result: null,
699
+ runtimeError:
700
+ error instanceof Error && error.message
701
+ ? error.message
702
+ : 'Unknown calculation simulation error.',
703
+ warnings: sanitizeCollectedWarnings(warningSystem.getCollectedWarnings()),
704
+ };
705
+ }
706
+ }