form0-core 0.3.1 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +75 -0
  2. package/package.json +5 -2
  3. package/src/authoring/form-authoring.js +124 -13
  4. package/src/builtins/builtin-definitions.js +320 -0
  5. package/src/builtins/choice/choicelabel.js +2 -17
  6. package/src/builtins/choice/choicelabels.js +2 -17
  7. package/src/builtins/choice/choicevalue.js +2 -20
  8. package/src/builtins/choice/choicevalues.js +2 -17
  9. package/src/builtins/choice/hasother.js +2 -20
  10. package/src/builtins/choice/other.js +2 -20
  11. package/src/builtins/control/eval.js +78 -31
  12. package/src/builtins/control/setresult.js +29 -17
  13. package/src/builtins/date/dateadd.js +24 -0
  14. package/src/builtins/date/days.js +22 -0
  15. package/src/builtins/event/control/off.js +3 -27
  16. package/src/builtins/event/control/on.js +3 -19
  17. package/src/builtins/event/event-operations-collector.js +33 -9
  18. package/src/builtins/event/field/setvalue.js +3 -32
  19. package/src/builtins/event/ui/alert.js +3 -11
  20. package/src/builtins/logical/and.js +2 -17
  21. package/src/builtins/logical/array.js +2 -35
  22. package/src/builtins/logical/count.js +2 -20
  23. package/src/builtins/logical/counta.js +2 -20
  24. package/src/builtins/logical/countblank.js +2 -20
  25. package/src/builtins/logical/if.js +2 -20
  26. package/src/builtins/logical/or.js +2 -17
  27. package/src/builtins/math/abs.js +2 -20
  28. package/src/builtins/math/ceiling.js +2 -20
  29. package/src/builtins/math/cos.js +2 -20
  30. package/src/builtins/math/round.js +2 -23
  31. package/src/builtins/math/sin.js +2 -20
  32. package/src/builtins/registry.js +23 -40
  33. package/src/builtins/schema/datanames.js +9 -20
  34. package/src/builtins/schema/form.js +2 -20
  35. package/src/builtins/string/upper.js +2 -20
  36. package/src/engine/calculation-diagnostics.js +107 -0
  37. package/src/engine/calculation.js +65 -32
  38. package/src/engine/evaluator.js +56 -18
  39. package/src/engine/event-operation-scope.js +61 -0
  40. package/src/engine/event-source.js +164 -0
  41. package/src/engine/events.js +105 -19
  42. package/src/engine/form-engine.js +61 -16
  43. package/src/engine/warning-system.js +7 -5
  44. package/src/schema/field-specs.js +2 -2
  45. package/src/security/validation.js +10 -8
  46. package/src/utilities/calculation-editor.js +42 -6
  47. package/src/utilities/date-utils.js +72 -0
  48. package/src/builtins/docs/choice/choicelabel.mdx +0 -110
  49. package/src/builtins/docs/choice/choicelabels.mdx +0 -230
  50. package/src/builtins/docs/choice/choicevalue.mdx +0 -109
  51. package/src/builtins/docs/choice/choicevalues.mdx +0 -196
  52. package/src/builtins/docs/choice/hasother.mdx +0 -133
  53. package/src/builtins/docs/choice/other.mdx +0 -150
  54. package/src/builtins/docs/control/setresult.mdx +0 -203
  55. package/src/builtins/docs/event/field/setvalue.mdx +0 -239
  56. package/src/builtins/docs/index.mdx +0 -234
  57. package/src/builtins/docs/logical/and.mdx +0 -141
  58. package/src/builtins/docs/logical/if.mdx +0 -94
  59. package/src/builtins/docs/logical/or.mdx +0 -191
  60. package/src/builtins/docs/overview.mdx +0 -258
@@ -1,7 +1,14 @@
1
1
  import { runExpression } from './evaluator.js';
2
- import { __consumeEventOperations } from '../builtins/registry.js';
2
+ import { eventBuiltins } from '../builtins/registry.js';
3
3
  import { ContextResolver } from './context-resolver.js';
4
4
  import { WarningSystem } from './warning-system.js';
