form0-core 0.1.2 → 0.1.4

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 (33) hide show
  1. package/package.json +2 -2
  2. package/src/builtins/builtin-metadata.js +44 -0
  3. package/src/builtins/choice/choicelabel.js +11 -0
  4. package/src/builtins/choice/choicelabels.js +11 -0
  5. package/src/builtins/choice/choicevalue.js +11 -0
  6. package/src/builtins/choice/choicevalues.js +11 -0
  7. package/src/builtins/choice/hasother.js +11 -0
  8. package/src/builtins/choice/other.js +11 -0
  9. package/src/builtins/control/eval.js +10 -0
  10. package/src/builtins/control/setresult.js +11 -0
  11. package/src/builtins/event/control/off.js +10 -0
  12. package/src/builtins/event/control/on.js +10 -0
  13. package/src/builtins/event/field/setvalue.js +10 -0
  14. package/src/builtins/event/ui/alert.js +10 -0
  15. package/src/builtins/logical/and.js +11 -0
  16. package/src/builtins/logical/array.js +11 -0
  17. package/src/builtins/logical/count.js +11 -0
  18. package/src/builtins/logical/counta.js +11 -0
  19. package/src/builtins/logical/countblank.js +12 -3
  20. package/src/builtins/logical/if.js +11 -0
  21. package/src/builtins/logical/or.js +11 -0
  22. package/src/builtins/math/abs.js +11 -0
  23. package/src/builtins/math/ceiling.js +11 -0
  24. package/src/builtins/math/cos.js +12 -1
  25. package/src/builtins/math/round.js +13 -2
  26. package/src/builtins/math/sin.js +12 -1
  27. package/src/builtins/registry.js +130 -92
  28. package/src/builtins/schema/datanames.js +10 -0
  29. package/src/builtins/schema/form.js +11 -0
  30. package/src/builtins/string/upper.js +12 -1
  31. package/src/engine/context-resolver.js +202 -56
  32. package/src/index.js +6 -0
  33. package/src/utilities/calculation-editor.js +534 -0
@@ -1,99 +1,137 @@
1
- // Import all builtins
2
- import { IF } from './logical/if.js';
3
- import { AND } from './logical/and.js';
4
- import { OR } from './logical/or.js';
5
- import { COUNT } from './logical/count.js';
6
- import { COUNTA } from './logical/counta.js';
7
- import { COUNTBLANK } from './logical/countblank.js';
8
- import { ARRAY } from './logical/array.js';
9
- import { SETRESULT, __consumeResult } from './control/setresult.js';
10
- import { EVAL } from './control/eval.js';
11
- import { CHOICEVALUE } from './choice/choicevalue.js';
12
- import { CHOICELABEL } from './choice/choicelabel.js';
13
- import { HASOTHER } from './choice/hasother.js';
14
- import { OTHER } from './choice/other.js';
15
- import { CHOICEVALUES } from './choice/choicevalues.js';
16
- import { CHOICELABELS } from './choice/choicelabels.js';
17
- import { FORM } from './schema/form.js';
18
- import { DATANAMES } from './schema/datanames.js';
19
- import { ABS } from './math/abs.js';
20
- import { CEILING } from './math/ceiling.js';
21
- import { COS } from './math/cos.js';
22
- import { SIN } from './math/sin.js';
23
- import { ROUND } from './math/round.js';
24
- import { UPPER } from './string/upper.js';
1
+ import { cloneBuiltinMetadata, BUILTIN_CONTEXTS } from './builtin-metadata.js';
25
2
 
