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