@orcalang/orca-lang 0.1.0

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 (183) hide show
  1. package/LICENSE +176 -0
  2. package/README.md +128 -0
  3. package/dist/auth/index.d.ts +6 -0
  4. package/dist/auth/index.d.ts.map +1 -0
  5. package/dist/auth/index.js +6 -0
  6. package/dist/auth/index.js.map +1 -0
  7. package/dist/auth/lock.d.ts +2 -0
  8. package/dist/auth/lock.d.ts.map +1 -0
  9. package/dist/auth/lock.js +59 -0
  10. package/dist/auth/lock.js.map +1 -0
  11. package/dist/auth/providers/anthropic.d.ts +14 -0
  12. package/dist/auth/providers/anthropic.d.ts.map +1 -0
  13. package/dist/auth/providers/anthropic.js +145 -0
  14. package/dist/auth/providers/anthropic.js.map +1 -0
  15. package/dist/auth/providers/index.d.ts +3 -0
  16. package/dist/auth/providers/index.d.ts.map +1 -0
  17. package/dist/auth/providers/index.js +3 -0
  18. package/dist/auth/providers/index.js.map +1 -0
  19. package/dist/auth/providers/minimax.d.ts +6 -0
  20. package/dist/auth/providers/minimax.d.ts.map +1 -0
  21. package/dist/auth/providers/minimax.js +65 -0
  22. package/dist/auth/providers/minimax.js.map +1 -0
  23. package/dist/auth/refresh.d.ts +8 -0
  24. package/dist/auth/refresh.d.ts.map +1 -0
  25. package/dist/auth/refresh.js +104 -0
  26. package/dist/auth/refresh.js.map +1 -0
  27. package/dist/auth/store.d.ts +11 -0
  28. package/dist/auth/store.d.ts.map +1 -0
  29. package/dist/auth/store.js +63 -0
  30. package/dist/auth/store.js.map +1 -0
  31. package/dist/auth/types.d.ts +51 -0
  32. package/dist/auth/types.d.ts.map +1 -0
  33. package/dist/auth/types.js +2 -0
  34. package/dist/auth/types.js.map +1 -0
  35. package/dist/compiler/mermaid.d.ts +3 -0
  36. package/dist/compiler/mermaid.d.ts.map +1 -0
  37. package/dist/compiler/mermaid.js +86 -0
  38. package/dist/compiler/mermaid.js.map +1 -0
  39. package/dist/compiler/xstate.d.ts +15 -0
  40. package/dist/compiler/xstate.d.ts.map +1 -0
  41. package/dist/compiler/xstate.js +542 -0
  42. package/dist/compiler/xstate.js.map +1 -0
  43. package/dist/config/index.d.ts +3 -0
  44. package/dist/config/index.d.ts.map +1 -0
  45. package/dist/config/index.js +3 -0
  46. package/dist/config/index.js.map +1 -0
  47. package/dist/config/loader.d.ts +4 -0
  48. package/dist/config/loader.d.ts.map +1 -0
  49. package/dist/config/loader.js +109 -0
  50. package/dist/config/loader.js.map +1 -0
  51. package/dist/config/types.d.ts +13 -0
  52. package/dist/config/types.d.ts.map +1 -0
  53. package/dist/config/types.js +8 -0
  54. package/dist/config/types.js.map +1 -0
  55. package/dist/generators/index.d.ts +5 -0
  56. package/dist/generators/index.d.ts.map +1 -0
  57. package/dist/generators/index.js +5 -0
  58. package/dist/generators/index.js.map +1 -0
  59. package/dist/generators/registry.d.ts +12 -0
  60. package/dist/generators/registry.d.ts.map +1 -0
  61. package/dist/generators/registry.js +15 -0
  62. package/dist/generators/registry.js.map +1 -0
  63. package/dist/generators/typescript.d.ts +9 -0
  64. package/dist/generators/typescript.d.ts.map +1 -0
  65. package/dist/generators/typescript.js +55 -0
  66. package/dist/generators/typescript.js.map +1 -0
  67. package/dist/index.d.ts +10 -0
  68. package/dist/index.d.ts.map +1 -0
  69. package/dist/index.js +630 -0
  70. package/dist/index.js.map +1 -0
  71. package/dist/llm/anthropic.d.ts +14 -0
  72. package/dist/llm/anthropic.d.ts.map +1 -0
  73. package/dist/llm/anthropic.js +87 -0
  74. package/dist/llm/anthropic.js.map +1 -0
  75. package/dist/llm/grok.d.ts +13 -0
  76. package/dist/llm/grok.d.ts.map +1 -0
  77. package/dist/llm/grok.js +60 -0
  78. package/dist/llm/grok.js.map +1 -0
  79. package/dist/llm/index.d.ts +11 -0
  80. package/dist/llm/index.d.ts.map +1 -0
  81. package/dist/llm/index.js +23 -0
  82. package/dist/llm/index.js.map +1 -0
  83. package/dist/llm/ollama.d.ts +11 -0
  84. package/dist/llm/ollama.d.ts.map +1 -0
  85. package/dist/llm/ollama.js +51 -0
  86. package/dist/llm/ollama.js.map +1 -0
  87. package/dist/llm/openai.d.ts +13 -0
  88. package/dist/llm/openai.d.ts.map +1 -0
  89. package/dist/llm/openai.js +61 -0
  90. package/dist/llm/openai.js.map +1 -0
  91. package/dist/llm/provider.d.ts +32 -0
  92. package/dist/llm/provider.d.ts.map +1 -0
  93. package/dist/llm/provider.js +2 -0
  94. package/dist/llm/provider.js.map +1 -0
  95. package/dist/parser/ast-to-markdown.d.ts +3 -0
  96. package/dist/parser/ast-to-markdown.d.ts.map +1 -0
  97. package/dist/parser/ast-to-markdown.js +209 -0
  98. package/dist/parser/ast-to-markdown.js.map +1 -0
  99. package/dist/parser/ast.d.ts +183 -0
  100. package/dist/parser/ast.d.ts.map +1 -0
  101. package/dist/parser/ast.js +3 -0
  102. package/dist/parser/ast.js.map +1 -0
  103. package/dist/parser/markdown-parser.d.ts +8 -0
  104. package/dist/parser/markdown-parser.d.ts.map +1 -0
  105. package/dist/parser/markdown-parser.js +838 -0
  106. package/dist/parser/markdown-parser.js.map +1 -0
  107. package/dist/runtime/effects.d.ts +17 -0
  108. package/dist/runtime/effects.d.ts.map +1 -0
  109. package/dist/runtime/effects.js +28 -0
  110. package/dist/runtime/effects.js.map +1 -0
  111. package/dist/runtime/machine.d.ts +8 -0
  112. package/dist/runtime/machine.d.ts.map +1 -0
  113. package/dist/runtime/machine.js +158 -0
  114. package/dist/runtime/machine.js.map +1 -0
  115. package/dist/runtime/types.d.ts +37 -0
  116. package/dist/runtime/types.d.ts.map +1 -0
  117. package/dist/runtime/types.js +3 -0
  118. package/dist/runtime/types.js.map +1 -0
  119. package/dist/skills.d.ts +114 -0
  120. package/dist/skills.d.ts.map +1 -0
  121. package/dist/skills.js +1103 -0
  122. package/dist/skills.js.map +1 -0
  123. package/dist/tools.d.ts +18 -0
  124. package/dist/tools.d.ts.map +1 -0
  125. package/dist/tools.js +124 -0
  126. package/dist/tools.js.map +1 -0
  127. package/dist/verifier/completeness.d.ts +4 -0
  128. package/dist/verifier/completeness.d.ts.map +1 -0
  129. package/dist/verifier/completeness.js +82 -0
  130. package/dist/verifier/completeness.js.map +1 -0
  131. package/dist/verifier/determinism.d.ts +17 -0
  132. package/dist/verifier/determinism.d.ts.map +1 -0
  133. package/dist/verifier/determinism.js +301 -0
  134. package/dist/verifier/determinism.js.map +1 -0
  135. package/dist/verifier/properties.d.ts +6 -0
  136. package/dist/verifier/properties.d.ts.map +1 -0
  137. package/dist/verifier/properties.js +404 -0
  138. package/dist/verifier/properties.js.map +1 -0
  139. package/dist/verifier/structural.d.ts +50 -0
  140. package/dist/verifier/structural.d.ts.map +1 -0
  141. package/dist/verifier/structural.js +692 -0
  142. package/dist/verifier/structural.js.map +1 -0
  143. package/dist/verifier/types.d.ts +40 -0
  144. package/dist/verifier/types.d.ts.map +1 -0
  145. package/dist/verifier/types.js +2 -0
  146. package/dist/verifier/types.js.map +1 -0
  147. package/package.json +49 -0
  148. package/src/auth/index.ts +5 -0
  149. package/src/auth/lock.ts +71 -0
  150. package/src/auth/providers/anthropic.ts +192 -0
  151. package/src/auth/providers/index.ts +17 -0
  152. package/src/auth/providers/minimax.ts +100 -0
  153. package/src/auth/refresh.ts +138 -0
  154. package/src/auth/store.ts +75 -0
  155. package/src/auth/types.ts +62 -0
  156. package/src/compiler/mermaid.ts +109 -0
  157. package/src/compiler/xstate.ts +615 -0
  158. package/src/config/index.ts +2 -0
  159. package/src/config/loader.ts +122 -0
  160. package/src/config/types.ts +21 -0
  161. package/src/generators/index.ts +6 -0
  162. package/src/generators/registry.ts +27 -0
  163. package/src/generators/typescript.ts +67 -0
  164. package/src/index.ts +671 -0
  165. package/src/llm/anthropic.ts +102 -0
  166. package/src/llm/grok.ts +73 -0
  167. package/src/llm/index.ts +29 -0
  168. package/src/llm/ollama.ts +62 -0
  169. package/src/llm/openai.ts +74 -0
  170. package/src/llm/provider.ts +35 -0
  171. package/src/parser/ast-to-markdown.ts +220 -0
  172. package/src/parser/ast.ts +236 -0
  173. package/src/parser/markdown-parser.ts +844 -0
  174. package/src/runtime/effects.ts +48 -0
  175. package/src/runtime/machine.ts +201 -0
  176. package/src/runtime/types.ts +44 -0
  177. package/src/skills.ts +1339 -0
  178. package/src/tools.ts +144 -0
  179. package/src/verifier/completeness.ts +89 -0
  180. package/src/verifier/determinism.ts +328 -0
  181. package/src/verifier/properties.ts +507 -0
  182. package/src/verifier/structural.ts +803 -0
  183. package/src/verifier/types.ts +45 -0
