form0-core 0.1.5 → 0.1.7

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.7",
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,8 @@ export {
35
35
  getCalculationBuiltinCatalog,
36
36
  getCalculationReferenceCatalog,
37
37
  analyzeCalculationExpression,
38
+ createCalculationPreviewSession,
39
+ simulateCalculationExpression,
38
40
  } from './utilities/calculation-editor.js';
39
41
  export {
40
42
  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,89 @@ 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
+
299
+ function clearRecord(record) {
300
+ if (!record || typeof record !== 'object') {
301
+ return;
302
+ }
303
+
304
+ Object.keys(record).forEach((key) => {
305
+ delete record[key];
306
+ });
307
+ }
308
+
309
+ function resetEngineValues(targetValues, sourceValues) {
310
+ clearRecord(targetValues);
311
+ Object.assign(targetValues, deepClone(sourceValues));
312
+ }
313
+
314
+ function normalizeSimulationValues(values) {
315
+ return values && typeof values === 'object' && !Array.isArray(values) ? values : {};
316
+ }
317
+
318
+ function createSimulationErrorResult(runtimeError, warnings = []) {
319
+ return {
320
+ result: null,
321
+ runtimeError,
322
+ warnings,
323
+ };
324
+ }
325
+
241
326
  /**
242
327
  * @typedef {'stable' | 'advanced' | 'unavailable'} CalculationBuiltinStatus
243
328
  *
@@ -555,3 +640,172 @@ export function analyzeCalculationExpression({
555
640
  referencedFields,
556
641
  };
557
642
  }
643
+
644
+ /**
645
+ * Create a reusable preview session for CalculatedField authoring.
646
+ *
647
+ * @param {Object} options
648
+ * @param {Object} options.schema
649
+ * @param {string} options.fieldDataName
650
+ * @param {string} options.expression
651
+ * @param {Object} [options.security]
652
+ * @returns {{
653
+ * run: (options?: {
654
+ * expression?: string,
655
+ * values?: Object,
656
+ * }) => {
657
+ * result: unknown,
658
+ * runtimeError: string | null,
659
+ * warnings: Array<{
660
+ * message: string,
661
+ * suggestion: string | null,
662
+ * context: Object | null,
663
+ * fieldContext: Object | null,
664
+ * }>,
665
+ * },
666
+ * dispose: () => void,
667
+ * }}
668
+ */
669
+ export function createCalculationPreviewSession({
670
+ schema,
671
+ fieldDataName,
672
+ expression,
673
+ security = DEFAULT_SECURITY_CONFIG,
674
+ }) {
675
+ const simulationSchema = cloneSimulationSchema(schema);
676
+ const form = normalizeFormSchema(simulationSchema);
677
+ const targetField = findFieldByDataName(form.elements, fieldDataName);
678
+
679
+ if (!targetField) {
680
+ return {
681
+ run: () =>
682
+ createSimulationErrorResult(
683
+ `Field '${fieldDataName}' does not exist in the form schema.`
684
+ ),
685
+ dispose: () => {},
686
+ };
687
+ }
688
+
689
+ if (targetField.type !== 'CalculatedField') {
690
+ return {
691
+ run: () =>
692
+ createSimulationErrorResult(`Field '${fieldDataName}' is not a CalculatedField.`),
693
+ dispose: () => {},
694
+ };
695
+ }
696
+
697
+ targetField.calculate = typeof expression === 'string' ? expression : '';
698
+
699
+ const warningSystem = new WarningSystem({
700
+ enableCollection: true,
701
+ enableConsoleWarnings: false,
702
+ throttleMs: 0,
703
+ });
704
+ const runtimeDiagnostics = [];
705
+ const engine = createFormEngine({
706
+ schema: simulationSchema,
707
+ initialValues: {},
708
+ security,
709
+ warningSystem,
710
+ runtimeDiagnostics,
711
+ });
712
+
713
+ engine.eval();
714
+
715
+ const engineState = engine.getState();
716
+ const baselineValues = deepClone(engineState.values);
717
+ let currentExpression = targetField.calculate;
718
+ let disposed = false;
719
+
720
+ return {
721
+ run: ({ expression: nextExpression, values = {} } = {}) => {
722
+ if (disposed) {
723
+ return createSimulationErrorResult('Calculation preview session has been disposed.');
724
+ }
725
+
726
+ warningSystem.clearCollectedWarnings();
727
+ runtimeDiagnostics.length = 0;
728
+
729
+ if (typeof nextExpression === 'string' && nextExpression !== currentExpression) {
730
+ targetField.calculate = nextExpression;
731
+ currentExpression = nextExpression;
732
+ }
733
+
734
+ resetEngineValues(engineState.values, baselineValues);
735
+ Object.assign(engineState.values, normalizeSimulationValues(values));
736
+
737
+ try {
738
+ engine.eval();
739
+
740
+ const simulatedResult = engineState.values[fieldDataName];
741
+ const runtimeError =
742
+ runtimeDiagnostics.find((diagnostic) => diagnostic.fieldName === fieldDataName)
743
+ ?.message ||
744
+ runtimeDiagnostics[0]?.message ||
745
+ null;
746
+
747
+ return {
748
+ result: simulatedResult === undefined ? null : simulatedResult,
749
+ runtimeError,
750
+ warnings: sanitizeCollectedWarnings(warningSystem.getCollectedWarnings()),
751
+ };
752
+ } catch (error) {
753
+ return createSimulationErrorResult(
754
+ error instanceof Error && error.message
755
+ ? error.message
756
+ : 'Unknown calculation simulation error.',
757
+ sanitizeCollectedWarnings(warningSystem.getCollectedWarnings())
758
+ );
759
+ }
760
+ },
761
+ dispose: () => {
762
+ disposed = true;
763
+ warningSystem.clearCollectedWarnings();
764
+ runtimeDiagnostics.length = 0;
765
+ clearRecord(engineState.values);
766
+ },
767
+ };
768
+ }
769
+
770
+ /**
771
+ * Simulate a CalculatedField expression against form0-core's real evaluation engine.
772
+ *
773
+ * @param {Object} options
774
+ * @param {Object} options.schema
775
+ * @param {string} options.fieldDataName
776
+ * @param {string} options.expression
777
+ * @param {Object} [options.values]
778
+ * @param {Object} [options.security]
779
+ * @returns {{
780
+ * result: unknown,
781
+ * runtimeError: string | null,
782
+ * warnings: Array<{
783
+ * message: string,
784
+ * suggestion: string | null,
785
+ * context: Object | null,
786
+ * fieldContext: Object | null,
787
+ * }>,
788
+ * }}
789
+ */
790
+ export function simulateCalculationExpression({
791
+ schema,
792
+ fieldDataName,
793
+ expression,
794
+ values = {},
795
+ security = DEFAULT_SECURITY_CONFIG,
796
+ }) {
797
+ const session = createCalculationPreviewSession({
798
+ schema,
799
+ fieldDataName,
800
+ expression,
801
+ security,
802
+ });
803
+ try {
804
+ return session.run({
805
+ expression,
806
+ values,
807
+ });
808
+ } finally {
809
+ session.dispose();
810
+ }
811
+ }