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
package/README.md CHANGED
@@ -81,6 +81,69 @@ console.log(engine.getState());
81
81
  The engine API exposes `eval()` for calculations, conditions, and validation; `trigger()` for form
82
82
  events; and `getState()` for current values and evaluated field state.
83
83
 
84
+ Schema event callbacks retain their authored source, so ordinary functions and arrows defined in
85
+ `form.events.code` also work on Hermes. Each dispatch uses current scoped field values; initialization
86
+ runs once. Local initialization variables are not captured. Callbacks supplied by host helpers still
87
+ require executable `toString()` output; unavailable source produces an event warning.
88
+
89
+ ## Calculation diagnostics
90
+
91
+ Diagnostics are collected automatically, including when console reporting is disabled:
92
+
93
+ ```js
94
+ const engine = createFormEngine({ schema });
95
+ console.log(engine.getDiagnostics()); // [] before the first evaluation
96
+ engine.eval();
97
+ console.log(engine.getDiagnostics()); // latest evaluation, returned as a defensive copy
98
+ ```
99
+
100
+ Configure reporting in application code:
101
+
102
+ ```js
103
+ const engine = createFormEngine({
104
+ schema,
105
+ diagnostics: { console: false },
106
+ onDiagnostics(snapshot) {
107
+ for (const diagnostic of snapshot) {
108
+ reportCalculationDiagnostic(diagnostic);
109
+ }
110
+ },
111
+ });
112
+
113
+ engine.eval();
114
+ // For a schema calculating quotient from divisor, correct the input and evaluate again:
115
+ engine.getState().values.divisor = 4;
116
+ engine.eval(); // replaces the snapshot; recovered failures disappear
117
+ ```
118
+
119
+ `onDiagnostics` runs once after every completed evaluation, including identical snapshots and
120
+ `[]`. Observer exceptions cannot interrupt evaluation and are reported according to the console
121
+ policy. Each diagnostic has `code`, `severity`, `message`, `fieldName`, `phase`, `suggestion`, and
122
+ `context`. Context identifies the schema parent path, related/referenced fields, and `expression`
123
+ versus `EVAL` origin. The engine adds no expression source, field values, or stack traces.
124
+
125
+ Identical diagnostics are deduplicated within an evaluation. Distinct failures are retained,
126
+ including failures observed in intermediate stabilization passes even if a later pass succeeds.
127
+ The snapshot records that evaluation; it is not a history or just the final unresolved errors.
128
+ Legitimate `null`, `undefined`, `NaN`, and `Infinity` results do not produce errors by themselves.
129
+ Diagnostics stay separate from `state.errors` and do not change submission validity.
130
+
131
+ Omit `diagnostics.console` to preserve existing defaults and supplied `WarningSystem` settings.
132
+ `false` silences calculation-owned reporting, including `EVAL()` warnings and debug logs; `true`
133
+ prints diagnostic reporting in development and production. Event reporting retains its existing
134
+ behavior. `WarningSystem` remains compatible.
135
+
136
+ The append-only `runtimeDiagnostics` option is deprecated but remains supported for compatibility.
137
+ For new integrations, omit that option and use `engine.getDiagnostics()` for the latest evaluation,
138
+ or `onDiagnostics(snapshot)` to receive every completed evaluation, including recovery (`[]`).
139
+ This is not a drop-in replacement for the legacy array: snapshots use the structured diagnostic
140
+ contract above and replace prior results rather than accumulating them. If you need a history,
141
+ store snapshots in application code. No removal version is scheduled.
142
+
143
+ See [calculation diagnostics](https://docs.form0.dev/core/engine/calculations-dependencies#seeing-errors-and-warnings)
144
+ for stable codes and the reporting contract. CLI and renderer diagnostics adoption is separate
145
+ from this core API.
146
+
84
147
  ## Schema and record ownership
85
148
 
86
149
  `form0-core` owns behavioral schema concerns such as fields, conditions, calculations, events, and
@@ -109,6 +172,18 @@ Record utilities follow this contract:
109
172
 
110
173
  Record status remains top-level as `@status`; it is not stored inside `form_values`.
111
174
 
175
+ ## Authoring mutation validation
176
+
177
+ `getFormAuthoringContext({ schema })` includes `mutationOperationCatalog`: the installed operation
178
+ formats, required parameters, and examples. `mutationOperations` remains the list of supported names.
179
+ Use these formats with `applyFormMutationBatch`; for example, a rename uses
180
+ `{ op: 'updateForm', changes: { name: 'Renamed form' } }`, not a `patch` parameter.
181
+
182
+ Malformed operations, unknown operation parameters, and empty change objects are rejected atomically
183
+ with diagnostics, rather than silently ignored. Clearing a title/status field, calculation, or event
184
+ code requires its explicit parameter (for example, `field: null`), not an omitted value. Valid
185
+ idempotent batches remain valid in core; consumers should check actual changes before offering to save.
186
+
112
187
  ## ✅ Requirements
113
188
 
114
189
  - Node.js 22 or newer
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "form0-core",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org/"
6
6
  },