@@ -0,0 +1,692 @@
1
+ /**
2
+ * Recursively flatten nested states into dot-notation names.
3
+ * E.g., "movement" with children "walking", "running" becomes:
4
+ * - "movement" (compound)
5
+ * - "movement.walking" (child)
6
+ * - "movement.running" (child)
7
+ */
8
+ export function flattenStates(states, parentPrefix) {
9
+ const result = [];
10
+ for (const state of states) {
11
+ const fullName = parentPrefix ? `${parentPrefix}.${state.name}` : state.name;
12
+ const isCompound = state.contains && state.contains.length > 0;
13
+ const isParallel = Boolean(state.parallel);
14
+ const flattened = {
15
+ name: fullName,
16
+ simpleName: state.name,
17
+ parentName: parentPrefix,
18
+ isCompound: Boolean(isCompound || isParallel),
19
+ isParallel,
20
+ isRegion: false,
21
+ isInitial: state.isInitial,
22
+ isFinal: state.isFinal,
23
+ };
24
+ if (isCompound) {
25
+ flattened.contains = flattenStates(state.contains, fullName);
26
+ }
27
+ result.push(flattened);
28
+ // Recursively flatten hierarchical children
29
+ if (isCompound) {
30
+ result.push(...flattened.contains);
31
+ }
32
+ // Flatten parallel regions
33
+ if (state.parallel) {
34
+ const regionChildren = [];
35
+ for (const region of state.parallel.regions) {
36
+ const regionFullName = `${fullName}.${region.name}`;
37
+ const regionFlattened = {
38
+ name: regionFullName,
39
+ simpleName: region.name,
40
+ parentName: fullName,
41
+ isCompound: true,
42
+ isParallel: false,
43
+ isRegion: true,
44
+ isInitial: false,
45
+ isFinal: false,
46
+ regionOf: fullName,
47
+ contains: flattenStates(region.states, regionFullName),
48
+ };
49
+ result.push(regionFlattened);
50
+ result.push(...regionFlattened.contains);
51
+ regionChildren.push(regionFlattened);
52
+ }
53
+ flattened.contains = regionChildren;
54
+ }
55
+ }
56
+ return result;
57
+ }
58
+ /**
59
+ * Find the initial child state of a compound state.
60
+ */
61
+ export function findInitialChild(state) {
62
+ if (!state.contains || state.contains.length === 0)
63
+ return undefined;
64
+ return state.contains.find(child => child.isInitial) || state.contains[0];
65
+ }
66
+ /**
67
+ * Resolve a state name - if it's a compound state, return its initial child.
68
+ */
69
+ export function resolveState(states, name) {
70
+ const state = states.find(s => s.name === name);
71
+ if (!state)
72
+ return undefined;
73
+ // If it's a compound state, return the initial child instead
74
+ if (state.isCompound) {
75
+ return findInitialChild(state);
76
+ }
77
+ return state;
78
+ }
79
+ export function analyzeMachine(machine) {
80
+ const stateMap = new Map();
81
+ const finalStates = [];
82
+ let initialState = null;
83
+ // Flatten nested states for analysis
84
+ const flattenedStates = flattenStates(machine.states);
85
+ const flattenedStateMap = new Map();
86
+ for (const fs of flattenedStates) {
87
+ flattenedStateMap.set(fs.name, fs);
88
+ }
89
+ // Initialize state info from flattened states
90
+ for (const fs of flattenedStates) {
91
+ // Skip children individually - they're reached through compound states
92
+ // But we need them in the map for reference
93
+ stateMap.set(fs.name, {
94
+ state: { name: fs.name, isInitial: fs.isInitial, isFinal: fs.isFinal },
95
+ incoming: [],
96
+ outgoing: [],
97
+ eventsHandled: new Set(),
98
+ eventsIgnored: new Set(),
99
+ });
100
+ if (fs.isFinal && !fs.parentName)
101
+ finalStates.push({ name: fs.name, isFinal: true, isInitial: false });
102
+ if (fs.isInitial && !fs.parentName)
103
+ initialState = { name: fs.name, isFinal: false, isInitial: true };
104
+ }
105
+ // Process transitions - handle compound state targets
106
+ for (const transition of machine.transitions) {
107
+ // Find source and target states (may be compound or leaf)
108
+ const sourceFS = flattenedStateMap.get(transition.source);
109
+ const targetFS = flattenedStateMap.get(transition.target);
110
+ // If target is a compound state, redirect to its initial child
111
+ let resolvedTarget = transition.target;
112
+ if (targetFS?.isCompound && !targetFS?.isParallel) {
113
+ const initialChild = findInitialChild(targetFS);
114
+ if (initialChild) {
115
+ resolvedTarget = initialChild.name;
116
+ }
117
+ }
118
+ const sourceInfo = stateMap.get(transition.source);
119
+ const targetInfo = stateMap.get(resolvedTarget);
120
+ if (sourceInfo) {
121
+ sourceInfo.outgoing.push(transition);
122
+ sourceInfo.eventsHandled.add(transition.event);
123
+ }
124
+ if (targetInfo) {
125
+ targetInfo.incoming.push(transition);
126
+ }
127
+ }
128
+ // Process onDone transitions for parallel states
129
+ for (const state of machine.states) {
130
+ if (state.parallel && state.onDone) {
131
+ const syntheticTransition = {
132
+ source: state.name,
133
+ event: '__onDone__',
134
+ target: state.onDone,
135
+ };
136
+ const sourceInfo = stateMap.get(state.name);
137
+ const targetInfo = stateMap.get(state.onDone);
138
+ if (sourceInfo) {
139
+ sourceInfo.outgoing.push(syntheticTransition);
140
+ }
141
+ if (targetInfo) {
142
+ targetInfo.incoming.push(syntheticTransition);
143
+ }
144
+ }
145
+ }
146
+ // Process ignored events from state definitions (check flattened map)
147
+ for (const [name, info] of stateMap) {
148
+ const fs = flattenedStateMap.get(name);
149
+ if (fs) {
150
+ // Find the original state definition
151
+ const originalState = findOriginalState(machine.states, fs.simpleName, fs.parentName);
152
+ if (originalState?.ignoredEvents) {
153
+ for (const event of originalState.ignoredEvents) {
154
+ info.eventsIgnored.add(event);
155
+ }
156
+ }
157
+ }
158
+ }
159
+ // Find orphan events and actions
160
+ const usedEvents = new Set();
161
+ const usedActions = new Set();
162
+ // Actions referenced in transitions
163
+ for (const t of machine.transitions) {
164
+ usedEvents.add(t.event);
165
+ if (t.action)
166
+ usedActions.add(t.action);
167
+ }
168
+ // Actions referenced in state on_entry/on_exit
169
+ for (const state of machine.states) {
170
+ collectActionsFromState(state, usedActions);
171
+ }
172
+ const orphanEvents = machine.events.filter(e => !usedEvents.has(e.name)).map(e => e.name);
173
+ const orphanActions = machine.actions.filter(a => !usedActions.has(a.name)).map(a => a.name);
174
+ // Orphan effects: declared in ## effects but no action references them via effectType
175
+ const usedEffectTypes = new Set(machine.actions.filter(a => a.hasEffect && a.effectType).map(a => a.effectType));
176
+ const orphanEffects = machine.effects
177
+ ? machine.effects.filter(e => !usedEffectTypes.has(e.name)).map(e => e.name)
178
+ : [];
179
+ return {
180
+ machine,
181
+ stateMap,
182
+ initialState,
183
+ finalStates,
184
+ orphanEvents,
185
+ orphanActions,
186
+ orphanEffects,
187
+ };
188
+ }
189
+ // Helper to find original state in nested structure (including parallel regions)
190
+ function findOriginalState(states, name, parentName) {
191
+ if (!parentName) {
192
+ return states.find(s => s.name === name);
193
+ }
194
+ for (const state of states) {
195
+ if (state.contains) {
196
+ if (state.name === parentName) {
197
+ return state.contains.find(s => s.name === name);
198
+ }
199
+ const found = findOriginalState(state.contains, name, parentName);
200
+ if (found)
201
+ return found;
202
+ }
203
+ if (state.parallel) {
204
+ for (const region of state.parallel.regions) {
205
+ // Check if parentName matches "stateName.regionName"
206
+ const regionFullName = `${state.name}.${region.name}`;
207
+ if (regionFullName === parentName) {
208
+ return region.states.find(s => s.name === name);
209
+ }
210
+ // Recurse into region states
211
+ const found = findOriginalState(region.states, name, parentName);
212
+ if (found)
213
+ return found;
214
+ }
215
+ }
216
+ }
217
+ return undefined;
218
+ }
219
+ // Helper to collect actions from state and nested states (including parallel regions)
220
+ function collectActionsFromState(state, actions) {
221
+ if (state.onEntry)
222
+ actions.add(state.onEntry);
223
+ if (state.onExit)
224
+ actions.add(state.onExit);
225
+ if (state.contains) {
226
+ for (const child of state.contains) {
227
+ collectActionsFromState(child, actions);
228
+ }
229
+ }
230
+ if (state.parallel) {
231
+ for (const region of state.parallel.regions) {
232
+ for (const child of region.states) {
233
+ collectActionsFromState(child, actions);
234
+ }
235
+ }
236
+ }
237
+ }
238
+ export function checkReachability(analysis) {
239
+ const errors = [];
240
+ const { stateMap, initialState } = analysis;
241
+ if (!initialState) {
242
+ errors.push({
243
+ code: 'NO_INITIAL_STATE',
244
+ message: 'Machine has no initial state',
245
+ severity: 'error',
246
+ suggestion: 'Mark one state with [initial] annotation',
247
+ });
248
+ return errors;
249
+ }
250
+ const visited = new Set();
251
+ const queue = [initialState.name];
252
+ while (queue.length > 0) {
253
+ const name = queue.shift();
254
+ if (visited.has(name))
255
+ continue;
256
+ visited.add(name);
257
+ const info = stateMap.get(name);
258
+ if (!info)
259
+ continue;
260
+ for (const t of info.outgoing) {
261
+ if (!visited.has(t.target)) {
262
+ queue.push(t.target);
263
+ }
264
+ }
265
+ }
266
+ for (const state of analysis.machine.states) {
267
+ if (!visited.has(state.name)) {
268
+ errors.push({
269
+ code: 'UNREACHABLE_STATE',
270
+ message: `State '${state.name}' is unreachable from initial state '${initialState.name}'`,
271
+ severity: 'error',
272
+ location: { state: state.name },
273
+ suggestion: `Add a transition that reaches '${state.name}'`,
274
+ });
275
+ }
276
+ }
277
+ return errors;
278
+ }
279
+ export function checkDeadlocks(analysis) {
280
+ const errors = [];
281
+ const { stateMap, finalStates } = analysis;
282
+ const finalStateNames = new Set(finalStates.map(s => s.name));
283
+ // Build a set of compound state names and child state names
284
+ const compoundStates = new Set();
285
+ const childStates = new Set();
286
+ for (const [name, info] of stateMap) {
287
+ // If state name has a dot, it's a child of a compound state
288
+ if (name.includes('.')) {
289
+ childStates.add(name);
290
+ const parentName = name.split('.')[0];
291
+ compoundStates.add(parentName);
292
+ }
293
+ }
294
+ for (const [name, info] of stateMap) {
295
+ // Skip child states - they're controlled by parent transitions
296
+ if (childStates.has(name)) {
297
+ continue;
298
+ }
299
+ // Skip compound states (parents) - they delegate to children
300
+ if (compoundStates.has(name)) {
301
+ continue;
302
+ }
303
+ // Final states should have no outgoing transitions (except self-loops for ignored events)
304
+ if (finalStateNames.has(name)) {
305
+ const realOutgoing = info.outgoing.filter(t => t.target !== name);
306
+ if (realOutgoing.length > 0) {
307
+ errors.push({
308
+ code: 'FINAL_STATE_OUTGOING',
309
+ message: `Final state '${name}' has outgoing transitions`,
310
+ severity: 'error',
311
+ location: { state: name },
312
+ suggestion: 'Remove transitions from final states or remove [final] marker',
313
+ });
314
+ }
315
+ }
316
+ else {
317
+ // Non-final states must have outgoing transitions
318
+ if (info.outgoing.length === 0) {
319
+ errors.push({
320
+ code: 'DEADLOCK',
321
+ message: `Non-final state '${name}' has no outgoing transitions`,
322
+ severity: 'error',
323
+ location: { state: name },
324
+ suggestion: `Add transitions from '${name}' or mark it as [final]`,
325
+ });
326
+ }
327
+ }
328
+ }
329
+ return errors;
330
+ }
331
+ export function checkOrphans(analysis) {
332
+ const errors = [];
333
+ for (const event of analysis.orphanEvents) {
334
+ errors.push({
335
+ code: 'ORPHAN_EVENT',
336
+ message: `Event '${event}' is declared but never used in any transition`,
337
+ severity: 'warning',
338
+ suggestion: `Use '${event}' in a transition or remove it from the events declaration`,
339
+ });
340
+ }
341
+ for (const action of analysis.orphanActions) {
342
+ errors.push({
343
+ code: 'ORPHAN_ACTION',
344
+ message: `Action '${action}' is declared but never referenced in any transition`,
345
+ severity: 'warning',
346
+ suggestion: `Reference '${action}' in a transition or remove it from the actions declaration`,
347
+ });
348
+ }
349
+ for (const effect of analysis.orphanEffects) {
350
+ errors.push({
351
+ code: 'ORPHAN_EFFECT',
352
+ message: `Effect '${effect}' is declared but never referenced by any action`,
353
+ severity: 'warning',
354
+ suggestion: `Reference '${effect}' in an action signature or remove it from the effects declaration`,
355
+ });
356
+ }
357
+ // Undeclared effects: actions reference an effectType not in ## effects
358
+ // Only checked when the ## effects section is explicitly present
359
+ if (analysis.machine.effects !== undefined) {
360
+ const declaredEffects = new Set(analysis.machine.effects.map(e => e.name));
361
+ for (const action of analysis.machine.actions) {
362
+ if (action.hasEffect && action.effectType && !declaredEffects.has(action.effectType)) {
363
+ errors.push({
364
+ code: 'UNDECLARED_EFFECT',
365
+ message: `Action '${action.name}' references effect '${action.effectType}' which is not declared in ## effects`,
366
+ severity: 'warning',
367
+ suggestion: `Add '${action.effectType}' to the ## effects section or remove the effect reference`,
368
+ });
369
+ }
370
+ }
371
+ }
372
+ return errors;
373
+ }
374
+ export function checkStructural(machine) {
375
+ const analysis = analyzeMachine(machine);
376
+ const errors = [
377
+ ...checkReachability(analysis),
378
+ ...checkDeadlocks(analysis),
379
+ ...checkOrphans(analysis),
380
+ ];
381
+ return {
382
+ valid: errors.filter(e => e.severity === 'error').length === 0,
383
+ errors,
384
+ };
385
+ }
386
+ // ============================================================
387
+ // Cross-Machine Analysis (for multi-machine files)
388
+ // ============================================================
389
+ const MAX_TOTAL_STATES = 64;
390
+ /**
391
+ * Build a map of machine name -> list of machines it invokes
392
+ */
393
+ function buildInvocationGraph(file) {
394
+ const graph = new Map();
395
+ for (const machine of file.machines) {
396
+ const invoked = [];
397
+ collectInvocations(machine.states, invoked);
398
+ graph.set(machine.name, invoked);
399
+ }
400
+ return graph;
401
+ }
402
+ function collectInvocations(states, result) {
403
+ for (const state of states) {
404
+ if (state.invoke) {
405
+ result.push(state.invoke.machine);
406
+ }
407
+ if (state.contains) {
408
+ collectInvocations(state.contains, result);
409
+ }
410
+ if (state.parallel) {
411
+ for (const region of state.parallel.regions) {
412
+ collectInvocations(region.states, result);
413
+ }
414
+ }
415
+ }
416
+ }
417
+ /**
418
+ * Detect cycles in the invocation graph using DFS.
419
+ * Returns an array of machine names forming a cycle, or empty if no cycle.
420
+ */
421
+ function detectCycle(graph, machine, visited, path) {
422
+ visited.add(machine);
423
+ path.push(machine);
424
+ const invoked = graph.get(machine) || [];
425
+ for (const child of invoked) {
426
+ if (path.includes(child)) {
427
+ // Found cycle - return the cycle starting from the child
428
+ const cycleStart = path.indexOf(child);
429
+ return [...path.slice(cycleStart), child];
430
+ }
431
+ if (!visited.has(child)) {
432
+ const cycle = detectCycle(graph, child, visited, [...path]);
433
+ if (cycle.length > 0)
434
+ return cycle;
435
+ }
436
+ }
437
+ return [];
438
+ }
439
+ /**
440
+ * Check that a machine can reach a final state.
441
+ */
442
+ function canReachFinalState(machine, visited = new Set()) {
443
+ if (visited.has(machine.name))
444
+ return false; // Prevent infinite recursion
445
+ visited.add(machine.name);
446
+ // Check if machine has any final states
447
+ const finalStateNames = new Set();
448
+ collectFinalStates(machine.states, finalStateNames);
449
+ if (finalStateNames.size === 0)
450
+ return false;
451
+ // Build transition map for reachability check
452
+ const transitionMap = new Map();
453
+ for (const t of machine.transitions) {
454
+ if (!transitionMap.has(t.source)) {
455
+ transitionMap.set(t.source, new Set());
456
+ }
457
+ transitionMap.get(t.source).add(t.target);
458
+ }
459
+ // BFS from initial state to see if we can reach any final state
460
+ const initialState = machine.states.find(s => s.isInitial);
461
+ if (!initialState)
462
+ return false;
463
+ const queue = [initialState.name];
464
+ const seen = new Set();
465
+ while (queue.length > 0) {
466
+ const current = queue.shift();
467
+ if (seen.has(current))
468
+ continue;
469
+ seen.add(current);
470
+ if (finalStateNames.has(current))
471
+ return true;
472
+ const targets = transitionMap.get(current);
473
+ if (targets) {
474
+ for (const target of targets) {
475
+ if (!seen.has(target)) {
476
+ queue.push(target);
477
+ }
478
+ }
479
+ }
480
+ }
481
+ return false;
482
+ }
483
+ function collectFinalStates(states, result) {
484
+ for (const state of states) {
485
+ if (state.isFinal) {
486
+ result.add(state.name);
487
+ }
488
+ if (state.contains) {
489
+ collectFinalStates(state.contains, result);
490
+ }
491
+ if (state.parallel) {
492
+ for (const region of state.parallel.regions) {
493
+ collectFinalStates(region.states, result);
494
+ }
495
+ }
496
+ }
497
+ }
498
+ /**
499
+ * Validate input field mappings - fields must exist in parent context
500
+ */
501
+ function validateInputMappings(file, machineMap, errors, warnings) {
502
+ for (const machine of file.machines) {
503
+ const contextFields = new Set(machine.context.map(c => c.name));
504
+ // Also check for ctx.field references in transitions that might give us field names
505
+ for (const t of machine.transitions) {
506
+ if (t.guard) {
507
+ // Guard references might use context fields
508
+ // For now we just validate explicit input mappings
509
+ }
510
+ }
511
+ // Check invoke input mappings
512
+ validateInvokeInputs(machine.states, machine.name, contextFields, errors, warnings);
513
+ }
514
+ }
515
+ function validateInvokeInputs(states, machineName, contextFields, errors, warnings) {
516
+ for (const state of states) {
517
+ if (state.invoke?.input) {
518
+ for (const [childField, parentField] of Object.entries(state.invoke.input)) {
519
+ // parentField is like "ctx.order_id" or just "order_id"
520
+ const fieldName = parentField.replace(/^ctx\./, '');
521
+ if (!contextFields.has(fieldName)) {
522
+ errors.push({
523
+ code: 'INVALID_INPUT_MAPPING',
524
+ message: `Machine '${machineName}' state '${state.name}': input mapping references '${fieldName}' which does not exist in context`,
525
+ severity: 'error',
526
+ location: { state: state.name },
527
+ suggestion: `Add '${fieldName}' to the context declaration or use an existing field`,
528
+ });
529
+ }
530
+ }
531
+ }
532
+ if (state.contains) {
533
+ validateInvokeInputs(state.contains, machineName, contextFields, errors, warnings);
534
+ }
535
+ if (state.parallel) {
536
+ for (const region of state.parallel.regions) {
537
+ validateInvokeInputs(region.states, machineName, contextFields, errors, warnings);
538
+ }
539
+ }
540
+ }
541
+ }
542
+ /**
543
+ * Analyze an entire OrcaFile with multiple machines.
544
+ * Performs cross-machine validation including:
545
+ * - Machine resolution (invoke.machine must exist)
546
+ * - Circular invocation detection
547
+ * - Child reachability to final state
548
+ * - onDone/onError event validation
549
+ * - Missing on_error warning
550
+ * - Combined state budget
551
+ * - Input field validation
552
+ */
553
+ export function analyzeFile(file) {
554
+ const errors = [];
555
+ const warnings = [];
556
+ const machineMap = new Map();
557
+ for (const machine of file.machines) {
558
+ machineMap.set(machine.name, machine);
559
+ }
560
+ // Build invocation graph
561
+ const invocationGraph = buildInvocationGraph(file);
562
+ // Check total state count
563
+ let totalStates = 0;
564
+ for (const machine of file.machines) {
565
+ const stateCount = countStates(machine.states);
566
+ totalStates += stateCount;
567
+ }
568
+ if (totalStates > MAX_TOTAL_STATES) {
569
+ errors.push({
570
+ code: 'STATE_LIMIT_EXCEEDED',
571
+ message: `Combined state count (${totalStates}) exceeds limit of ${MAX_TOTAL_STATES}`,
572
+ severity: 'error',
573
+ suggestion: 'Split machines into separate files or reduce state count',
574
+ });
575
+ }
576
+ // Analyze each machine
577
+ const analyses = new Map();
578
+ for (const machine of file.machines) {
579
+ analyses.set(machine.name, analyzeMachine(machine));
580
+ }
581
+ // Check for cycles
582
+ const visited = new Set();
583
+ for (const machine of file.machines) {
584
+ if (!visited.has(machine.name)) {
585
+ const cycle = detectCycle(invocationGraph, machine.name, visited, []);
586
+ if (cycle.length > 0) {
587
+ errors.push({
588
+ code: 'CIRCULAR_INVOCATION',
589
+ message: `Circular invocation detected: ${cycle.join(' -> ')}`,
590
+ severity: 'error',
591
+ suggestion: 'Remove the circular invocation chain',
592
+ });
593
+ }
594
+ }
595
+ }
596
+ // Check each invoke
597
+ for (const machine of file.machines) {
598
+ const eventNames = new Set(machine.events.map(e => e.name));
599
+ checkInvocations(machine.states, machine.name, machineMap, eventNames, errors, warnings);
600
+ }
601
+ // Validate input mappings
602
+ validateInputMappings(file, machineMap, errors, warnings);
603
+ return {
604
+ machines: analyses,
605
+ invocationGraph,
606
+ errors,
607
+ warnings,
608
+ };
609
+ }
610
+ function checkInvocations(states, machineName, machineMap, eventNames, errors, warnings) {
611
+ for (const state of states) {
612
+ if (state.invoke) {
613
+ const invokedMachine = state.invoke.machine;
614
+ // Check machine exists
615
+ if (!machineMap.has(invokedMachine)) {
616
+ errors.push({
617
+ code: 'UNKNOWN_MACHINE',
618
+ message: `Machine '${machineName}' state '${state.name}': invokes unknown machine '${invokedMachine}'`,
619
+ severity: 'error',
620
+ location: { state: state.name },
621
+ suggestion: `Define a machine named '${invokedMachine}' in the same file`,
622
+ });
623
+ }
624
+ else {
625
+ // Check child can reach final state
626
+ const childMachine = machineMap.get(invokedMachine);
627
+ if (!canReachFinalState(childMachine)) {
628
+ errors.push({
629
+ code: 'CHILD_NO_FINAL_STATE',
630
+ message: `Machine '${machineName}' state '${state.name}': invoked machine '${invokedMachine}' has no reachable final state`,
631
+ severity: 'error',
632
+ location: { state: state.name },
633
+ suggestion: `Add at least one final state to '${invokedMachine}'`,
634
+ });
635
+ }
636
+ }
637
+ // Check onDone event exists in parent's events
638
+ if (state.invoke.onDone && !eventNames.has(state.invoke.onDone)) {
639
+ errors.push({
640
+ code: 'UNKNOWN_ON_DONE_EVENT',
641
+ message: `Machine '${machineName}' state '${state.name}': on_done references event '${state.invoke.onDone}' which is not declared`,
642
+ severity: 'error',
643
+ location: { state: state.name },
644
+ suggestion: `Add '${state.invoke.onDone}' to the events declaration`,
645
+ });
646
+ }
647
+ // Check onError event exists in parent's events
648
+ if (state.invoke.onError && !eventNames.has(state.invoke.onError)) {
649
+ errors.push({
650
+ code: 'UNKNOWN_ON_ERROR_EVENT',
651
+ message: `Machine '${machineName}' state '${state.name}': on_error references event '${state.invoke.onError}' which is not declared`,
652
+ severity: 'error',
653
+ location: { state: state.name },
654
+ suggestion: `Add '${state.invoke.onError}' to the events declaration`,
655
+ });
656
+ }
657
+ // Warn if no onError (potential deadlock on child error)
658
+ if (!state.invoke.onError) {
659
+ warnings.push({
660
+ code: 'MISSING_ON_ERROR',
661
+ message: `Machine '${machineName}' state '${state.name}': invoke has no on_error handler - child errors will cause deadlock`,
662
+ severity: 'warning',
663
+ location: { state: state.name },
664
+ suggestion: `Add on_error: EVENT to handle child machine failures`,
665
+ });
666
+ }
667
+ }
668
+ if (state.contains) {
669
+ checkInvocations(state.contains, machineName, machineMap, eventNames, errors, warnings);
670
+ }
671
+ if (state.parallel) {
672
+ for (const region of state.parallel.regions) {
673
+ checkInvocations(region.states, machineName, machineMap, eventNames, errors, warnings);
674
+ }
675
+ }
676
+ }
677
+ }
678
+ function countStates(states) {
679
+ let count = states.length;
680
+ for (const state of states) {
681
+ if (state.contains) {
682
+ count += countStates(state.contains);
683
+ }
684
+ if (state.parallel) {
685
+ for (const region of state.parallel.regions) {
686
+ count += countStates(region.states);
687
+ }
688
+ }
689
+ }
690
+ return count;
691
+ }
692
+ //# sourceMappingURL=structural.js.map