26
- // Import event builtins
27
- import { ALERT } from './event/ui/alert.js';
28
- import { SETVALUE } from './event/field/setvalue.js';
29
- import { ON } from './event/control/on.js';
30
- import { OFF } from './event/control/off.js';
3
+ import { IF, IF_METADATA } from './logical/if.js';
4
+ import { AND, AND_METADATA } from './logical/and.js';
5
+ import { OR, OR_METADATA } from './logical/or.js';
6
+ import { COUNT, COUNT_METADATA } from './logical/count.js';
7
+ import { COUNTA, COUNTA_METADATA } from './logical/counta.js';
8
+ import { COUNTBLANK, COUNTBLANK_METADATA } from './logical/countblank.js';
9
+ import { ARRAY, ARRAY_METADATA } from './logical/array.js';
10
+ import { SETRESULT, SETRESULT_METADATA, __consumeResult } from './control/setresult.js';
11
+ import { EVAL, EVAL_METADATA } from './control/eval.js';
12
+ import { CHOICEVALUE, CHOICEVALUE_METADATA } from './choice/choicevalue.js';
13
+ import { CHOICELABEL, CHOICELABEL_METADATA } from './choice/choicelabel.js';
14
+ import { HASOTHER, HASOTHER_METADATA } from './choice/hasother.js';
15
+ import { OTHER, OTHER_METADATA } from './choice/other.js';
16
+ import { CHOICEVALUES, CHOICEVALUES_METADATA } from './choice/choicevalues.js';
17
+ import { CHOICELABELS, CHOICELABELS_METADATA } from './choice/choicelabels.js';
18
+ import { FORM, FORM_METADATA } from './schema/form.js';
19
+ import { DATANAMES, DATANAMES_METADATA } from './schema/datanames.js';
20
+ import { ABS, ABS_METADATA } from './math/abs.js';
21
+ import { CEILING, CEILING_METADATA } from './math/ceiling.js';
22
+ import { COS, COS_METADATA } from './math/cos.js';
23
+ import { SIN, SIN_METADATA } from './math/sin.js';
24
+ import { ROUND, ROUND_METADATA } from './math/round.js';
25
+ import { UPPER, UPPER_METADATA } from './string/upper.js';
26
+
27
+ import { ALERT, ALERT_METADATA } from './event/ui/alert.js';
28
+ import { SETVALUE, SETVALUE_METADATA } from './event/field/setvalue.js';
29
+ import { ON, ON_METADATA } from './event/control/on.js';
30
+ import { OFF, OFF_METADATA } from './event/control/off.js';
31
31
  import { __consumeEventOperations } from './event/event-operations-collector.js';
32
32
 
33
- // Export individual builtins
34
- export { IF } from './logical/if.js';
35
- export { AND } from './logical/and.js';
36
- export { OR } from './logical/or.js';
37
- export { COUNT } from './logical/count.js';
38
- export { COUNTA } from './logical/counta.js';
39
- export { COUNTBLANK } from './logical/countblank.js';
40
- export { ARRAY } from './logical/array.js';
41
- export { SETRESULT, __consumeResult } from './control/setresult.js';
42
- export { EVAL, __setEvalContext, __clearEvalContext } from './control/eval.js';
43
- export { CHOICEVALUE } from './choice/choicevalue.js';
44
- export { CHOICELABEL } from './choice/choicelabel.js';
45
- export { HASOTHER } from './choice/hasother.js';
46
- export { OTHER } from './choice/other.js';
47
- export { CHOICEVALUES } from './choice/choicevalues.js';
48
- export { CHOICELABELS } from './choice/choicelabels.js';
49
- export { FORM } from './schema/form.js';
50
- export { DATANAMES, __setDataNamesContext, __clearDataNamesContext } from './schema/datanames.js';
51
- export { ABS } from './math/abs.js';
52
- export { CEILING } from './math/ceiling.js';
53
- export { COS } from './math/cos.js';
54
- export { SIN } from './math/sin.js';
55
- export { ROUND } from './math/round.js';
56
- export { UPPER } from './string/upper.js';
33
+ export { IF, IF_METADATA } from './logical/if.js';
34
+ export { AND, AND_METADATA } from './logical/and.js';
35
+ export { OR, OR_METADATA } from './logical/or.js';
36
+ export { COUNT, COUNT_METADATA } from './logical/count.js';
37
+ export { COUNTA, COUNTA_METADATA } from './logical/counta.js';
38
+ export { COUNTBLANK, COUNTBLANK_METADATA } from './logical/countblank.js';
39
+ export { ARRAY, ARRAY_METADATA } from './logical/array.js';
40
+ export { SETRESULT, SETRESULT_METADATA, __consumeResult } from './control/setresult.js';
41
+ export { EVAL, EVAL_METADATA, __setEvalContext, __clearEvalContext } from './control/eval.js';
42
+ export { CHOICEVALUE, CHOICEVALUE_METADATA } from './choice/choicevalue.js';
43
+ export { CHOICELABEL, CHOICELABEL_METADATA } from './choice/choicelabel.js';
44
+ export { HASOTHER, HASOTHER_METADATA } from './choice/hasother.js';
45
+ export { OTHER, OTHER_METADATA } from './choice/other.js';
46
+ export { CHOICEVALUES, CHOICEVALUES_METADATA } from './choice/choicevalues.js';
47
+ export { CHOICELABELS, CHOICELABELS_METADATA } from './choice/choicelabels.js';
48
+ export { FORM, FORM_METADATA } from './schema/form.js';
49
+ export {
50
+ DATANAMES,
51
+ DATANAMES_METADATA,
52
+ __setDataNamesContext,
53
+ __clearDataNamesContext,
54
+ } from './schema/datanames.js';
55
+ export { ABS, ABS_METADATA } from './math/abs.js';
56
+ export { CEILING, CEILING_METADATA } from './math/ceiling.js';
57
+ export { COS, COS_METADATA } from './math/cos.js';
58
+ export { SIN, SIN_METADATA } from './math/sin.js';
59
+ export { ROUND, ROUND_METADATA } from './math/round.js';
60
+ export { UPPER, UPPER_METADATA } from './string/upper.js';
57
61
 