5
+ import { createEventSourceRegistry } from './event-source.js';
6
+ import {
7
+ bindEventBuiltins,
8
+ closeEventOperationScope,
9
+ createEventOperationScope,
10
+ runInEventOperationScope,
11
+ } from './event-operation-scope.js';
5
12
 
6
13
  // Global registry to track logged event handlers (development only)
7
14
  const _loggedHandlers = new Set();
@@ -19,6 +26,7 @@ export class EventManager {
19
26
  this.contextResolver = contextResolver || (schema ? new ContextResolver(schema) : null);
20
27
  this.warningSystem = warningSystem || new WarningSystem();
21
28
  this.schema = schema;
29
+ this.callbackSources = createEventSourceRegistry();
22
30
  }
23
31
 
24
32
  /**
@@ -29,7 +37,7 @@ export class EventManager {
29
37
  initializeEventCode(eventCode, context) {
30
38
  this.eventContext = context;
31
39
  try {
32
- // Execute the event code to register listeners (recompile each time)
40
+ // Execute initialization once; retain authored callback source for scoped dispatch.
33
41
  this.executeEventCode(eventCode, context);
34
42
  } catch (error) {
35
43
  console.warn('[form0] Event code initialization failed:', error.message);
@@ -42,13 +50,79 @@ export class EventManager {
42
50
  * @param {Object} context - The execution context
43
51
  */
44
52
  executeEventCode(code, context) {
45
- // Execute the code to register event listeners (same security as calculated fields)
46
- // ON() and OFF() are now available as regular event builtins
47
- runExpression(code, context, this.securityConfig, true, this.schema);
53
+ const executionContext = {
54
+ type: 'event',
55
+ eventType: 'initialization',
56
+ fieldName: null,
57
+ };
58
+ const scope = this.createOperationScope(executionContext, 'form.events.code');
59
+ const scopedContext = bindEventBuiltins(context, eventBuiltins, scope);
60
+
61
+ try {
62
+ // Execute the code to register event listeners (same security as calculated fields).
63
+ const prepared = this.callbackSources.prepare(code, scopedContext);
64
+ const result = runInEventOperationScope(scope, () =>
65
+ runExpression(prepared.code, prepared.context, this.securityConfig, true, this.schema, {
66
+ sourceExpression: code,
67
+ })
68
+ );
69
+ this.reportAsyncHandlerResult(result, executionContext, 'form.events.code');
70
+ } finally {
71
+ this.processEventOperations(closeEventOperationScope(scope));
72
+ }
73
+ }
74
+
75
+ createOperationScope(executionContext, handlerName) {
76
+ return createEventOperationScope((operationName) => {
77
+ this.emitHandlerWarning(
78
+ 'late_event_operation_discarded',
79
+ executionContext,
80
+ handlerName,
81
+ operationName
82
+ );
83
+ });
84
+ }
85
+
86
+ emitHandlerWarning(reason, executionContext, handlerName, operationName = null) {
87
+ const eventName = executionContext.eventType;
88
+ const messages = {
89
+ async_handler_unsupported: `Event handler '${handlerName}' for event '${eventName}' returned a Promise or thenable.`,
90
+ late_event_operation_discarded: `Event handler '${handlerName}' for event '${eventName}' attempted ${operationName || 'an event operation'} after its synchronous scope closed. The operation was discarded.`,
91
+ async_handler_rejected: `Event handler '${handlerName}' for event '${eventName}' rejected after its synchronous scope closed.`,
92
+ };
93
+
94
+ this.warningSystem.emitWarning({
95
+ type: 'EVENT_HANDLER_WARNING',
96
+ reason,
97
+ severity: reason === 'async_handler_rejected' ? 'error' : 'warning',
98
+ message: messages[reason],
99
+ suggestion:
100
+ 'Keep event handlers synchronous and create every event operation before the handler returns.',
101
+ executionContext: { ...executionContext },
102
+ fieldContext: {
103
+ handlerName,
104
+ ...(operationName ? { operationName } : {}),
105
+ },
106
+ });
107
+ }
108
+
109
+ reportAsyncHandlerResult(result, executionContext, handlerName) {
110
+ if ((typeof result !== 'object' || result === null) && typeof result !== 'function') return;
111
+
112
+ let then;
113
+ try {
114
+ then = result.then;
115
+ } catch {
116
+ this.emitHandlerWarning('async_handler_rejected', executionContext, handlerName);
117
+ return;
118
+ }
48
119
 
49
- // Process any ON/OFF operations that were collected during initialization
50
- const initOperations = __consumeEventOperations();
51
- this.processEventOperations(initOperations);
120
+ if (typeof then !== 'function') return;
121
+
122
+ this.emitHandlerWarning('async_handler_unsupported', executionContext, handlerName);
123
+ Promise.resolve(result).catch(() => {
124
+ this.emitHandlerWarning('async_handler_rejected', executionContext, handlerName);
125
+ });
52
126
  }
