@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,507 @@
|
|
|
1
|
+
import { MachineDef, Property, ReachabilityProperty, PassesThroughProperty, RespondsProperty, InvariantProperty, GuardRef, GuardDef } from '../parser/ast.js';
|
|
2
|
+
import { VerificationResult, VerificationError, MachineAnalysis, StateInfo } from './types.js';
|
|
3
|
+
import { analyzeMachine, flattenStates, FlattenedState } from './structural.js';
|
|
4
|
+
import { resolveGuardExpression, isExpressionStaticallyFalse } from './determinism.js';
|
|
5
|
+
|
|
6
|
+
const DEFAULT_MAX_STATES = 64;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Check if a transition's guard is statically false (can never fire).
|
|
10
|
+
* Used in guard-aware BFS to prune impossible transitions.
|
|
11
|
+
*/
|
|
12
|
+
function isTransitionStaticallyBlocked(
|
|
13
|
+
transition: { guard?: GuardRef },
|
|
14
|
+
guardDefs: Map<string, GuardDef>
|
|
15
|
+
): boolean {
|
|
16
|
+
if (!transition.guard) return false; // No guard = always possible
|
|
17
|
+
|
|
18
|
+
const resolved = resolveGuardExpression(transition.guard, guardDefs);
|
|
19
|
+
if (!resolved) return false; // Can't resolve = assume possible
|
|
20
|
+
|
|
21
|
+
return isExpressionStaticallyFalse(resolved);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* BFS from source state, returning reachable set and parent map for counterexample traces.
|
|
26
|
+
* Supports guard-aware mode: skips transitions with statically false guards.
|
|
27
|
+
*/
|
|
28
|
+
function bfs(
|
|
29
|
+
stateMap: Map<string, StateInfo>,
|
|
30
|
+
source: string,
|
|
31
|
+
options?: {
|
|
32
|
+
excludeState?: string;
|
|
33
|
+
maxDepth?: number;
|
|
34
|
+
guardDefs?: Map<string, GuardDef>; // Enable guard-aware pruning
|
|
35
|
+
}
|
|
36
|
+
): { reachable: Set<string>; parent: Map<string, { state: string; event: string; guard?: string }> } {
|
|
37
|
+
const reachable = new Set<string>();
|
|
38
|
+
const parent = new Map<string, { state: string; event: string; guard?: string }>();
|
|
39
|
+
const queue: Array<{ name: string; depth: number }> = [{ name: source, depth: 0 }];
|
|
40
|
+
|
|
41
|
+
reachable.add(source);
|
|
42
|
+
|
|
43
|
+
while (queue.length > 0) {
|
|
44
|
+
const { name, depth } = queue.shift()!;
|
|
45
|
+
|
|
46
|
+
if (options?.maxDepth !== undefined && depth >= options.maxDepth) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const info = stateMap.get(name);
|
|
51
|
+
if (!info) continue;
|
|
52
|
+
|
|
53
|
+
for (const t of info.outgoing) {
|
|
54
|
+
const target = t.target;
|
|
55
|
+
|
|
56
|
+
// Skip excluded state
|
|
57
|
+
if (options?.excludeState && target === options.excludeState) continue;
|
|
58
|
+
|
|
59
|
+
// Guard-aware: skip transitions with statically false guards
|
|
60
|
+
if (options?.guardDefs && isTransitionStaticallyBlocked(t, options.guardDefs)) continue;
|
|
61
|
+
|
|
62
|
+
if (!reachable.has(target)) {
|
|
63
|
+
reachable.add(target);
|
|
64
|
+
const guardName = t.guard ? (t.guard.negated ? `!${t.guard.name}` : t.guard.name) : undefined;
|
|
65
|
+
parent.set(target, { state: name, event: t.event, guard: guardName });
|
|
66
|
+
queue.push({ name: target, depth: depth + 1 });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { reachable, parent };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Reconstruct a path from source to target using the parent map.
|
|
76
|
+
*/
|
|
77
|
+
function reconstructPath(
|
|
78
|
+
parent: Map<string, { state: string; event: string; guard?: string }>,
|
|
79
|
+
source: string,
|
|
80
|
+
target: string
|
|
81
|
+
): Array<{ state: string; event?: string; guard?: string }> {
|
|
82
|
+
const path: Array<{ state: string; event?: string; guard?: string }> = [];
|
|
83
|
+
let current = target;
|
|
84
|
+
|
|
85
|
+
while (current !== source) {
|
|
86
|
+
const prev = parent.get(current);
|
|
87
|
+
if (!prev) break;
|
|
88
|
+
path.unshift({ state: current, event: prev.event, guard: prev.guard });
|
|
89
|
+
current = prev.state;
|
|
90
|
+
}
|
|
91
|
+
path.unshift({ state: source });
|
|
92
|
+
|
|
93
|
+
return path;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Format a path as a readable string, including guard conditions.
|
|
98
|
+
*/
|
|
99
|
+
function formatPath(path: Array<{ state: string; event?: string; guard?: string }>): string {
|
|
100
|
+
return path
|
|
101
|
+
.map((step, i) => {
|
|
102
|
+
if (i === 0) return step.state;
|
|
103
|
+
const guardStr = step.guard ? ` [${step.guard}]` : '';
|
|
104
|
+
return `[${step.event}${guardStr}] -> ${step.state}`;
|
|
105
|
+
})
|
|
106
|
+
.join(' ');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Check if any step in a path requires a guard condition.
|
|
111
|
+
*/
|
|
112
|
+
function pathHasGuards(path: Array<{ state: string; event?: string; guard?: string }>): boolean {
|
|
113
|
+
return path.some(step => step.guard !== undefined);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Resolve a state name to a flattened state name.
|
|
118
|
+
* Supports both exact match and simple name match.
|
|
119
|
+
*/
|
|
120
|
+
function resolveStateName(
|
|
121
|
+
name: string,
|
|
122
|
+
flattenedStates: FlattenedState[]
|
|
123
|
+
): { resolved: string; error?: VerificationError } {
|
|
124
|
+
// Exact match
|
|
125
|
+
const exact = flattenedStates.find(fs => fs.name === name);
|
|
126
|
+
if (exact) return { resolved: exact.name };
|
|
127
|
+
|
|
128
|
+
// Simple name match
|
|
129
|
+
const matches = flattenedStates.filter(fs => fs.simpleName === name && !fs.isRegion);
|
|
130
|
+
if (matches.length === 1) return { resolved: matches[0].name };
|
|
131
|
+
if (matches.length > 1) {
|
|
132
|
+
return {
|
|
133
|
+
resolved: '',
|
|
134
|
+
error: {
|
|
135
|
+
code: 'PROPERTY_AMBIGUOUS_STATE',
|
|
136
|
+
message: `State name '${name}' is ambiguous — matches: ${matches.map(m => m.name).join(', ')}`,
|
|
137
|
+
severity: 'error',
|
|
138
|
+
suggestion: `Use the full dot-notation name to disambiguate`,
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
resolved: '',
|
|
145
|
+
error: {
|
|
146
|
+
code: 'PROPERTY_INVALID_STATE',
|
|
147
|
+
message: `State '${name}' does not exist in this machine`,
|
|
148
|
+
severity: 'error',
|
|
149
|
+
suggestion: `Check the state name. Available states: ${flattenedStates.filter(fs => !fs.isRegion).map(fs => fs.name).join(', ')}`,
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// --- Property checkers ---
|
|
155
|
+
|
|
156
|
+
function checkReachable(
|
|
157
|
+
prop: ReachabilityProperty,
|
|
158
|
+
analysis: MachineAnalysis,
|
|
159
|
+
flattenedStates: FlattenedState[],
|
|
160
|
+
guardDefs: Map<string, GuardDef>
|
|
161
|
+
): VerificationError[] {
|
|
162
|
+
const errors: VerificationError[] = [];
|
|
163
|
+
|
|
164
|
+
const fromRes = resolveStateName(prop.from, flattenedStates);
|
|
165
|
+
if (fromRes.error) return [fromRes.error];
|
|
166
|
+
const toRes = resolveStateName(prop.to, flattenedStates);
|
|
167
|
+
if (toRes.error) return [toRes.error];
|
|
168
|
+
|
|
169
|
+
const { reachable } = bfs(analysis.stateMap, fromRes.resolved, { guardDefs });
|
|
170
|
+
|
|
171
|
+
if (!reachable.has(toRes.resolved)) {
|
|
172
|
+
errors.push({
|
|
173
|
+
code: 'PROPERTY_REACHABILITY_FAIL',
|
|
174
|
+
message: `Property 'reachable: ${prop.to} from ${prop.from}' violated — no path exists from '${fromRes.resolved}' to '${toRes.resolved}'`,
|
|
175
|
+
severity: 'error',
|
|
176
|
+
location: { state: fromRes.resolved },
|
|
177
|
+
suggestion: `Add transitions that create a path from '${fromRes.resolved}' to '${toRes.resolved}'`,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return errors;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function checkUnreachable(
|
|
185
|
+
prop: ReachabilityProperty,
|
|
186
|
+
analysis: MachineAnalysis,
|
|
187
|
+
flattenedStates: FlattenedState[],
|
|
188
|
+
guardDefs: Map<string, GuardDef>
|
|
189
|
+
): VerificationError[] {
|
|
190
|
+
const errors: VerificationError[] = [];
|
|
191
|
+
|
|
192
|
+
const fromRes = resolveStateName(prop.from, flattenedStates);
|
|
193
|
+
if (fromRes.error) return [fromRes.error];
|
|
194
|
+
const toRes = resolveStateName(prop.to, flattenedStates);
|
|
195
|
+
if (toRes.error) return [toRes.error];
|
|
196
|
+
|
|
197
|
+
const { reachable, parent } = bfs(analysis.stateMap, fromRes.resolved, { guardDefs });
|
|
198
|
+
|
|
199
|
+
if (reachable.has(toRes.resolved)) {
|
|
200
|
+
const path = reconstructPath(parent, fromRes.resolved, toRes.resolved);
|
|
201
|
+
const hasGuards = pathHasGuards(path);
|
|
202
|
+
const guardNote = hasGuards
|
|
203
|
+
? ' Note: this path requires guard conditions — it may be prevented at runtime by guards.'
|
|
204
|
+
: '';
|
|
205
|
+
errors.push({
|
|
206
|
+
code: 'PROPERTY_EXCLUSION_FAIL',
|
|
207
|
+
message: `Property 'unreachable: ${prop.to} from ${prop.from}' violated — path exists: ${formatPath(path)}${guardNote}`,
|
|
208
|
+
severity: 'error',
|
|
209
|
+
location: { state: fromRes.resolved },
|
|
210
|
+
suggestion: `Remove transitions that allow reaching '${toRes.resolved}' from '${fromRes.resolved}', or remove this property if the path is intentional.`,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return errors;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function checkPassesThrough(
|
|
218
|
+
prop: PassesThroughProperty,
|
|
219
|
+
analysis: MachineAnalysis,
|
|
220
|
+
flattenedStates: FlattenedState[],
|
|
221
|
+
guardDefs: Map<string, GuardDef>
|
|
222
|
+
): VerificationError[] {
|
|
223
|
+
const errors: VerificationError[] = [];
|
|
224
|
+
|
|
225
|
+
const fromRes = resolveStateName(prop.from, flattenedStates);
|
|
226
|
+
if (fromRes.error) return [fromRes.error];
|
|
227
|
+
const toRes = resolveStateName(prop.to, flattenedStates);
|
|
228
|
+
if (toRes.error) return [toRes.error];
|
|
229
|
+
const throughRes = resolveStateName(prop.through, flattenedStates);
|
|
230
|
+
if (throughRes.error) return [throughRes.error];
|
|
231
|
+
|
|
232
|
+
// First check: is target reachable from source at all?
|
|
233
|
+
const { reachable: fullReachable } = bfs(analysis.stateMap, fromRes.resolved, { guardDefs });
|
|
234
|
+
if (!fullReachable.has(toRes.resolved)) {
|
|
235
|
+
errors.push({
|
|
236
|
+
code: 'PROPERTY_PATH_FAIL',
|
|
237
|
+
message: `Property 'passes_through: ${prop.through} for ${prop.from} -> ${prop.to}' — '${toRes.resolved}' is not reachable from '${fromRes.resolved}' at all`,
|
|
238
|
+
severity: 'error',
|
|
239
|
+
location: { state: fromRes.resolved },
|
|
240
|
+
suggestion: `Ensure '${toRes.resolved}' is reachable from '${fromRes.resolved}' before adding path constraints`,
|
|
241
|
+
});
|
|
242
|
+
return errors;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Core check: remove the intermediate state and see if target is still reachable
|
|
246
|
+
const { reachable: withoutThrough, parent } = bfs(analysis.stateMap, fromRes.resolved, {
|
|
247
|
+
excludeState: throughRes.resolved,
|
|
248
|
+
guardDefs,
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
if (withoutThrough.has(toRes.resolved)) {
|
|
252
|
+
const path = reconstructPath(parent, fromRes.resolved, toRes.resolved);
|
|
253
|
+
errors.push({
|
|
254
|
+
code: 'PROPERTY_PATH_FAIL',
|
|
255
|
+
message: `Property 'passes_through: ${prop.through} for ${prop.from} -> ${prop.to}' violated — path bypassing '${throughRes.resolved}': ${formatPath(path)}`,
|
|
256
|
+
severity: 'error',
|
|
257
|
+
location: { state: fromRes.resolved },
|
|
258
|
+
suggestion: `Ensure all transitions from '${fromRes.resolved}' to '${toRes.resolved}' must pass through '${throughRes.resolved}'. Note: this check ignores guard conditions.`,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return errors;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function checkLive(
|
|
266
|
+
analysis: MachineAnalysis,
|
|
267
|
+
flattenedStates: FlattenedState[],
|
|
268
|
+
guardDefs: Map<string, GuardDef>
|
|
269
|
+
): VerificationError[] {
|
|
270
|
+
const errors: VerificationError[] = [];
|
|
271
|
+
|
|
272
|
+
if (!analysis.initialState) return errors;
|
|
273
|
+
|
|
274
|
+
// Find all reachable states from initial
|
|
275
|
+
const { reachable: reachableFromInitial } = bfs(analysis.stateMap, analysis.initialState.name, { guardDefs });
|
|
276
|
+
|
|
277
|
+
// Find all final state names
|
|
278
|
+
const finalStateNames = new Set(analysis.finalStates.map(s => s.name));
|
|
279
|
+
|
|
280
|
+
// For each reachable non-final leaf state, check if some final state is reachable
|
|
281
|
+
for (const stateName of reachableFromInitial) {
|
|
282
|
+
if (finalStateNames.has(stateName)) continue;
|
|
283
|
+
|
|
284
|
+
// Skip compound/parallel/region states — liveness is about leaf states
|
|
285
|
+
const fs = flattenedStates.find(f => f.name === stateName);
|
|
286
|
+
if (fs && (fs.isCompound || fs.isRegion)) continue;
|
|
287
|
+
|
|
288
|
+
const { reachable: reachableFromState } = bfs(analysis.stateMap, stateName, { guardDefs });
|
|
289
|
+
|
|
290
|
+
let canReachFinal = false;
|
|
291
|
+
for (const finalName of finalStateNames) {
|
|
292
|
+
if (reachableFromState.has(finalName)) {
|
|
293
|
+
canReachFinal = true;
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (!canReachFinal) {
|
|
299
|
+
errors.push({
|
|
300
|
+
code: 'PROPERTY_LIVENESS_FAIL',
|
|
301
|
+
message: `Property 'live' violated — state '${stateName}' cannot reach any final state`,
|
|
302
|
+
severity: 'error',
|
|
303
|
+
location: { state: stateName },
|
|
304
|
+
suggestion: `Add transitions from '${stateName}' that lead to a final state, or mark '${stateName}' as [final]`,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return errors;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function checkResponds(
|
|
313
|
+
prop: RespondsProperty,
|
|
314
|
+
analysis: MachineAnalysis,
|
|
315
|
+
flattenedStates: FlattenedState[],
|
|
316
|
+
guardDefs: Map<string, GuardDef>
|
|
317
|
+
): VerificationError[] {
|
|
318
|
+
const errors: VerificationError[] = [];
|
|
319
|
+
|
|
320
|
+
const fromRes = resolveStateName(prop.from, flattenedStates);
|
|
321
|
+
if (fromRes.error) return [fromRes.error];
|
|
322
|
+
const toRes = resolveStateName(prop.to, flattenedStates);
|
|
323
|
+
if (toRes.error) return [toRes.error];
|
|
324
|
+
|
|
325
|
+
const { reachable } = bfs(analysis.stateMap, fromRes.resolved, {
|
|
326
|
+
maxDepth: prop.within,
|
|
327
|
+
guardDefs,
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
if (!reachable.has(toRes.resolved)) {
|
|
331
|
+
// Check if it's reachable at all (just beyond the bound)
|
|
332
|
+
const { reachable: unbounded } = bfs(analysis.stateMap, fromRes.resolved, { guardDefs });
|
|
333
|
+
const reachableButBeyondBound = unbounded.has(toRes.resolved);
|
|
334
|
+
|
|
335
|
+
const suffix = reachableButBeyondBound
|
|
336
|
+
? ` (reachable beyond ${prop.within} transitions — increase the bound or shorten the path)`
|
|
337
|
+
: ` (not reachable at all from '${fromRes.resolved}')`;
|
|
338
|
+
|
|
339
|
+
errors.push({
|
|
340
|
+
code: 'PROPERTY_RESPONSE_FAIL',
|
|
341
|
+
message: `Property 'responds: ${prop.to} from ${prop.from} within ${prop.within}' violated — '${toRes.resolved}' not reachable within ${prop.within} transitions${suffix}`,
|
|
342
|
+
severity: 'error',
|
|
343
|
+
location: { state: fromRes.resolved },
|
|
344
|
+
suggestion: `Shorten the path from '${fromRes.resolved}' to '${toRes.resolved}' or increase the bound`,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return errors;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function checkInvariant(
|
|
352
|
+
prop: InvariantProperty,
|
|
353
|
+
machine: MachineDef,
|
|
354
|
+
flattenedStates: FlattenedState[]
|
|
355
|
+
): VerificationError[] {
|
|
356
|
+
const errors: VerificationError[] = [];
|
|
357
|
+
|
|
358
|
+
// If a specific state is referenced, validate it exists
|
|
359
|
+
if (prop.inState) {
|
|
360
|
+
const stateRes = resolveStateName(prop.inState, flattenedStates);
|
|
361
|
+
if (stateRes.error) return [stateRes.error];
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Validate that the invariant expression references declared context fields
|
|
365
|
+
const contextFieldNames = new Set(machine.context.map(f => f.name));
|
|
366
|
+
const undeclaredFields = findUndeclaredFields(prop.expression, contextFieldNames);
|
|
367
|
+
|
|
368
|
+
for (const field of undeclaredFields) {
|
|
369
|
+
errors.push({
|
|
370
|
+
code: 'PROPERTY_INVARIANT_INVALID',
|
|
371
|
+
message: `Invariant references undeclared context field '${field}'`,
|
|
372
|
+
severity: 'error',
|
|
373
|
+
suggestion: `Declare '${field}' in the context block or fix the field name`,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (undeclaredFields.length === 0) {
|
|
378
|
+
// Advisory warning — topology-level check cannot prove context invariants
|
|
379
|
+
const stateDesc = prop.inState ? ` in state '${prop.inState}'` : '';
|
|
380
|
+
errors.push({
|
|
381
|
+
code: 'PROPERTY_INVARIANT_ADVISORY',
|
|
382
|
+
message: `Invariant${stateDesc} is syntactically valid but cannot be fully verified at topology level — requires runtime trace simulation`,
|
|
383
|
+
severity: 'warning',
|
|
384
|
+
suggestion: `This invariant will be checked during runtime verification when action implementations are available`,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return errors;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Find context field references in a guard expression that are not declared.
|
|
393
|
+
*/
|
|
394
|
+
function findUndeclaredFields(expr: import('../parser/ast.js').GuardExpression, declared: Set<string>): string[] {
|
|
395
|
+
const undeclared: string[] = [];
|
|
396
|
+
|
|
397
|
+
function walk(e: import('../parser/ast.js').GuardExpression): void {
|
|
398
|
+
switch (e.kind) {
|
|
399
|
+
case 'true':
|
|
400
|
+
case 'false':
|
|
401
|
+
break;
|
|
402
|
+
case 'not':
|
|
403
|
+
walk(e.expr);
|
|
404
|
+
break;
|
|
405
|
+
case 'and':
|
|
406
|
+
case 'or':
|
|
407
|
+
walk(e.left);
|
|
408
|
+
walk(e.right);
|
|
409
|
+
break;
|
|
410
|
+
case 'compare': {
|
|
411
|
+
// Check variable path — first segment after 'ctx' is the field name
|
|
412
|
+
const path = e.left.path;
|
|
413
|
+
if (path.length >= 2 && path[0] === 'ctx') {
|
|
414
|
+
if (!declared.has(path[1])) {
|
|
415
|
+
undeclared.push(path[1]);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
420
|
+
case 'nullcheck': {
|
|
421
|
+
const path = e.expr.path;
|
|
422
|
+
if (path.length >= 2 && path[0] === 'ctx') {
|
|
423
|
+
if (!declared.has(path[1])) {
|
|
424
|
+
undeclared.push(path[1]);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
walk(expr);
|
|
433
|
+
return [...new Set(undeclared)];
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// --- Size limit check ---
|
|
437
|
+
|
|
438
|
+
function checkMachineSize(
|
|
439
|
+
flattenedStates: FlattenedState[],
|
|
440
|
+
maxStates: number
|
|
441
|
+
): VerificationError[] {
|
|
442
|
+
const leafCount = flattenedStates.filter(fs => !fs.isRegion).length;
|
|
443
|
+
if (leafCount > maxStates) {
|
|
444
|
+
return [{
|
|
445
|
+
code: 'MACHINE_TOO_LARGE',
|
|
446
|
+
message: `Machine has ${leafCount} states (limit: ${maxStates}). Decompose into hierarchical states or separate machines communicating via events.`,
|
|
447
|
+
severity: 'error',
|
|
448
|
+
suggestion: `Split the machine into smaller composed machines. Each machine should stay under ${maxStates} states for verifiable complexity.`,
|
|
449
|
+
}];
|
|
450
|
+
}
|
|
451
|
+
return [];
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// --- Main entry point ---
|
|
455
|
+
|
|
456
|
+
export function checkProperties(machine: MachineDef, options?: { maxStates?: number }): VerificationResult {
|
|
457
|
+
const maxStates = options?.maxStates ?? DEFAULT_MAX_STATES;
|
|
458
|
+
const flattenedStates = flattenStates(machine.states);
|
|
459
|
+
const errors: VerificationError[] = [];
|
|
460
|
+
|
|
461
|
+
// Size limit check (always runs, even without properties block)
|
|
462
|
+
errors.push(...checkMachineSize(flattenedStates, maxStates));
|
|
463
|
+
if (errors.some(e => e.severity === 'error')) {
|
|
464
|
+
return { valid: false, errors };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// If no properties defined, pass
|
|
468
|
+
if (!machine.properties || machine.properties.length === 0) {
|
|
469
|
+
return { valid: true, errors };
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const analysis = analyzeMachine(machine);
|
|
473
|
+
|
|
474
|
+
// Build guard definition map for guard-aware BFS
|
|
475
|
+
const guardDefs = new Map<string, GuardDef>();
|
|
476
|
+
for (const g of machine.guards) {
|
|
477
|
+
guardDefs.set(g.name, g);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
for (const prop of machine.properties) {
|
|
481
|
+
switch (prop.kind) {
|
|
482
|
+
case 'reachable':
|
|
483
|
+
errors.push(...checkReachable(prop, analysis, flattenedStates, guardDefs));
|
|
484
|
+
break;
|
|
485
|
+
case 'unreachable':
|
|
486
|
+
errors.push(...checkUnreachable(prop, analysis, flattenedStates, guardDefs));
|
|
487
|
+
break;
|
|
488
|
+
case 'passes_through':
|
|
489
|
+
errors.push(...checkPassesThrough(prop, analysis, flattenedStates, guardDefs));
|
|
490
|
+
break;
|
|
491
|
+
case 'live':
|
|
492
|
+
errors.push(...checkLive(analysis, flattenedStates, guardDefs));
|
|
493
|
+
break;
|
|
494
|
+
case 'responds':
|
|
495
|
+
errors.push(...checkResponds(prop, analysis, flattenedStates, guardDefs));
|
|
496
|
+
break;
|
|
497
|
+
case 'invariant':
|
|
498
|
+
errors.push(...checkInvariant(prop, machine, flattenedStates));
|
|
499
|
+
break;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return {
|
|
504
|
+
valid: errors.filter(e => e.severity === 'error').length === 0,
|
|
505
|
+
errors,
|
|
506
|
+
};
|
|
507
|
+
}
|