58
- // Export event builtins
59
- export { ALERT } from './event/ui/alert.js';
60
- export { SETVALUE } from './event/field/setvalue.js';
61
- export { ON } from './event/control/on.js';
62
- export { OFF } from './event/control/off.js';
62
+ export { ALERT, ALERT_METADATA } from './event/ui/alert.js';
63
+ export { SETVALUE, SETVALUE_METADATA } from './event/field/setvalue.js';
64
+ export { ON, ON_METADATA } from './event/control/on.js';
65
+ export { OFF, OFF_METADATA } from './event/control/off.js';
63
66
  export { __consumeEventOperations } from './event/event-operations-collector.js';
64
67
 
65
- // Main builtins object for backward compatibility
66
- export const builtins = {
67
- IF,
68
- AND,
69
- OR,
70
- COUNT,
71
- COUNTA,
72
- COUNTBLANK,
73
- ARRAY,
74
- SETRESULT,
75
- EVAL,
76
- CHOICEVALUE,
77
- CHOICELABEL,
78
- HASOTHER,
79
- OTHER,
80
- CHOICEVALUES,
81
- CHOICELABELS,
82
- FORM, // Available in both calculations and events
83
- DATANAMES,
84
- ABS,
85
- CEILING,
86
- COS,
87
- SIN,
88
- ROUND,
89
- UPPER,
90
- };
68
+ const CALCULATION_AND_EVENT_ENTRIES = Object.freeze([
69
+ { implementation: IF, definition: IF_METADATA },
70
+ { implementation: AND, definition: AND_METADATA },
71
+ { implementation: OR, definition: OR_METADATA },
72
+ { implementation: COUNT, definition: COUNT_METADATA },
73
+ { implementation: COUNTA, definition: COUNTA_METADATA },
74
+ { implementation: COUNTBLANK, definition: COUNTBLANK_METADATA },
75
+ { implementation: ARRAY, definition: ARRAY_METADATA },
76
+ { implementation: SETRESULT, definition: SETRESULT_METADATA },
77
+ { implementation: EVAL, definition: EVAL_METADATA },
78
+ { implementation: CHOICEVALUE, definition: CHOICEVALUE_METADATA },
79
+ { implementation: CHOICELABEL, definition: CHOICELABEL_METADATA },
80
+ { implementation: HASOTHER, definition: HASOTHER_METADATA },
81
+ { implementation: OTHER, definition: OTHER_METADATA },
82
+ { implementation: CHOICEVALUES, definition: CHOICEVALUES_METADATA },
83
+ { implementation: CHOICELABELS, definition: CHOICELABELS_METADATA },
84
+ { implementation: FORM, definition: FORM_METADATA },
85
+ { implementation: DATANAMES, definition: DATANAMES_METADATA },
86
+ { implementation: ABS, definition: ABS_METADATA },
87
+ { implementation: CEILING, definition: CEILING_METADATA },
88
+ { implementation: COS, definition: COS_METADATA },
89
+ { implementation: SIN, definition: SIN_METADATA },
90
+ { implementation: ROUND, definition: ROUND_METADATA },
91
+ { implementation: UPPER, definition: UPPER_METADATA },
92
+ ]);
93
+
94
+ const EVENT_ONLY_ENTRIES = Object.freeze([
95
+ { implementation: ALERT, definition: ALERT_METADATA },
96
+ { implementation: SETVALUE, definition: SETVALUE_METADATA },
97
+ { implementation: ON, definition: ON_METADATA },
98
+ { implementation: OFF, definition: OFF_METADATA },
99
+ ]);
100
+
101
+ function buildBuiltinObject(entries) {
102
+ return Object.freeze(
103
+ Object.fromEntries(
104
+ entries.map(({ implementation, definition }) => [definition.name, implementation])
105
+ )
106
+ );
107
+ }
108
+
109
+ function filterDefinitionsByContext(definitions, context) {
110
+ return Object.freeze(definitions.filter((definition) => definition.contexts.includes(context)));
111
+ }
112
+
113
+ export const builtins = buildBuiltinObject(CALCULATION_AND_EVENT_ENTRIES);
114
+
115
+ export const eventBuiltins = buildBuiltinObject(EVENT_ONLY_ENTRIES);
116
+
117
+ export const BUILTIN_DEFINITIONS = Object.freeze(
118
+ [...CALCULATION_AND_EVENT_ENTRIES, ...EVENT_ONLY_ENTRIES].map(({ definition }) => definition)
119
+ );
120
+
121
+ export const CALCULATION_BUILTIN_DEFINITIONS = filterDefinitionsByContext(
122
+ BUILTIN_DEFINITIONS,
123
+ BUILTIN_CONTEXTS.CALCULATION
124
+ );
125
+
126
+ export const EVENT_BUILTIN_DEFINITIONS = filterDefinitionsByContext(
127
+ BUILTIN_DEFINITIONS,
128
+ BUILTIN_CONTEXTS.EVENT
129
+ );
130
+
131
+ export const BUILTIN_DEFINITION_BY_NAME = new Map(
132
+ BUILTIN_DEFINITIONS.map((definition) => [definition.name, definition])
133
+ );
91
134
 