@@ -12,7 +12,7 @@
12
12
  "SECURITY.md"
13
13
  ],
14
14
  "scripts": {
15
- "test": "node tests/form0-core.test.js && node tests/conditions.test.js && node tests/numeric-field-schema.test.js && node tests/dataset-descriptors.test.js && node tests/structured-record.test.js && node tests/title-fields.test.js && node tests/builtin-metadata.test.js && node tests/calculation-expression-utils.test.js && node tests/calculation-editor.test.js && node tests/form-event-editor.test.js && node tests/form-authoring.test.js && node tests/security-test.js",
15
+ "test": "node tests/form0-core.test.js && node tests/conditions.test.js && node tests/numeric-field-schema.test.js && node tests/dataset-descriptors.test.js && node tests/structured-record.test.js && node tests/title-fields.test.js && node tests/builtin-metadata.test.js && node tests/builtin-import-isolation.test.js && node tests/builtin-catalog-examples.test.js && node tests/date-builtins.test.js && node tests/calculation-expression-utils.test.js && node tests/calculation-editor.test.js && node tests/form-event-editor.test.js && node tests/form-authoring.test.js && node tests/calculation-dependency-engine.test.js && node tests/calculation-diagnostics.characterization.test.js && node tests/calculation-diagnostics.test.js && node tests/calculation-missing-result.test.js && node tests/evaluator-reentry.test.js && node tests/event-compatibility.characterization.test.js && node tests/event-hermes.test.js && node tests/event-async-isolation.test.js && node tests/security-test.js",
16
16
  "format": "prettier --write \"src/**/*.{js,jsx}\" \"tests/**/*.{js,jsx}\" \"scripts/**/*.{js,mjs}\" package.json README.md SECURITY.md SUPPORT.md CONTRIBUTING.md eslint.config.js .github/workflows/ci.yml .github/workflows/publish.yml",
17
17
  "format:check": "prettier --check \"src/**/*.{js,jsx}\" \"tests/**/*.{js,jsx}\" \"scripts/**/*.{js,mjs}\" package.json README.md SECURITY.md SUPPORT.md CONTRIBUTING.md eslint.config.js .github/workflows/ci.yml .github/workflows/publish.yml",
18
18
  "lint:check": "eslint src tests",
@@ -63,5 +63,8 @@
63
63
  "globals": "^17.12.0",
64
64
  "ignore": "^7.0.5",
65
65
  "prettier": "^3.5.3"
66
+ },
67
+ "dependencies": {
68
+ "acorn": "8.18.0"
66
69
  }
67
70
  }
@@ -16,17 +16,77 @@ import {
16
16
  } from '../utilities/form-event-editor.js';
17
17
 
18
18
  const CONTAINER_TYPES = new Set(['Section', 'RepeatableSection', 'BuildingPlanSection']);
