@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,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
+ }