92
- // Event-specific builtins (only available in event context)
93
- export const eventBuiltins = {
94
- ALERT,
95
- SETVALUE,
96
- ON,
97
- OFF,
98
- // Future: CONFIRM, NOTIFY, etc.
99
- };
135
+ export function getBuiltinDefinitions() {
136
+ return BUILTIN_DEFINITIONS.map(cloneBuiltinMetadata);
137
+ }
@@ -1,3 +1,5 @@
1
+ import { BUILTIN_CONTEXTS, defineBuiltinMetadata } from '../builtin-metadata.js';
2
+
1
3
  /**
2
4
  * @builtin DATANAMES
3
5
  * @description Returns the data names of form fields, optionally filtered by field type
@@ -13,6 +15,14 @@
13
15
  * // Get only TextField data names
14
16
  * DATANAMES('TextField')
15
17
  */
18
+ export const DATANAMES_METADATA = defineBuiltinMetadata({
19
+ name: 'DATANAMES',
20
+ category: 'schema',
21
+ signature: "DATANAMES(type = 'any')",
22
+ description: 'Return form field data names, optionally filtered by field type.',
23
+ examples: ["DATANAMES('NumericField')"],
24
+ contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
25
+ });
16
26
 
17
27
  // Global schema context for DATANAMES() - set during expression evaluation
18
28
  let _dataNamesSchema = null;
@@ -1,3 +1,5 @@
1
+ import { BUILTIN_CONTEXTS, defineBuiltinMetadata } from '../builtin-metadata.js';
2
+
1
3
  /**
2
4
  * @builtin FORM
3
5
  * @description Access to the nested JSON form definition (future implementation)
@@ -12,6 +14,15 @@
12
14
  * // Check if form has RepeatableSections
13
15
  * FORM().elements.some(el => el.type === 'RepeatableSection')
14
16
  */