19
- const MUTATION_OPERATIONS = Object.freeze([
20
- 'updateForm',
21
- 'addField',
22
- 'updateField',
23
- 'moveField',
24
- 'removeField',
25
- 'setTitleField',
26
- 'setStatusField',
27
- 'setCalculation',
28
- 'setFormEventCode',
19
+ const FIELD_KEY_PARAMETER = Object.freeze({ type: 'string', minLength: 1 });
20
+ const CHANGES_PARAMETER = Object.freeze({ type: 'object', minProperties: 1 });
21
+ const PLACEMENT_PARAMETERS = Object.freeze({
22
+ parentKey: { type: ['string', 'null'], minLength: 1 },
23
+ position: {
24
+ type: ['object', 'null'],
25
+ properties: { beforeKey: FIELD_KEY_PARAMETER, afterKey: FIELD_KEY_PARAMETER },
26
+ maxProperties: 1,
27
+ description: 'Use one sibling key, or omit position to append in the target container.',
28
+ },
29
+ });
30
+ const MUTATION_OPERATION_CATALOG = Object.freeze([
31
+ {
32
+ op: 'updateForm',
33
+ description:
34
+ 'Update form attributes; elements, events, title_field and status_field use dedicated operations.',
35
+ required: ['changes'],
36
+ parameters: { changes: CHANGES_PARAMETER },
37
+ example: { op: 'updateForm', changes: { name: 'Renamed form' } },
38
+ },
39
+ {
40
+ op: 'addField',
41
+ description: 'Add a complete field matching fieldSpecs; missing keys are generated.',
42
+ required: ['field'],
43
+ parameters: { field: { type: 'object', minProperties: 1 }, ...PLACEMENT_PARAMETERS },
44
+ },
45
+ {
46
+ op: 'updateField',
47
+ description: 'Update an existing field by key; type changes require remove/add.',
48
+ required: ['fieldKey', 'changes'],
49
+ parameters: { fieldKey: FIELD_KEY_PARAMETER, changes: CHANGES_PARAMETER },
50
+ example: { op: 'updateField', fieldKey: 'existing-field-key', changes: { label: 'New label' } },
51
+ },
52
+ {
53
+ op: 'moveField',
54
+ description: 'Move an existing field by key to a container/sibling position.',
55
+ required: ['fieldKey'],
56
+ parameters: { fieldKey: FIELD_KEY_PARAMETER, ...PLACEMENT_PARAMETERS },
57
+ },
58
+ {
59
+ op: 'removeField',
60
+ description: 'Remove an existing field by key only if it is unreferenced.',
61
+ required: ['fieldKey'],
62
+ parameters: { fieldKey: FIELD_KEY_PARAMETER },
63
+ },
64
+ {
65
+ op: 'setTitleField',
66
+ description: 'Set a complete title field, or explicitly pass field: null to clear it.',
67
+ required: ['field'],
68
+ parameters: { field: { type: ['object', 'null'] } },
69
+ },
70
+ {
71
+ op: 'setStatusField',
72
+ description: 'Set a complete status field, or explicitly pass field: null to clear it.',
73
+ required: ['field'],
74
+ parameters: { field: { type: ['object', 'null'] } },
75
+ },
76
+ {
77
+ op: 'setCalculation',
78
+ description: 'Set a CalculatedField expression by key; null explicitly clears it.',
79
+ required: ['fieldKey', 'expression'],
80
+ parameters: { fieldKey: FIELD_KEY_PARAMETER, expression: { type: ['string', 'null'] } },
81
+ },
82
+ {
83
+ op: 'setFormEventCode',
84
+ description: 'Set form event code; an empty string or null explicitly clears it.',
85
+ required: ['code'],
86
+ parameters: { code: { type: ['string', 'null'] } },
87
+ },
29
88
  ]);
89
+ const MUTATION_OPERATIONS = Object.freeze(MUTATION_OPERATION_CATALOG.map(({ op }) => op));
30
90
 
31
91
  const CALCULATION_GUIDANCE = Object.freeze({
32
92
  version: 1,
@@ -306,16 +366,66 @@ function mutationError(index, operation, error) {
306
366
  };
307
367
  }
308
368
 
369
+ function validateMutationParameter(value, spec, path) {
370
+ const type = value === null ? 'null' : Array.isArray(value) ? 'array' : typeof value;
371
+ const types = Array.isArray(spec.type) ? spec.type : [spec.type];
372
+ if (!types.includes(type)) throw new Error(`${path} must be ${types.join(' or ')}`);
373
+ if (value === null) return;
374
+ if (spec.minLength && value.length < spec.minLength) {
375
+ throw new Error(`${path} must not be empty`);
376
+ }
377
+ if (spec.minProperties && Object.keys(value).length < spec.minProperties) {
378
+ throw new Error(`${path} must be a non-empty object`);
379
+ }
380
+ if (spec.maxProperties && Object.keys(value).length > spec.maxProperties) {
381
+ throw new Error(`${path} must specify at most one property`);
382
+ }
383
+ if (spec.properties) {
384
+ for (const key of Object.keys(value)) {
385
+ if (!Object.hasOwn(spec.properties, key)) {
386
+ throw new Error(`Unexpected mutation parameter '${path}.${key}'`);
387
+ }
388
+ validateMutationParameter(value[key], spec.properties[key], `${path}.${key}`);
389
+ }
390
+ }
391
+ }
392
+
393
+ function validateMutationOperation(operation) {
394
+ if (!operation || typeof operation !== 'object' || Array.isArray(operation)) {
395
+ throw new Error('A mutation operation must be an object with an op property');
396
+ }
397
+ const definition = MUTATION_OPERATION_CATALOG.find(({ op }) => op === operation.op);
398
+ if (!definition) throw new Error(`Unsupported mutation operation '${operation.op}'`);
399
+ for (const name of definition.required) {
400
+ if (!Object.hasOwn(operation, name) || operation[name] === undefined) {
401
+ const example = definition.example ? ` Example: ${JSON.stringify(definition.example)}` : '';
402
+ throw new Error(`${operation.op} requires '${name}'.${example}`);
403
+ }
404
+ }
405
+ for (const name of Object.keys(operation)) {
406
+ if (name === 'op') continue;
407
+ if (!Object.hasOwn(definition.parameters, name)) {
408
+ throw new Error(`Unexpected ${operation.op} parameter '${name}'`);
409
+ }
410
+ validateMutationParameter(
411
+ operation[name],
412
+ definition.parameters[name],
413
+ `${operation.op}.${name}`
414
+ );
415
+ }
416
+ }
417
+
309
418
  function applyMutation(root, operation) {
419
+ validateMutationOperation(operation);
310
420
  const form = root.form;
311
421
  const fields = indexFields(form.elements);
312
422
  switch (operation.op) {
313
423
  case 'updateForm': {
314
424
  const forbidden = new Set(['elements', 'events', 'title_field', 'status_field']);
315
- for (const key of Object.keys(operation.changes || {})) {
425
+ for (const key of Object.keys(operation.changes)) {
316
426
  if (forbidden.has(key)) throw new Error(`updateForm cannot change '${key}'`);
317
427
  }
318
- Object.assign(form, deepClone(operation.changes || {}));
428
+ Object.assign(form, deepClone(operation.changes));
319
429
  return `Updated form attributes`;
320
430
  }
321
431
  case 'addField': {
@@ -340,7 +450,7 @@ function applyMutation(root, operation) {
340
450
  }
341
451
  const original = deepClone(target.field);
342
452
  const children = target.field.elements;
343
- Object.assign(target.field, deepClone(operation.changes || {}));
453
+ Object.assign(target.field, deepClone(operation.changes));
344
454
  if (
345
455
  target.field.data_name !== original.data_name &&
346
456
  [...fields.values()].some(
@@ -473,6 +583,7 @@ export function getFormAuthoringContext({ schema, coreVersion = null }) {
473
583
  revision: getFormSchemaRevision(root),
474
584
  schema: deepClone(root),
475
585
  mutationOperations: [...MUTATION_OPERATIONS],
586
+ mutationOperationCatalog: deepClone(MUTATION_OPERATION_CATALOG),
476
587
  fieldSpecs,
477
588
  operators,
478
589
  eventTypes: getAllEventTypes(),
@@ -0,0 +1,320 @@
1
+ import { BUILTIN_CONTEXTS, defineBuiltinMetadata } from './builtin-metadata.js';
2
+
3
+ // Authoritative builtin metadata. Keep this catalog independent of implementations
4
+ // so security validation can inspect names and contexts without loading EVAL().
5
+
6
+ export const IF_METADATA = defineBuiltinMetadata({
7
+ name: 'IF',
8
+ category: 'logical',
9
+ signature: 'IF(condition, trueValue, falseValue)',
10
+ description: 'Return one of two values based on a condition.',
11
+ examples: [
12
+ 'IF($age >= 18, "adult", "minor")',
13
+ 'IF($country === "US", IF($state === "CA", 8.99, 5.99), 15.99)',
14
+ ],
15
+ contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
16
+ });
17
+
18
+ export const AND_METADATA = defineBuiltinMetadata({
19
+ name: 'AND',
20
+ category: 'logical',
21
+ signature: 'AND(...conditions)',
22
+ description: 'Return true only when all arguments are truthy.',
23
+ examples: ['AND($age >= 18, $country === "it")'],
24
+ contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
25
+ });
26
+
27
+ export const OR_METADATA = defineBuiltinMetadata({
28
+ name: 'OR',
29
+ category: 'logical',
30
+ signature: 'OR(...conditions)',
31
+ description: 'Return true when at least one argument is truthy.',
32
+ examples: ['OR($email, $phone)'],
33
+ contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
34
+ });
35
+
36
+ export const COUNT_METADATA = defineBuiltinMetadata({
37
+ name: 'COUNT',
38
+ category: 'logical',
39
+ signature: 'COUNT(values)',
40
+ description: 'Count numeric values.',
41
+ examples: ['COUNT(ARRAY($score_1, $score_2, $score_3))'],
42
+ contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
43
+ });
44
+
45
+ export const COUNTA_METADATA = defineBuiltinMetadata({
46
+ name: 'COUNTA',
47
+ category: 'logical',
48
+ signature: 'COUNTA(values)',
49
+ description: 'Count non-empty values.',
50
+ examples: ['COUNTA(ARRAY($first_name, $last_name, $email))'],
51
+ contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
52
+ });
53
+
54
+ export const COUNTBLANK_METADATA = defineBuiltinMetadata({
55
+ name: 'COUNTBLANK',
56
+ category: 'logical',
57
+ signature: 'COUNTBLANK(values)',
58
+ description: 'Count blank values.',
59
+ examples: ['COUNTBLANK(ARRAY($first_name, $last_name, $email))'],
60
+ contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
61
+ });
62
+
63
+ export const ARRAY_METADATA = defineBuiltinMetadata({
64
+ name: 'ARRAY',
65
+ category: 'logical',
66
+ signature: 'ARRAY(...values)',
67
+ description: 'Build an array from individual arguments.',
68
+ examples: ['ARRAY($city, $country)'],
69
+ contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
70
+ });
71
+
72
+ export const EVAL_METADATA = defineBuiltinMetadata({
73
+ name: 'EVAL',
74
+ category: 'control',
75
+ signature: 'EVAL(expression)',
76
+ description: 'Evaluate a dynamically built expression string in a restricted context.',
77
+ examples: ['EVAL("$" + dynamicFieldName)'],
78
+ contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
79
+ });
80
+
81
+ export const CHOICEVALUE_METADATA = defineBuiltinMetadata({
82
+ name: 'CHOICEVALUE',
83
+ category: 'choice',
84
+ signature: 'CHOICEVALUE(fieldValue)',
85
+ description: 'Return the selected choice value from a choice field.',
86
+ examples: [
87
+ 'CHOICEVALUE($city)',
88
+ 'IF(CHOICEVALUE($city) === "bogota", "Welcome to Bogota!", "Welcome!")',
89
+ ],
90
+ contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
91
+ });
92
+
93
+ export const CHOICELABEL_METADATA = defineBuiltinMetadata({
94
+ name: 'CHOICELABEL',
95
+ category: 'choice',
96
+ signature: 'CHOICELABEL(fieldValue)',
97
+ description: 'Return the selected choice label from a choice field.',
98
+ examples: ['CHOICELABEL($city)'],
99
+ contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
100
+ });
101
+
102
+ export const HASOTHER_METADATA = defineBuiltinMetadata({
103
+ name: 'HASOTHER',
104
+ category: 'choice',
105
+ signature: 'HASOTHER(fieldValue)',
106
+ description: 'Return true when the field has an "other" value.',
107
+ examples: ['HASOTHER($city)'],
108
+ contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
109
+ });
110
+
111
+ export const OTHER_METADATA = defineBuiltinMetadata({
112
+ name: 'OTHER',
113
+ category: 'choice',
114
+ signature: 'OTHER(fieldValue)',
115
+ description: 'Return the "other" label from a choice field.',
116
+ examples: ['OTHER($city)'],
117
+ contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
118
+ });
119
+
120
+ export const CHOICEVALUES_METADATA = defineBuiltinMetadata({
121
+ name: 'CHOICEVALUES',
122
+ category: 'choice',
123
+ signature: 'CHOICEVALUES(fieldValue)',
124
+ description: 'Return all selected values from a MultiChoiceField.',
125
+ examples: ['CHOICEVALUES($colors)', 'CHOICEVALUES($colors).includes("red")'],
126
+ contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
127
+ });
128
+
129
+ export const CHOICELABELS_METADATA = defineBuiltinMetadata({
130
+ name: 'CHOICELABELS',
131
+ category: 'choice',
132
+ signature: 'CHOICELABELS(fieldValue)',
133
+ description: 'Return all selected labels from a MultiChoiceField.',
134
+ examples: ['CHOICELABELS($colors)'],
135
+ contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
136
+ });
137
+
138
+ export const FORM_METADATA = defineBuiltinMetadata({
139
+ name: 'FORM',
140
+ category: 'schema',
141
+ signature: 'FORM()',
142
+ description: 'Access the form definition. Reserved for a future implementation.',
143
+ examples: ['FORM()'],
144
+ contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
145
+ });
146
+
147
+ export const DATANAMES_METADATA = defineBuiltinMetadata({
148
+ name: 'DATANAMES',
149
+ category: 'schema',
150
+ signature: "DATANAMES(type = 'any')",
151
+ description: 'Return form field data names, optionally filtered by field type.',
152
+ examples: ["DATANAMES('NumericField')"],
153
+ contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
154
+ });
155
+
156
+ export const ABS_METADATA = defineBuiltinMetadata({
157
+ name: 'ABS',
158
+ category: 'math',
159
+ signature: 'ABS(value)',
160
+ description: 'Return the absolute value of a number.',
161
+ examples: ['ABS($variance)'],
162
+ contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
163
+ });
164
+
165
+ export const CEILING_METADATA = defineBuiltinMetadata({
166
+ name: 'CEILING',
167
+ category: 'math',
168
+ signature: 'CEILING(value, factor)',
169
+ description: 'Round a number up to the nearest multiple.',
170
+ examples: ['CEILING($amount, 5)'],
171
+ contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
172
+ });
173
+
174
+ export const COS_METADATA = defineBuiltinMetadata({
175
+ name: 'COS',
176
+ category: 'math',
177
+ signature: 'COS(value)',
178
+ description: 'Return the cosine of a value in radians.',
179
+ examples: ['COS($angle_radians)'],
180
+ contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
181
+ });
182
+
183
+ export const SIN_METADATA = defineBuiltinMetadata({
184
+ name: 'SIN',
185
+ category: 'math',
186
+ signature: 'SIN(value)',
187
+ description: 'Return the sine of a value in radians.',
188
+ examples: ['SIN($angle_radians)'],
189
+ contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
190
+ });
191
+
192
+ export const ROUND_METADATA = defineBuiltinMetadata({
193
+ name: 'ROUND',
194
+ category: 'math',
195
+ signature: 'ROUND(value, places)',
196
+ description: 'Round a number to a fixed number of decimal places.',
197
+ examples: ['ROUND($total, 2)'],
198
+ contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
199
+ });
200
+
201
+ export const DATEADD_METADATA = defineBuiltinMetadata({
202
+ name: 'DATEADD',
203
+ category: 'date',
204
+ signature: 'DATEADD(date, days)',
205
+ description:
206
+ 'Add whole days to a YYYY-MM-DD date from 0001-01-01 through 9999-12-31. Returns null when an argument is blank.',
207
+ examples: ['DATEADD($start_date, 30)', 'DATEADD($due_date, -7)'],
208
+ contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
209
+ });
210
+
211
+ export const DAYS_METADATA = defineBuiltinMetadata({
212
+ name: 'DAYS',
213
+ category: 'date',
214
+ signature: 'DAYS(endDate, startDate)',
215
+ description:
216
+ 'Return the days between YYYY-MM-DD dates from 0001-01-01 through 9999-12-31. Returns null when an argument is blank.',
217
+ examples: ['DAYS($end_date, $start_date)'],
218
+ contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
219
+ });
220
+
221
+ export const UPPER_METADATA = defineBuiltinMetadata({
222
+ name: 'UPPER',
223
+ category: 'string',
224
+ signature: 'UPPER(value)',
225
+ description: 'Convert a string to uppercase.',
226
+ examples: ['UPPER($city_name)'],
227
+ contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
228
+ });
229
+
230
+ export const SETRESULT_METADATA = defineBuiltinMetadata({
231
+ name: 'SETRESULT',
232
+ category: 'control',
233
+ signature: 'SETRESULT(value)',
234
+ description: 'Set the final return value for multiline calculations.',
235
+ examples: ['SETRESULT($price * $quantity)'],
236
+ contexts: [BUILTIN_CONTEXTS.CALCULATION],
237
+ });
238
+
239
+ export const ALERT_METADATA = defineBuiltinMetadata({
240
+ name: 'ALERT',
241
+ category: 'event',
242
+ signature: "ALERT(title, message = '')",
243
+ description: 'Display an alert from a form event handler.',
244
+ examples: ["ALERT('Saved', 'The record was saved successfully.')"],
245
+ contexts: [BUILTIN_CONTEXTS.EVENT],
246
+ });
247
+
248
+ export const SETVALUE_METADATA = defineBuiltinMetadata({
249
+ name: 'SETVALUE',
250
+ category: 'event',
251
+ signature: 'SETVALUE(fieldDataName, valueToSet)',
252
+ description: 'Set a field value from a form event handler.',
253
+ examples: ["SETVALUE('field_dataname', 'value_to_set')"],
254
+ contexts: [BUILTIN_CONTEXTS.EVENT],
255
+ });
256
+
257
+ export const ON_METADATA = defineBuiltinMetadata({
258
+ name: 'ON',
259
+ category: 'event',
260
+ signature: 'ON(eventType, fieldKeyOrCallback, callback)',
261
+ description: 'Register an event handler within form event code.',
262
+ examples: ["ON('change', 'city', function(event) { ALERT('City changed!'); })"],
263
+ contexts: [BUILTIN_CONTEXTS.EVENT],
264
+ });
265
+
266
+ export const OFF_METADATA = defineBuiltinMetadata({
267
+ name: 'OFF',
268
+ category: 'event',
269
+ signature: 'OFF(eventType, fieldKeyOrCallback, callback)',
270
+ description: 'Remove event handlers within form event code.',
271
+ examples: ["OFF('change', 'city', specificCallback)"],
272
+ contexts: [BUILTIN_CONTEXTS.EVENT],
273
+ });
274
+
275
+ export const BUILTIN_DEFINITIONS = Object.freeze([
276
+ IF_METADATA,
277
+ AND_METADATA,
278
+ OR_METADATA,
279
+ COUNT_METADATA,
280
+ COUNTA_METADATA,
281
+ COUNTBLANK_METADATA,
282
+ ARRAY_METADATA,
283
+ EVAL_METADATA,
284
+ CHOICEVALUE_METADATA,
285
+ CHOICELABEL_METADATA,
286
+ HASOTHER_METADATA,
287
+ OTHER_METADATA,
288
+ CHOICEVALUES_METADATA,
289
+ CHOICELABELS_METADATA,
290
+ FORM_METADATA,
291
+ DATANAMES_METADATA,
292
+ ABS_METADATA,
293
+ CEILING_METADATA,
294
+ COS_METADATA,
295
+ SIN_METADATA,
296
+ ROUND_METADATA,
297
+ DATEADD_METADATA,
298
+ DAYS_METADATA,
299
+ UPPER_METADATA,
300
+ SETRESULT_METADATA,
301
+ ALERT_METADATA,
302
+ SETVALUE_METADATA,
303
+ ON_METADATA,
304
+ OFF_METADATA,
305
+ ]);
306
+
307
+ function filterDefinitionsByContext(context) {
308
+ return Object.freeze(
309
+ BUILTIN_DEFINITIONS.filter((definition) => definition.contexts.includes(context))
310
+ );
311
+ }
312
+
313
+ export const CALCULATION_BUILTIN_DEFINITIONS = filterDefinitionsByContext(
314
+ BUILTIN_CONTEXTS.CALCULATION
315
+ );
316
+ export const EVENT_BUILTIN_DEFINITIONS = filterDefinitionsByContext(BUILTIN_CONTEXTS.EVENT);
317
+
318
+ export const BUILTIN_DEFINITION_BY_NAME = new Map(
319
+ BUILTIN_DEFINITIONS.map((definition) => [definition.name, definition])
320
+ );
@@ -1,26 +1,11 @@
1
- import { BUILTIN_CONTEXTS, defineBuiltinMetadata } from '../builtin-metadata.js';
1
+ export { CHOICELABEL_METADATA } from '../builtin-definitions.js';
2
2
 
3
3
  /**
4
4
  * @builtin CHOICELABEL
5
- * @description Retrieves the currently selected choice field label, preserving the type (if the label is a number preserves the number type, otherwise string). If no selection it returns null.
5
+ * Name, signature, description, examples, and contexts: see builtin-definitions.js.
6
6
  * @param {Object} choiceField - The choice field object with choice and other arrays
7
7
  * @returns {*} The selected choice label with preserved type, or null if no selection
8
- * @example
9
- * // Get the selected city label
10
- * CHOICELABEL($city)
11
- * @example
12
- * // Use in display logic
13
- * "You selected: " + CHOICELABEL($city)
14
8
  */
15
- export const CHOICELABEL_METADATA = defineBuiltinMetadata({
16
- name: 'CHOICELABEL',
17
- category: 'choice',
18
- signature: 'CHOICELABEL(fieldValue)',
19
- description: 'Return the selected choice label from a choice field.',
20
- examples: ['CHOICELABEL($city)'],
21
- contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
22
- });
23
-
24
9
  export const CHOICELABEL = (choiceField) => {
25
10
  // Handle null/undefined input
26
11
  if (!choiceField || typeof choiceField !== 'object') {
@@ -1,26 +1,11 @@
1
- import { BUILTIN_CONTEXTS, defineBuiltinMetadata } from '../builtin-metadata.js';
1
+ export { CHOICELABELS_METADATA } from '../builtin-definitions.js';
2
2
 
3
3
  /**
4
4
  * @builtin CHOICELABELS
5
- * @description Retrieves an array of all selected choice field labels from a MultiChoiceField, preserving the type of each label. Returns an empty array if no selections.
5
+ * Name, signature, description, examples, and contexts: see builtin-definitions.js.
6
6
  * @param {Object} multiChoiceField - The multi choice field object with choices and other arrays
7
7
  * @returns {Array} Array of selected choice labels with preserved types
8
- * @example
9
- * // Get all selected color labels
10
- * CHOICELABELS($colors)
11
- * @example
12
- * // Display selected colors
13
- * "Selected colors: " + CHOICELABELS($colors).join(", ")
14
8
  */
15
- export const CHOICELABELS_METADATA = defineBuiltinMetadata({
16
- name: 'CHOICELABELS',
17
- category: 'choice',
18
- signature: 'CHOICELABELS(fieldValue)',
19
- description: 'Return all selected labels from a MultiChoiceField.',
20
- examples: ['CHOICELABELS($colors)'],
21
- contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
22
- });
23
-
24
9
  export const CHOICELABELS = (multiChoiceField) => {
25
10
  // Handle null/undefined input
26
11
  if (!multiChoiceField || typeof multiChoiceField !== 'object') {