@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.
- package/LICENSE +176 -0
- package/README.md +128 -0
- package/dist/auth/index.d.ts +6 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +6 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/lock.d.ts +2 -0
- package/dist/auth/lock.d.ts.map +1 -0
- package/dist/auth/lock.js +59 -0
- package/dist/auth/lock.js.map +1 -0
- package/dist/auth/providers/anthropic.d.ts +14 -0
- package/dist/auth/providers/anthropic.d.ts.map +1 -0
- package/dist/auth/providers/anthropic.js +145 -0
- package/dist/auth/providers/anthropic.js.map +1 -0
- package/dist/auth/providers/index.d.ts +3 -0
- package/dist/auth/providers/index.d.ts.map +1 -0
- package/dist/auth/providers/index.js +3 -0
- package/dist/auth/providers/index.js.map +1 -0
- package/dist/auth/providers/minimax.d.ts +6 -0
- package/dist/auth/providers/minimax.d.ts.map +1 -0
- package/dist/auth/providers/minimax.js +65 -0
- package/dist/auth/providers/minimax.js.map +1 -0
- package/dist/auth/refresh.d.ts +8 -0
- package/dist/auth/refresh.d.ts.map +1 -0
- package/dist/auth/refresh.js +104 -0
- package/dist/auth/refresh.js.map +1 -0
- package/dist/auth/store.d.ts +11 -0
- package/dist/auth/store.d.ts.map +1 -0
- package/dist/auth/store.js +63 -0
- package/dist/auth/store.js.map +1 -0
- package/dist/auth/types.d.ts +51 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/auth/types.js +2 -0
- package/dist/auth/types.js.map +1 -0
- package/dist/compiler/mermaid.d.ts +3 -0
- package/dist/compiler/mermaid.d.ts.map +1 -0
- package/dist/compiler/mermaid.js +86 -0
- package/dist/compiler/mermaid.js.map +1 -0
- package/dist/compiler/xstate.d.ts +15 -0
- package/dist/compiler/xstate.d.ts.map +1 -0
- package/dist/compiler/xstate.js +542 -0
- package/dist/compiler/xstate.js.map +1 -0
- package/dist/config/index.d.ts +3 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +3 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/loader.d.ts +4 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +109 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/types.d.ts +13 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +8 -0
- package/dist/config/types.js.map +1 -0
- package/dist/generators/index.d.ts +5 -0
- package/dist/generators/index.d.ts.map +1 -0
- package/dist/generators/index.js +5 -0
- package/dist/generators/index.js.map +1 -0
- package/dist/generators/registry.d.ts +12 -0
- package/dist/generators/registry.d.ts.map +1 -0
- package/dist/generators/registry.js +15 -0
- package/dist/generators/registry.js.map +1 -0
- package/dist/generators/typescript.d.ts +9 -0
- package/dist/generators/typescript.d.ts.map +1 -0
- package/dist/generators/typescript.js +55 -0
- package/dist/generators/typescript.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +630 -0
- package/dist/index.js.map +1 -0
- package/dist/llm/anthropic.d.ts +14 -0
- package/dist/llm/anthropic.d.ts.map +1 -0
- package/dist/llm/anthropic.js +87 -0
- package/dist/llm/anthropic.js.map +1 -0
- package/dist/llm/grok.d.ts +13 -0
- package/dist/llm/grok.d.ts.map +1 -0
- package/dist/llm/grok.js +60 -0
- package/dist/llm/grok.js.map +1 -0
- package/dist/llm/index.d.ts +11 -0
- package/dist/llm/index.d.ts.map +1 -0
- package/dist/llm/index.js +23 -0
- package/dist/llm/index.js.map +1 -0
- package/dist/llm/ollama.d.ts +11 -0
- package/dist/llm/ollama.d.ts.map +1 -0
- package/dist/llm/ollama.js +51 -0
- package/dist/llm/ollama.js.map +1 -0
- package/dist/llm/openai.d.ts +13 -0
- package/dist/llm/openai.d.ts.map +1 -0
- package/dist/llm/openai.js +61 -0
- package/dist/llm/openai.js.map +1 -0
- package/dist/llm/provider.d.ts +32 -0
- package/dist/llm/provider.d.ts.map +1 -0
- package/dist/llm/provider.js +2 -0
- package/dist/llm/provider.js.map +1 -0
- package/dist/parser/ast-to-markdown.d.ts +3 -0
- package/dist/parser/ast-to-markdown.d.ts.map +1 -0
- package/dist/parser/ast-to-markdown.js +209 -0
- package/dist/parser/ast-to-markdown.js.map +1 -0
- package/dist/parser/ast.d.ts +183 -0
- package/dist/parser/ast.d.ts.map +1 -0
- package/dist/parser/ast.js +3 -0
- package/dist/parser/ast.js.map +1 -0
- package/dist/parser/markdown-parser.d.ts +8 -0
- package/dist/parser/markdown-parser.d.ts.map +1 -0
- package/dist/parser/markdown-parser.js +838 -0
- package/dist/parser/markdown-parser.js.map +1 -0
- package/dist/runtime/effects.d.ts +17 -0
- package/dist/runtime/effects.d.ts.map +1 -0
- package/dist/runtime/effects.js +28 -0
- package/dist/runtime/effects.js.map +1 -0
- package/dist/runtime/machine.d.ts +8 -0
- package/dist/runtime/machine.d.ts.map +1 -0
- package/dist/runtime/machine.js +158 -0
- package/dist/runtime/machine.js.map +1 -0
- package/dist/runtime/types.d.ts +37 -0
- package/dist/runtime/types.d.ts.map +1 -0
- package/dist/runtime/types.js +3 -0
- package/dist/runtime/types.js.map +1 -0
- package/dist/skills.d.ts +114 -0
- package/dist/skills.d.ts.map +1 -0
- package/dist/skills.js +1103 -0
- package/dist/skills.js.map +1 -0
- package/dist/tools.d.ts +18 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +124 -0
- package/dist/tools.js.map +1 -0
- package/dist/verifier/completeness.d.ts +4 -0
- package/dist/verifier/completeness.d.ts.map +1 -0
- package/dist/verifier/completeness.js +82 -0
- package/dist/verifier/completeness.js.map +1 -0
- package/dist/verifier/determinism.d.ts +17 -0
- package/dist/verifier/determinism.d.ts.map +1 -0
- package/dist/verifier/determinism.js +301 -0
- package/dist/verifier/determinism.js.map +1 -0
- package/dist/verifier/properties.d.ts +6 -0
- package/dist/verifier/properties.d.ts.map +1 -0
- package/dist/verifier/properties.js +404 -0
- package/dist/verifier/properties.js.map +1 -0
- package/dist/verifier/structural.d.ts +50 -0
- package/dist/verifier/structural.d.ts.map +1 -0
- package/dist/verifier/structural.js +692 -0
- package/dist/verifier/structural.js.map +1 -0
- package/dist/verifier/types.d.ts +40 -0
- package/dist/verifier/types.d.ts.map +1 -0
- package/dist/verifier/types.js +2 -0
- package/dist/verifier/types.js.map +1 -0
- package/package.json +49 -0
- package/src/auth/index.ts +5 -0
- package/src/auth/lock.ts +71 -0
- package/src/auth/providers/anthropic.ts +192 -0
- package/src/auth/providers/index.ts +17 -0
- package/src/auth/providers/minimax.ts +100 -0
- package/src/auth/refresh.ts +138 -0
- package/src/auth/store.ts +75 -0
- package/src/auth/types.ts +62 -0
- package/src/compiler/mermaid.ts +109 -0
- package/src/compiler/xstate.ts +615 -0
- package/src/config/index.ts +2 -0
- package/src/config/loader.ts +122 -0
- package/src/config/types.ts +21 -0
- package/src/generators/index.ts +6 -0
- package/src/generators/registry.ts +27 -0
- package/src/generators/typescript.ts +67 -0
- package/src/index.ts +671 -0
- package/src/llm/anthropic.ts +102 -0
- package/src/llm/grok.ts +73 -0
- package/src/llm/index.ts +29 -0
- package/src/llm/ollama.ts +62 -0
- package/src/llm/openai.ts +74 -0
- package/src/llm/provider.ts +35 -0
- package/src/parser/ast-to-markdown.ts +220 -0
- package/src/parser/ast.ts +236 -0
- package/src/parser/markdown-parser.ts +844 -0
- package/src/runtime/effects.ts +48 -0
- package/src/runtime/machine.ts +201 -0
- package/src/runtime/types.ts +44 -0
- package/src/skills.ts +1339 -0
- package/src/tools.ts +144 -0
- package/src/verifier/completeness.ts +89 -0
- package/src/verifier/determinism.ts +328 -0
- package/src/verifier/properties.ts +507 -0
- package/src/verifier/structural.ts +803 -0
- 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
|