17
+ export const FORM_METADATA = defineBuiltinMetadata({
18
+ name: 'FORM',
19
+ category: 'schema',
20
+ signature: 'FORM()',
21
+ description: 'Access the form definition. Reserved for a future implementation.',
22
+ examples: ['FORM()'],
23
+ contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
24
+ });
25
+
15
26
  export function FORM() {
16
27
  // Placeholder for future implementation
17
28
  // Will provide access to nested JSON form definition
@@ -1,3 +1,5 @@
1
+ import { BUILTIN_CONTEXTS, defineBuiltinMetadata } from '../builtin-metadata.js';
2
+
1
3
  /**
2
4
  * @builtin UPPER
3
5
  * @description Returns the uppercase version of a string
@@ -13,9 +15,18 @@
13
15
  * // Returns "ABC123"
14
16
  * UPPER("abc123")
15
17
  */
18
+ export const UPPER_METADATA = defineBuiltinMetadata({
19
+ name: 'UPPER',
20
+ category: 'string',
21
+ signature: 'UPPER(value)',
22
+ description: 'Convert a string to uppercase.',
23
+ examples: ['UPPER($city_name)'],
24
+ contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
25
+ });
26
+
16
27
  export const UPPER = (value) => {
17
28
  if (value == null) {
18
29
  return '';
19
30
  }
20
31
  return String(value).toUpperCase();
21
- };
32
+ };
@@ -66,25 +66,48 @@ export class ContextResolver {
66
66
  }
67
67
 
68
68
  /**
69
- * Determine if a field is accessible from the current execution context
69
+ * Resolve field access with structured metadata for editor integrations.
70
70
  * @param {Object} executionContext - Current execution context
71
71
  * @param {string} fieldName - Field data_name being accessed
72
- * @returns {string} 'accessible', 'restricted', or 'not_found'
72
+ * @param {Object | null} [providedFieldInfo]
73
+ * @returns {{
74
+ * level: 'accessible' | 'restricted' | 'not_found',
75
+ * code: 'main_form' | 'same_repeatable_section' | 'ancestor_repeatable_context' | 'different_repeatable_section' | null,
76
+ * message: string | null,
77
+ * suggestion: string | null,
78
+ * fieldInfo: Object | null,
79
+ * }}
73
80
  */