53
127
 
54
128
  /**
@@ -175,7 +249,7 @@ export class EventManager {
175
249
  * @param {Object} event - The event object
176
250
  * @returns {*} The result of the event handler
177
251
  */
178
- executeHandlerWithContext(callback, event) {
252
+ executeHandlerWithContext(callback, event, scope) {
179
253
  // Create execution context for this event
180
254
  const executionContext = {
181
255
  type: 'event',
@@ -184,7 +258,7 @@ export class EventManager {
184
258
  };
185
259
 
186
260
  // Get the callback code for field reference analysis
187
- const callbackCode = callback.toString();
261
+ const callbackCode = this.callbackSources.getSource(callback);
188
262
 
189
263
  // Build scoped context if context resolver is available
190
264
  let contextWithEvent;
@@ -203,10 +277,16 @@ export class EventManager {
203
277
  };
204
278
  }
205
279
 
280
+ contextWithEvent = bindEventBuiltins(contextWithEvent, eventBuiltins, scope);
281
+
206
282
  // Execute the callback function with the scoped context
207
283
  // This ensures EVAL() and other builtins have access to field values
208
284
  const functionCall = `(${callbackCode})(event)`;
209
- return runExpression(functionCall, contextWithEvent, this.securityConfig, true, this.schema);
285
+ return runInEventOperationScope(scope, () =>
286
+ runExpression(functionCall, contextWithEvent, this.securityConfig, true, this.schema, {
287
+ evaluateAsExpression: true,
288
+ })
289
+ );
210
290
  }
211
291
 
212
292
  /**
@@ -408,13 +488,19 @@ export class EventManager {
408
488
  const wildcardListeners = eventMap.get('*') || [];
409
489
 
410
490
  [...fieldListeners, ...wildcardListeners].forEach((callback) => {
491
+ const handlerName = callback.name || '<anonymous>';
492
+ const scope = this.createOperationScope(executionContext, handlerName);
493
+ let result;
494
+ let collectedOps = [];
411
495
  try {
412
496
  // Execute the callback with current form context
413
497
  // This ensures EVAL() and other builtins have access to field values
414
- const result = this.executeHandlerWithContext(callback, event);
415
-
416
- // Consume any collected event operations
417
- const collectedOps = __consumeEventOperations();
498
+ result = this.executeHandlerWithContext(callback, event, scope);
499
+ } catch (error) {
500
+ // Simple error message for now
501
+ console.warn(`[form0] Event listener failed: ${error.message}`);
502
+ } finally {
503
+ collectedOps = closeEventOperationScope(scope);
418
504
  const runtimeOps = collectedOps.filter(
419
505
  (operation) => operation?.type !== 'EVENT_OPERATION'
420
506
  );
@@ -425,16 +511,16 @@ export class EventManager {
425
511
 
426
512
  // EVENT_OPERATION processing is initialization-only. ON/OFF calls inside a handler neither
427
513
  // mutate the listener registry nor leak internal registration descriptors to the host.
514
+ }
428
515
 
516
+ try {
517
+ this.reportAsyncHandlerResult(result, executionContext, handlerName);
429
518
  // For backward compatibility, still handle returned operations
430
- if (result && result.type === 'UI_OPERATION') {
519
+ if (result && result.type === 'UI_OPERATION' && !collectedOps.includes(result)) {
431
520
  operations.push(result);
432
521
  }
433
522
  } catch (error) {
434
- // Simple error message for now
435
523
  console.warn(`[form0] Event listener failed: ${error.message}`);
436
- // Make sure to consume operations even if callback fails
437
- __consumeEventOperations();
438
524
  }
439
525
  });
440
526
 
@@ -13,6 +13,35 @@ 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
15
  import { buildCalculationDependencyPlan } from '../utilities/calculation-dependencies.js';
16
+ import { createCalculationDiagnostics } from './calculation-diagnostics.js';
17
+ import { formatLocalDate } from '../utilities/date-utils.js';
18
+
19
+ /**
20
+ * @typedef {Object} CalculationDiagnostic
21
+ * @property {string} code Stable diagnostic identifier.
22
+ * @property {'warning'|'error'} severity
23
+ * @property {string} message
24
+ * @property {string} fieldName Calculated field's data_name.
25
+ * @property {'dependency'|'scope'|'validation'|'syntax'|'runtime'} phase
26
+ * @property {string|null} suggestion
27
+ * @property {Object} context JSON-safe metadata; no expression source, values or stack.
28
+ */
29
+
30
+ /**
31
+ * Create a form engine. Calculations collect diagnostics independently of validation.
32
+ * @param {Object} options
33
+ * @param {Object} options.schema
34
+ * @param {Object} [options.initialValues]
35
+ * @param {Object} [options.helpers]
36
+ * @param {Object} [options.security]
37
+ * @param {WarningSystem} [options.warningSystem]
38
+ * @param {Array} [options.runtimeDiagnostics] Deprecated append-only compatibility adapter.
39
+ * Use getDiagnostics() for the latest snapshot or onDiagnostics for evaluation notifications.
40
+ * @param {{console?: boolean}} [options.diagnostics] Omit console to preserve legacy reporting.
41
+ * @param {(snapshot: CalculationDiagnostic[]) => (void|Promise<void>)} [options.onDiagnostics]
42
+ * Called once per completed evaluation, including empty/identical snapshots. Failures are isolated.
43
+ * @returns {Object} Engine with getDiagnostics(): defensive copy of latest evaluation (initially []).
44
+ */
16
45
 
17
46
  export function createFormEngine({
18
47
  schema,
@@ -21,6 +50,8 @@ export function createFormEngine({
21
50
  security = DEFAULT_SECURITY_CONFIG,
22
51
  warningSystem = null,
23
52
  runtimeDiagnostics = null,
53
+ diagnostics = {},
54
+ onDiagnostics = null,
24
55
  }) {
25
56
  const { schema: preparedSchema, buildingPlanMeta } = expandBuildingPlanSchema(schema);
26
57
 
@@ -97,6 +128,12 @@ export function createFormEngine({
97
128
  const contextResolver = new ContextResolver(form);
98
129
  const calculationPlan = buildCalculationDependencyPlan(form, contextResolver);
99
130
  const sharedWarningSystem = warningSystem || new WarningSystem();
131
+ const calculationDiagnostics = createCalculationDiagnostics(
132
+ diagnostics,
133
+ onDiagnostics,
134
+ contextResolver,
135
+ sharedWarningSystem
136
+ );
100
137
 
101
138
  // Initialize event system with context resolution
102
139
  const eventManager = new EventManager(form, contextResolver, sharedWarningSystem);
@@ -123,20 +160,28 @@ export function createFormEngine({
123
160
  }
124
161
 
125
162
  function evalForm() {
126
- evaluateCalculatedFields(
127
- form,
128
- values,
129
- allHelpers,
130
- security,
131
- contextResolver,
132
- sharedWarningSystem,
133
- runtimeDiagnostics,
134
- calculationPlan
135
- );
136
- evaluateRequirement(form, values, required);
137
- evaluateVisibility(form, values, visible);
138
- evaluateReadOnly(form, values, read_only);
139
- validateFields(form, values, errors);
163
+ const frame = calculationDiagnostics.begin();
164
+ try {
165
+ evaluateCalculatedFields(
166
+ form,
167
+ values,
168
+ allHelpers,
169
+ security,
170
+ contextResolver,
171
+ sharedWarningSystem,
172
+ runtimeDiagnostics,
173
+ calculationPlan,
174
+ calculationDiagnostics
175
+ );
176
+ evaluateRequirement(form, values, required);
177
+ evaluateVisibility(form, values, visible);
178
+ evaluateReadOnly(form, values, read_only);
179
+ validateFields(form, values, errors);
180
+ } catch (error) {
181
+ calculationDiagnostics.abort(frame);
182
+ throw error;
183
+ }
184
+ calculationDiagnostics.complete(frame);
140
185
  }
141
186
 
142
187
  function trigger(eventType, fieldKey, metadata = {}) {
@@ -158,6 +203,7 @@ export function createFormEngine({
158
203
  eval: evalForm,
159
204
  trigger,
160
205
  getState,
206
+ getDiagnostics: calculationDiagnostics.getSnapshot,
161
207
  getWarningSystem: () => sharedWarningSystem,
162
208
  getContextResolver: () => contextResolver,
163
209
  getBuildingPlanMeta: () => buildingPlanMeta,
@@ -222,8 +268,7 @@ function getDefaultValueLegacy(field) {
222
268
 
223
269
  case 'DateField':
224
270
  if (field.default_value === 'now') {
225
- const today = new Date();
226
- return today.toISOString().split('T')[0]; // YYYY-MM-DD format
271
+ return formatLocalDate(); // YYYY-MM-DD, local calendar day
227
272
  }
228
273
  return null;
229
274
 
@@ -118,7 +118,7 @@ export class WarningSystem {
118
118
  * Emit warning to all registered handlers
119
119
  * @param {Object} warning - Warning object with structured information
120
120
  */
121
- emitWarning(warning) {
121
+ emitWarning(warning, options = {}) {
122
122
  // Throttle warnings to prevent spam
123
123
  if (this.shouldThrottleWarning(warning)) {
124
124
  return;
@@ -136,12 +136,12 @@ export class WarningSystem {
136
136
  }
137
137
 
138
138
  // Console logging for development
139
- if (this.enableConsoleWarnings) {
139
+ if (options.console ?? this.enableConsoleWarnings) {
140
140
  this.logWarningToConsole(warning);
141
141
  }
142
142
 
143
143
  // Custom handlers (for "reform" and other integrations)
144
- this.notifyCustomHandlers(warning);
144
+ this.notifyCustomHandlers(warning, options);
145
145
  }
146
146
 
147
147
  /**
@@ -194,12 +194,14 @@ export class WarningSystem {
194
194
  * Notify all custom warning handlers
195
195
  * @param {Object} warning - Warning object
196
196
  */
197
- notifyCustomHandlers(warning) {
197
+ notifyCustomHandlers(warning, options = {}) {
198
198
  for (const handler of this.warningHandlers) {
199
199
  try {
200
200
  handler(warning);
201
201
  } catch (err) {
202
- console.error('[form0] Warning handler failed:', err);
202
+ if (options.console ?? this.enableConsoleWarnings) {
203
+ console.error('[form0] Warning handler failed:', err);
204
+ }
203
205
  // Don't remove the handler automatically - let the user decide
204
206
  }
205
207
  }
@@ -6,6 +6,7 @@ import {
6
6
  validateFormLinkRecordConditions,
7
7
  } from './form-link-validators.js';
8
8
  import { BUILDING_PLAN_BLUEPRINT } from './building-plan-blueprint.js';
9
+ import { formatLocalDate } from '../utilities/date-utils.js';
9
10
  const AI_ATTRIBUTE_DEFINITION = {
10
11
  type: 'object',
11
12
  required: false,
@@ -725,8 +726,7 @@ export const FIELD_SPECS = {
725
726
  },
726
727
  defaultProducer: (field) => {
727
728
  if (field.default_value === 'now') {
728
- const today = new Date();
729
- return today.toISOString().split('T')[0]; // YYYY-MM-DD format
729
+ return formatLocalDate(); // YYYY-MM-DD, local calendar day
730
730
  }
731
731
  return null;
732
732
  },
@@ -1,15 +1,17 @@
1
- import { calculationBuiltins, eventBuiltins } from '../builtins/registry.js';
1
+ import {
2
+ CALCULATION_BUILTIN_DEFINITIONS,
3
+ EVENT_BUILTIN_DEFINITIONS,
4
+ } from '../builtins/builtin-definitions.js';
2
5
  import { DEFAULT_SECURITY_CONFIG, SAFE_SECURITY_CONFIG, SECURITY_MODES } from './config.js';
3
6
 
4
7
  // Helper function to validate builtin function names in expressions
5
8
  function validateBuiltinNames(expr, includeEventBuiltins = false) {
6
- // Always use fresh builtin sets (no caching) to support dynamic builtin registration
7
- const validCalculationBuiltinsSet = new Set(Object.keys(calculationBuiltins));
8
- const validEventBuiltinsSet = new Set(Object.keys(eventBuiltins));
9
-
10
- const allowedBuiltins = includeEventBuiltins
11
- ? validEventBuiltinsSet
12
- : validCalculationBuiltinsSet;
9
+ // Read the same immutable catalog used to build the runtime helpers, without
10
+ // importing their implementations (EVAL itself depends on this validator).
11
+ const definitions = includeEventBuiltins
12
+ ? EVENT_BUILTIN_DEFINITIONS
13
+ : CALCULATION_BUILTIN_DEFINITIONS;
14
+ const allowedBuiltins = new Set(definitions.map(({ name }) => name));
13
15
 
14
16
  // Extract function calls from the expression
15
17
  // This regex matches function calls like FUNCTIONNAME( allowing for whitespace
@@ -1,3 +1,4 @@
1
+ import { parse } from 'acorn';
1
2
  import { BUILTIN_CONTEXTS } from '../builtins/builtin-metadata.js';
2
3
  import {
3
4
  BUILTIN_DEFINITION_BY_NAME,
@@ -143,6 +144,38 @@ function isFinalFunctionCall(code, call) {
143
144
  return index === code.length;
144
145
  }
145
146
 
147
+ function hasTopLevelReturnStatement(code) {
148
+ let program;
149
+ try {
150
+ program = parse(code, {
151
+ ecmaVersion: 'latest',
152
+ allowReturnOutsideFunction: true,
153
+ });
154
+ } catch {
155
+ return false;
156
+ }
157
+
158
+ const visit = (node) => {
159
+ if (!node || typeof node !== 'object') return false;
160
+ if (node.type === 'ReturnStatement') return true;
161
+ if (
162
+ node.type === 'FunctionDeclaration' ||
163
+ node.type === 'FunctionExpression' ||
164
+ node.type === 'ArrowFunctionExpression'
165
+ ) {
166
+ return false;
167
+ }
168
+
169
+ return Object.entries(node).some(([key, value]) => {
170
+ if (key === 'start' || key === 'end' || key === 'loc') return false;
171
+ if (Array.isArray(value)) return value.some(visit);
172
+ return visit(value);
173
+ });
174
+ };
175
+
176
+ return program.body.some(visit);
177
+ }
178
+
146
179
  function cloneCatalogDefinition(definition) {
147
180
  return {
148
181
  ...definition,
@@ -582,14 +615,19 @@ export function analyzeCalculationExpression({
582
615
  );
583
616
  }
584
617
 
585
- if (isMultilineCalculationExpression(normalizedExpression) && setResultCalls.length === 0) {
618
+ if (
619
+ isMultilineCalculationExpression(normalizedExpression) &&
620
+ setResultCalls.length === 0 &&
621
+ !hasTopLevelReturnStatement(normalizedExpression)
622
+ ) {
586
623
  addIssue(
587
624
  issues,
588
625
  issueKeys,
589
626
  createIssue({
590
627
  code: 'noncanonical_multiline_result',
591
628
  severity: 'warning',
592
- message: 'Multiline calculations should finish with exactly one SETRESULT() call.',
629
+ message:
630
+ 'Multiline calculations should finish with exactly one SETRESULT() call or an explicit return statement.',
593
631
  index: 0,
594
632
  length: 1,
595
633
  })
@@ -754,13 +792,12 @@ export function createCalculationPreviewSession({
754
792
  enableConsoleWarnings: false,
755
793
  throttleMs: 0,
756
794
  });
757
- const runtimeDiagnostics = [];
758
795
  const engine = createFormEngine({
759
796
  schema: simulationSchema,
760
797
  initialValues: {},
761
798
  security,
762
799
  warningSystem,
763
- runtimeDiagnostics,
800
+ diagnostics: { console: false },
764
801
  });
765
802
 
766
803
  engine.eval();
@@ -777,7 +814,6 @@ export function createCalculationPreviewSession({
777
814
  }
778
815
 
779
816
  warningSystem.clearCollectedWarnings();
780
- runtimeDiagnostics.length = 0;
781
817
 
782
818
  if (typeof nextExpression === 'string' && nextExpression !== currentExpression) {
783
819
  targetField.calculate = nextExpression;
@@ -790,6 +826,7 @@ export function createCalculationPreviewSession({
790
826
  try {
791
827
  engine.eval();
792
828
 
829
+ const runtimeDiagnostics = engine.getDiagnostics();
793
830
  const simulatedResult = engineState.values[fieldDataName];
794
831
  const runtimeError =
795
832
  runtimeDiagnostics.find(
@@ -818,7 +855,6 @@ export function createCalculationPreviewSession({
818
855
  dispose: () => {
819
856
  disposed = true;
820
857
  warningSystem.clearCollectedWarnings();
821
- runtimeDiagnostics.length = 0;
822
858
  clearRecord(engineState.values);
823
859
  },
824
860
  };
@@ -0,0 +1,72 @@
1
+ // Calendar-date helpers for DateField values, which use the YYYY-MM-DD format.
2
+ // Dates are handled as UTC calendar days so that arithmetic never shifts across DST changes.
3
+
4
+ const ISO_DATE_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
5
+ const MS_PER_DAY = 24 * 60 * 60 * 1000;
6
+ const MIN_CALENDAR_YEAR = 1;
7
+ const MAX_CALENDAR_YEAR = 9999;
8
+
9
+ const pad = (value, length = 2) => String(value).padStart(length, '0');
10
+
11
+ /**
12
+ * Format a Date as YYYY-MM-DD using the local calendar day.
13
+ *
14
+ * @param {Date} [date]
15
+ * @returns {string}
16
+ */
17
+ export function formatLocalDate(date = new Date()) {
18
+ return `${pad(date.getFullYear(), 4)}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
19
+ }
20
+
21
+ /**
22
+ * Parse a YYYY-MM-DD string into a day number (days since 1970-01-01).
23
+ *
24
+ * @param {string} value
25
+ * @returns {number | null} null when the value is not a valid calendar date
26
+ */
27
+ export function parseIsoDate(value) {
28
+ if (typeof value !== 'string') return null;
29
+ const match = ISO_DATE_PATTERN.exec(value);
30
+ if (!match) return null;
31
+
32
+ const [year, month, day] = match.slice(1).map(Number);
33
+ if (year < MIN_CALENDAR_YEAR || year > MAX_CALENDAR_YEAR) return null;
34
+
35
+ const date = new Date(0);
36
+ date.setUTCFullYear(year, month - 1, day);
37
+ if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month - 1) return null;
38
+ if (date.getUTCDate() !== day) return null;
39
+
40
+ return date.getTime() / MS_PER_DAY;
41
+ }
42
+
43
+ /**
44
+ * Format a day number (days since 1970-01-01) as YYYY-MM-DD.
45
+ *
46
+ * @param {number} dayNumber
47
+ * @returns {string}
48
+ */
49
+ export function formatIsoDate(dayNumber) {
50
+ const date = new Date(dayNumber * MS_PER_DAY);
51
+ const year = date.getUTCFullYear();
52
+ if (
53
+ !Number.isInteger(dayNumber) ||
54
+ Number.isNaN(date.getTime()) ||
55
+ year < MIN_CALENDAR_YEAR ||
56
+ year > MAX_CALENDAR_YEAR
57
+ ) {
58
+ throw new Error(
59
+ 'Date arithmetic result is outside the supported date range 0001-01-01 to 9999-12-31'
60
+ );
61
+ }
62
+
63
+ return `${pad(year, 4)}-${pad(date.getUTCMonth() + 1)}-${pad(date.getUTCDate())}`;
64
+ }
65
+
66
+ /**
67
+ * @param {unknown} value
68
+ * @returns {boolean}
69
+ */
70
+ export function isBlankDateArgument(value) {
71
+ return value === null || value === undefined || value === '';
72
+ }
@@ -1,110 +0,0 @@
1
- ---
2
- title: "CHOICELABEL Builtin"
3
- description: "Retrieve the selected choice field label with type preservation"
4
- category: "choice"
5
- tags: ["choice", "label", "display"]
6
- ---
7
-
8
- # CHOICELABEL Function
9
-
10
- The `CHOICELABEL` builtin retrieves the currently selected choice field label, preserving the original data type. If the label is a number, it preserves the number type; otherwise, it returns a string.
11
-
12
- ## Syntax
13
-
14
- ```javascript
15
- CHOICELABEL(choiceField)
16
- ```
17
-
18
- ## Parameters
19
-
20
- | Parameter | Type | Description |
21
- |-----------|------|-------------|
22
- | `choiceField` | `Object` | The choice field object with choice and other arrays |
23
-
24
- ## Return Value
25
-
26
- Returns the selected choice label with preserved type, or `null` if no selection is made.
27
-
28
- ## Examples
29
-
30
- ### Basic Usage
31
-
32
- ```javascript
33
- // Get the selected city label
34
- CHOICELABEL($city)
35
- ```
36
-
37
- ### Display Logic
38
-
39
- ```javascript
40
- // Use in display expressions
41
- "You selected: " + CHOICELABEL($city)
42
- ```
43
-
44
- ### Conditional Display
45
-
46
- ```javascript
47
- // Show different content based on label
48
- IF(CHOICELABEL($membership) === "Premium", "Premium benefits apply", "Standard benefits")
49
- ```
50
-
51
- ### Form Feedback
52
-
53
- ```javascript
54
- // Provide user feedback
55
- "Your choice: " + CHOICELABEL($preference)
56
- ```
57
-
58
- ## Type Preservation
59
-
60
- The function preserves the original data type of the choice label:
61
-
62
- ```javascript
63
- // If choice label is "42" (string), returns 42 (number)
64
- // If choice label is "Premium" (string), returns "Premium" (string)
65
- // If choice label is 5 (number), returns 5 (number)
66
- ```
67
-
68
-
69
-
70
- ## Common Use Cases
71
-
72
- - **User feedback**: Display the human-readable label of selected choices
73
- - **Conditional display**: Show different content based on selected labels
74
- - **Form summaries**: Include readable choice labels in form summaries
75
- - **Reporting**: Use labels in reports and analytics
76
-
77
- ## Best Practices
78
-
79
- 1. **User-friendly display**: Use labels for user-facing content instead of values
80
- 2. **Handle null values**: Always check for null when no selection is made
81
- 3. **Internationalization**: Consider that labels might be translated
82
-
83
- ## Related Functions
84
-
85
- - [`CHOICEVALUE`](./choicevalue.mdx) - Get the selected choice value
86
- - [`HASOTHER`](./hasother.mdx) - Check if other option is selected
87
- - [`OTHER`](./other.mdx) - Get the other option value
88
-
89
- ## Examples by Use Case
90
-
91
- ### User Confirmation
92
-
93
- ```javascript
94
- // Confirm user selection
95
- "You have selected: " + CHOICELABEL($plan)
96
- ```
97
-
98
- ### Conditional Messaging
99
-
100
- ```javascript
101
- // Show different messages based on selection
102
- IF(CHOICELABEL($support) === "Priority Support", "Priority support activated", "Standard support")
103
- ```
104
-
105
- ### Form Summary
106
-
107
- ```javascript
108
- // Create form summary
109
- "Location: " + CHOICELABEL($location) + ", Plan: " + CHOICELABEL($plan)
110
- ```