74
- resolveFieldAccess(executionContext, fieldName) {
75
- const fieldInfo = this.fieldOwnership.get(fieldName);
81
+ resolveFieldAccessInfo(executionContext, fieldName, providedFieldInfo = null) {
82
+ const fieldInfo = providedFieldInfo || this.fieldOwnership.get(fieldName) || null;
76
83
 
77
84
  if (!fieldInfo) {
78
- return 'not_found';
85
+ return {
86
+ level: 'not_found',
87
+ code: null,
88
+ message: `Field '${fieldName}' does not exist in the form schema`,
89
+ suggestion: `Check that field '${fieldName}' exists in the form schema`,
90
+ fieldInfo: null,
91
+ };
79
92
  }
80
93
 
81
94
  // Main form fields (no parent RepeatableSection) are always accessible
82
95
  if (fieldInfo.parentPath.length === 0) {
83
- return 'accessible';
96
+ return this.buildAccessInfo('accessible', 'main_form', fieldName, fieldInfo, executionContext);
84
97
  }
85
98
 
86
99
  // RepeatableSection field access depends on execution context
87
- return this.checkRepeatableSectionAccess(executionContext, fieldInfo);
100
+ return this.checkRepeatableSectionAccessInfo(executionContext, fieldName, fieldInfo);
101
+ }
102
+
103
+ /**
104
+ * Determine if a field is accessible from the current execution context
105
+ * @param {Object} executionContext - Current execution context
106
+ * @param {string} fieldName - Field data_name being accessed
107
+ * @returns {string} 'accessible', 'restricted', or 'not_found'
108
+ */
109
+ resolveFieldAccess(executionContext, fieldName) {
110
+ return this.resolveFieldAccessInfo(executionContext, fieldName).level;
88
111
  }
89
112
 
90
113
  /**
@@ -94,22 +117,49 @@ export class ContextResolver {
94
117
  * @returns {string} 'accessible' or 'restricted'
95
118
  */
96
119
  checkRepeatableSectionAccess(executionContext, fieldInfo) {
120
+ return this.checkRepeatableSectionAccessInfo(
121
+ executionContext,
122
+ fieldInfo.field?.data_name || fieldInfo.preferredKey,
123
+ fieldInfo
124
+ ).level;
125
+ }
126
+
127
+ /**
128
+ * Check RepeatableSection field access and return structured access metadata.
129
+ * @param {Object} executionContext - Current execution context
130
+ * @param {string} fieldName - Field data_name being accessed
131
+ * @param {Object} fieldInfo - Field ownership information
132
+ * @returns {{
133
+ * level: 'accessible' | 'restricted' | 'not_found',
134
+ * code: 'main_form' | 'same_repeatable_section' | 'ancestor_repeatable_context' | 'different_repeatable_section' | null,
135
+ * message: string | null,
136
+ * suggestion: string | null,
137
+ * fieldInfo: Object | null,
138
+ * }}
139
+ */
140
+ checkRepeatableSectionAccessInfo(executionContext, fieldName, fieldInfo) {
97
141
  const { type: contextType, eventType, fieldName: contextFieldName } = executionContext;
98
142
 
99
143
  if (contextType === 'calculation') {
100
144
  // CalculatedField can only access fields in same RepeatableSection context
101
145
  const calculationFieldInfo = this.fieldOwnership.get(contextFieldName);
102
146
  if (!calculationFieldInfo) {
103
- return 'restricted'; // Unknown calculation field
147
+ return this.buildAccessInfo(
148
+ 'restricted',
149
+ 'different_repeatable_section',
150
+ fieldName,
151
+ fieldInfo,
152
+ executionContext
153
+ );
104
154
  }
105
155
 
106
- // Check if both fields have the same RepeatableSection ancestry
107
- return this.haveSameRepeatableSectionContext(
156
+ return this.resolveRepeatableSectionRelationship(
108
157
  calculationFieldInfo.parentPath,
109
- fieldInfo.parentPath
110
- )
111
- ? 'accessible'
112
- : 'restricted';
158
+ fieldInfo.parentPath,
159
+ fieldName,
160
+ fieldInfo,
161
+ executionContext
162
+ );
113
163
  }
114
164
 
115
165
  if (contextType === 'event') {
@@ -118,38 +168,75 @@ export class ContextResolver {
118
168
  switch (eventInfo.scope) {
119
169
  case 'main':
120
170
  // Record events can't access RepeatableSection fields
121
- return 'restricted';
171
+ return this.buildAccessInfo(
172
+ 'restricted',
173
+ 'different_repeatable_section',
174
+ fieldName,
175
+ fieldInfo,
176
+ executionContext
177
+ );
122
178
 
123
179
  case 'contextual':
124
180
  // Field events can access fields in same RepeatableSection context
125
181
  if (!contextFieldName) {
126
- return 'restricted'; // No field context
182
+ return this.buildAccessInfo(
183
+ 'restricted',
184
+ 'different_repeatable_section',
185
+ fieldName,
186
+ fieldInfo,
187
+ executionContext
188
+ );
127
189
  }
128
190
 
129
191
  const triggerFieldInfo = this.fieldOwnership.get(contextFieldName);
130
192
  if (!triggerFieldInfo) {
131
- return 'restricted'; // Unknown trigger field
193
+ return this.buildAccessInfo(
194
+ 'restricted',
195
+ 'different_repeatable_section',
196
+ fieldName,
197
+ fieldInfo,
198
+ executionContext
199
+ );
132
200
  }
133
201
 
134
- return this.haveSameRepeatableSectionContext(
202
+ return this.resolveRepeatableSectionRelationship(
135
203
  triggerFieldInfo.parentPath,
136
- fieldInfo.parentPath
137
- )
138
- ? 'accessible'
139
- : 'restricted';
204
+ fieldInfo.parentPath,
205
+ fieldName,
206
+ fieldInfo,
207
+ executionContext
208
+ );
140
209
 
141
210
  case 'repeatable':
142
211
  // RepeatableSection events - future implementation
143
212
  // For now, assume accessible (will be refined when implemented)
144
- return 'accessible';
213
+ return this.buildAccessInfo(
214
+ 'accessible',
215
+ 'same_repeatable_section',
216
+ fieldName,
217
+ fieldInfo,
218
+ executionContext
219
+ );
145
220
 
146
221
  default:
147
- return 'restricted';
222
+ return this.buildAccessInfo(
223
+ 'restricted',
224
+ 'different_repeatable_section',
225
+ fieldName,
226
+ fieldInfo,
227
+ executionContext
228
+ );
148
229
  }
149
230
  }
150
231
 
151
232
  // Unknown context type
152
- return 'restricted';
233
+ return this.buildAccessInfo(
234
+ 'restricted',
235
+ 'different_repeatable_section',
236
+ fieldName,
237
+ fieldInfo,
238
+ executionContext
239
+ );
153
240
  }
154
241
 
155
242
  /**
@@ -159,25 +246,80 @@ export class ContextResolver {
159
246
  * @returns {boolean} True if they share the same RepeatableSection context
160
247
  */
161
248
  haveSameRepeatableSectionContext(parentPath1, parentPath2) {
162
- const contextPath = parentPath1;
163
- const targetPath = parentPath2;
249
+ return this.resolveRepeatableSectionAccessCode(parentPath1, parentPath2) !== 'different_repeatable_section';
250
+ }
251
+
252
+ resolveRepeatableSectionRelationship(
253
+ contextPath,
254
+ targetPath,
255
+ fieldName,
256
+ fieldInfo,
257
+ executionContext
258
+ ) {
259
+ const code = this.resolveRepeatableSectionAccessCode(contextPath, targetPath);
260
+ const level = code === 'different_repeatable_section' ? 'restricted' : 'accessible';
261
+
262
+ return this.buildAccessInfo(level, code, fieldName, fieldInfo, executionContext);
263
+ }
164
264
 
165
- // Main form calculations can only access other main form fields
166
- if (contextPath.length === 0) {
167
- return targetPath.length === 0;
265
+ resolveRepeatableSectionAccessCode(contextPath, targetPath) {
266
+ if (targetPath.length === 0) {
267
+ return 'main_form';
168
268
  }
169
269
 
170
- // Child context can access its own fields
171
270
  if (ContextResolver.pathsEqual(contextPath, targetPath)) {
172
- return true;
271
+ return contextPath.length === 0 ? 'main_form' : 'same_repeatable_section';
173
272
  }
174
273
 
175
- // Child context can access ancestors (including main form)
176
274
  if (targetPath.length < contextPath.length && ContextResolver.isPrefix(targetPath, contextPath)) {
177
- return true;
275
+ return targetPath.length === 0 ? 'main_form' : 'ancestor_repeatable_context';
276
+ }
277
+
278
+ return 'different_repeatable_section';
279
+ }
280
+
281
+ buildAccessInfo(level, code, fieldName, fieldInfo, executionContext) {
282
+ if (level === 'accessible') {
283
+ return {
284
+ level,
285
+ code,
286
+ message: null,
287
+ suggestion: null,
288
+ fieldInfo,
289
+ };
290
+ }
291
+
292
+ return {
293
+ level,
294
+ code,
295
+ message: `Field '${fieldName}' is not accessible from current context`,
296
+ suggestion: this.buildRestrictedAccessSuggestion(executionContext, fieldName, fieldInfo, code),
297
+ fieldInfo,
298
+ };
299
+ }
300
+
301
+ buildRestrictedAccessSuggestion(executionContext, fieldName, fieldInfo, code) {
302
+ const { type: contextType, eventType } = executionContext;
303
+
304
+ if (contextType === 'calculation') {
305
+ if (code === 'different_repeatable_section') {
306
+ return `CalculatedField can only access fields in the same RepeatableSection or ancestor contexts. Field '${fieldName}' is in RepeatableSection: ${fieldInfo.parentPath.join(' -> ')}`;
307
+ }
178
308
  }
179
309
 
180
- return false;
310
+ if (contextType === 'event') {
311
+ const eventInfo = getEventInfo(eventType);
312
+
313
+ if (eventInfo.scope === 'main') {
314
+ return `Record-level events (like '${eventType}') can only access main form fields, not RepeatableSection fields`;
315
+ }
316
+
317
+ if (eventInfo.scope === 'contextual') {
318
+ return `Field events can only access fields in the same RepeatableSection or ancestor contexts as the triggering field`;
319
+ }
320
+ }
321
+
322
+ return 'Check the field access rules for your current context';
181
323
  }
182
324
 
183
325
  static isPrefix(prefix, fullPath) {
@@ -221,19 +363,25 @@ export class ContextResolver {
221
363
  * @returns {Object} Warning object with structured information
222
364
  */
223
365
  generateAccessWarning(executionContext, fieldName, reason) {
224
- const fieldInfo = this.fieldOwnership.get(fieldName);
225
- const suggestion = this.generateAccessSuggestion(executionContext, fieldName, fieldInfo);
366
+ const accessInfo =
367
+ reason === 'not_found'
368
+ ? this.resolveFieldAccessInfo(executionContext, fieldName)
369
+ : this.resolveFieldAccessInfo(executionContext, fieldName);
370
+ const fieldInfo = accessInfo.fieldInfo;
226
371
 
227
372
  return {
228
373
  type: 'FIELD_ACCESS_WARNING',
229
374
  fieldName,
230
375
  executionContext,
231
376
  reason,
377
+ accessCode: accessInfo.code,
232
378
  message:
233
- reason === 'not_found'
379
+ accessInfo.message ||
380
+ (reason === 'not_found'
234
381
  ? `Field '${fieldName}' does not exist in the form schema`
235
- : `Field '${fieldName}' is not accessible from current context`,
236
- suggestion,
382
+ : `Field '${fieldName}' is not accessible from current context`),
383
+ suggestion:
384
+ accessInfo.suggestion || this.generateAccessSuggestion(executionContext, fieldName, fieldInfo),
237
385
  fieldContext: fieldInfo
238
386
  ? {
239
387
  parentPath: fieldInfo.parentPath,
@@ -252,28 +400,26 @@ export class ContextResolver {
252
400
  * @returns {string} Helpful suggestion message
253
401
  */
254
402
  generateAccessSuggestion(executionContext, fieldName, fieldInfo) {
255
- if (!fieldInfo) {
256
- return `Check that field '${fieldName}' exists in the form schema`;
403
+ const accessInfo = this.resolveFieldAccessInfo(executionContext, fieldName, fieldInfo);
404
+
405
+ if (accessInfo.suggestion) {
406
+ return accessInfo.suggestion;
257
407
  }
258
408
 
259
- const { type: contextType, eventType, fieldName: contextFieldName } = executionContext;
409
+ if (accessInfo.level === 'not_found') {
410
+ return `Check that field '${fieldName}' exists in the form schema`;
411
+ }
260
412
 
261
- if (contextType === 'calculation') {
262
- if (fieldInfo.parentPath.length === 0) {
263
- return 'Main form fields are always accessible from calculations';
264
- } else {
265
- return `CalculatedField can only access fields in the same RepeatableSection. Field '${fieldName}' is in RepeatableSection: ${fieldInfo.parentPath.join(' -> ')}`;
266
- }
413
+ if (accessInfo.code === 'main_form') {
414
+ return 'Main form fields are always accessible from calculations';
267
415
  }
268
416
 
269
- if (contextType === 'event') {
270
- const eventInfo = getEventInfo(eventType);
417
+ if (accessInfo.code === 'same_repeatable_section') {
418
+ return 'Fields in the same RepeatableSection are accessible from this context';
419
+ }
271
420
 
272
- if (eventInfo.scope === 'main') {
273
- return `Record-level events (like '${eventType}') can only access main form fields, not RepeatableSection fields`;
274
- } else if (eventInfo.scope === 'contextual') {
275
- return `Field events can only access fields in the same RepeatableSection context as the triggering field`;
276
- }
421
+ if (accessInfo.code === 'ancestor_repeatable_context') {
422
+ return 'Fields in ancestor RepeatableSection contexts are accessible from this context';
277
423
  }
278
424
 
279
425
  return 'Check the field access rules for your current context';