@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
package/src/tools.ts ADDED
@@ -0,0 +1,144 @@
1
+ /** Shared Orca tool descriptors — used by the CLI (--tools --json) and the MCP server. */
2
+
3
+ export interface ToolInputSchema {
4
+ type: 'object';
5
+ properties: Record<string, { type: string; description?: string; enum?: string[]; items?: Record<string, unknown> }>;
6
+ required: string[];
7
+ }
8
+
9
+ export interface ToolDef {
10
+ name: string;
11
+ description: string;
12
+ inputSchema: ToolInputSchema;
13
+ }
14
+
15
+ export const ORCA_TOOLS: ToolDef[] = [
16
+ {
17
+ name: 'parse_machine',
18
+ description:
19
+ 'Parse an Orca machine definition and return its structure as JSON (states, events, transitions, guards, actions, context). Supports single and multi-machine files.',
20
+ inputSchema: {
21
+ type: 'object',
22
+ properties: {
23
+ source: { type: 'string', description: 'Raw .orca.md content' },
24
+ },
25
+ required: ['source'],
26
+ },
27
+ },
28
+ {
29
+ name: 'verify_machine',
30
+ description:
31
+ 'Verify an Orca machine definition for structural correctness, completeness, and determinism. Returns structured errors and warnings.',
32
+ inputSchema: {
33
+ type: 'object',
34
+ properties: {
35
+ source: { type: 'string', description: 'Raw .orca.md content' },
36
+ },
37
+ required: ['source'],
38
+ },
39
+ },
40
+ {
41
+ name: 'compile_machine',
42
+ description:
43
+ 'Compile an Orca machine to XState v5 config (TypeScript) or Mermaid stateDiagram-v2.',
44
+ inputSchema: {
45
+ type: 'object',
46
+ properties: {
47
+ source: { type: 'string', description: 'Raw .orca.md content' },
48
+ target: {
49
+ type: 'string',
50
+ enum: ['xstate', 'mermaid'],
51
+ description: 'Compilation target (default: xstate)',
52
+ },
53
+ },
54
+ required: ['source'],
55
+ },
56
+ },
57
+ {
58
+ name: 'generate_machine',
59
+ description:
60
+ 'Generate an Orca machine definition from a natural language specification. Requires LLM configuration via environment variables (ANTHROPIC_API_KEY, etc.). Loops up to max_iterations to produce a valid machine.',
61
+ inputSchema: {
62
+ type: 'object',
63
+ properties: {
64
+ spec: {
65
+ type: 'string',
66
+ description: 'Natural language description of the desired state machine',
67
+ },
68
+ max_iterations: {
69
+ type: 'number',
70
+ description: 'Maximum refinement iterations (default: 3)',
71
+ },
72
+ },
73
+ required: ['spec'],
74
+ },
75
+ },
76
+ {
77
+ name: 'generate_actions',
78
+ description:
79
+ 'Generate action scaffold code for an Orca machine in TypeScript, Python, or Go. Includes registration comments and optional test scaffolds.',
80
+ inputSchema: {
81
+ type: 'object',
82
+ properties: {
83
+ source: { type: 'string', description: 'Raw .orca.md content' },
84
+ lang: {
85
+ type: 'string',
86
+ enum: ['typescript', 'python', 'go'],
87
+ description: 'Target language (default: typescript)',
88
+ },
89
+ use_llm: {
90
+ type: 'boolean',
91
+ description: 'Use LLM to generate implementations instead of templates (default: false)',
92
+ },
93
+ generate_tests: {
94
+ type: 'boolean',
95
+ description: 'Include test scaffolds in the output (default: false)',
96
+ },
97
+ },
98
+ required: ['source'],
99
+ },
100
+ },
101
+ {
102
+ name: 'generate_multi_machine',
103
+ description:
104
+ 'Generate a coordinated set of Orca machines from a natural language specification. Returns multiple machine definitions in one .orca.md file (separated by ---) that pass the cross-machine verifier. Requires LLM configuration via environment variables.',
105
+ inputSchema: {
106
+ type: 'object',
107
+ properties: {
108
+ spec: {
109
+ type: 'string',
110
+ description: 'Natural language description of the desired multi-machine system',
111
+ },
112
+ max_iterations: {
113
+ type: 'number',
114
+ description: 'Maximum refinement iterations (default: 3)',
115
+ },
116
+ },
117
+ required: ['spec'],
118
+ },
119
+ },
120
+ {
121
+ name: 'refine_machine',
122
+ description:
123
+ 'Fix verification errors in an Orca machine using an LLM. Loops until the machine is valid or max_iterations is reached. If errors are not provided, verification runs automatically first.',
124
+ inputSchema: {
125
+ type: 'object',
126
+ properties: {
127
+ source: { type: 'string', description: 'Raw .orca.md content with errors' },
128
+ errors: {
129
+ type: 'array',
130
+ description:
131
+ 'Verification errors from verify_machine. If omitted, verification runs automatically.',
132
+ items: {
133
+ type: 'object',
134
+ },
135
+ },
136
+ max_iterations: {
137
+ type: 'number',
138
+ description: 'Maximum refinement iterations (default: 3)',
139
+ },
140
+ },
141
+ required: ['source'],
142
+ },
143
+ },
144
+ ];
@@ -0,0 +1,89 @@
1
+ import { MachineDef, Transition, StateDef } from '../parser/ast.js';
2
+ import { VerificationResult, VerificationError, analyzeMachine, flattenStates, FlattenedState } from './structural.js';
3
+
4
+ export function checkCompleteness(machine: MachineDef): VerificationResult {
5
+ const analysis = analyzeMachine(machine);
6
+ const errors: VerificationError[] = [];
7
+
8
+ // Build a map of (state, event) -> transitions
9
+ const transitionMap = new Map<string, Transition[]>();
10
+
11
+ for (const transition of machine.transitions) {
12
+ const key = `${transition.source}+${transition.event}`;
13
+ if (!transitionMap.has(key)) {
14
+ transitionMap.set(key, []);
15
+ }
16
+ transitionMap.get(key)!.push(transition);
17
+ }
18
+
19
+ // Flatten states for hierarchical handling
20
+ const flattenedStates = flattenStates(machine.states);
21
+
22
+ // Build a map of compound state name -> its child state names
23
+ const compoundChildren = new Map<string, string[]>();
24
+ // Map from dot-notation name to simple name for transition lookups
25
+ const simpleNameMap = new Map<string, string>();
26
+ for (const fs of flattenedStates) {
27
+ simpleNameMap.set(fs.name, fs.simpleName);
28
+ if (fs.parentName) {
29
+ if (!compoundChildren.has(fs.parentName)) {
30
+ compoundChildren.set(fs.parentName, []);
31
+ }
32
+ compoundChildren.get(fs.parentName)!.push(fs.name);
33
+ }
34
+ }
35
+
36
+ // Check every state handles every event
37
+ for (const fs of flattenedStates) {
38
+ // Skip child states - they're covered by parent compound state checks
39
+ // (transitions on compound state fire from any child)
40
+ if (fs.parentName) continue;
41
+
42
+ // For compound states, we check if ANY child handles the event
43
+ // For leaf states, we check directly
44
+ const isHandledForEvent = (stateName: string, eventName: string): boolean => {
45
+ // Check direct transitions from this state (both full and simple name)
46
+ const directKey = `${stateName}+${eventName}`;
47
+ if (transitionMap.has(directKey)) return true;
48
+ const simple = simpleNameMap.get(stateName);
49
+ if (simple && simple !== stateName) {
50
+ const simpleKey = `${simple}+${eventName}`;
51
+ if (transitionMap.has(simpleKey)) return true;
52
+ }
53
+
54
+ // For compound states, also check if any child has a transition for this event
55
+ const children = compoundChildren.get(stateName);
56
+ if (children) {
57
+ for (const child of children) {
58
+ if (isHandledForEvent(child, eventName)) return true;
59
+ }
60
+ }
61
+ return false;
62
+ };
63
+
64
+ const stateInfo = analysis.stateMap.get(fs.name);
65
+ if (!stateInfo) continue;
66
+
67
+ for (const event of machine.events) {
68
+ const transitions = transitionMap.get(`${fs.name}+${event.name}`) || [];
69
+ const isIgnored = stateInfo.eventsIgnored.has(event.name);
70
+ const hasHandler = isHandledForEvent(fs.name, event.name);
71
+
72
+ // Event is not handled and not ignored
73
+ if (!hasHandler && !isIgnored && !fs.isFinal) {
74
+ errors.push({
75
+ code: 'INCOMPLETE_EVENT_HANDLING',
76
+ message: `State '${fs.name}' does not handle event '${event.name}'`,
77
+ severity: 'error',
78
+ location: { state: fs.name, event: event.name },
79
+ suggestion: `Add transition: ${fs.name} + ${event.name} -> <target> : <action>`,
80
+ });
81
+ }
82
+ }
83
+ }
84
+
85
+ return {
86
+ valid: errors.length === 0,
87
+ errors,
88
+ };
89
+ }
@@ -0,0 +1,328 @@
1
+ import { MachineDef, Transition, GuardRef, GuardDef, GuardExpression, ComparisonOp, VariableRef, ValueRef } from '../parser/ast.js';
2
+ import { VerificationResult, VerificationError } from './structural.js';
3
+
4
+ export function checkDeterminism(machine: MachineDef): VerificationResult {
5
+ const errors: VerificationError[] = [];
6
+
7
+ // Build guard lookup by name
8
+ const guardDefMap = new Map<string, GuardDef>();
9
+ for (const g of machine.guards) {
10
+ guardDefMap.set(g.name, g);
11
+ }
12
+
13
+ // Build a map of (state, event) -> transitions with guards
14
+ const transitionMap = new Map<string, Transition[]>();
15
+
16
+ for (const transition of machine.transitions) {
17
+ const key = `${transition.source}+${transition.event}`;
18
+ if (!transitionMap.has(key)) {
19
+ transitionMap.set(key, []);
20
+ }
21
+ transitionMap.get(key)!.push(transition);
22
+ }
23
+
24
+ // Check each (state, event) pair
25
+ for (const [key, transitions] of transitionMap) {
26
+ if (transitions.length <= 1) continue;
27
+
28
+ // Multiple transitions for same state+event
29
+ const guards = transitions.map(t => t.guard);
30
+ const hasUnguarded = guards.some(g => !g);
31
+
32
+ // If there are multiple unguarded transitions, that's always an error
33
+ if (hasUnguarded && guards.filter(g => !g).length > 1) {
34
+ const [stateName, eventName] = key.split('+');
35
+ errors.push({
36
+ code: 'NON_DETERMINISTIC',
37
+ message: `State '${stateName}' has multiple unguarded transitions for event '${eventName}'`,
38
+ severity: 'error',
39
+ location: {
40
+ state: stateName,
41
+ event: eventName,
42
+ },
43
+ suggestion: 'Add guards to make transitions mutually exclusive',
44
+ });
45
+ }
46
+
47
+ // Check mutual exclusivity of guards
48
+ const guardedTransitions = transitions.filter(t => t.guard);
49
+ if (guardedTransitions.length > 1) {
50
+ const guardNames = guardedTransitions.map(t => {
51
+ const g = t.guard!;
52
+ return g.negated ? `!${g.name}` : g.name;
53
+ });
54
+
55
+ const mutuallyExclusive = areGuardsMutuallyExclusive(
56
+ guardedTransitions.map(t => t.guard!),
57
+ guardDefMap,
58
+ );
59
+
60
+ if (!mutuallyExclusive) {
61
+ const [stateName, eventName] = key.split('+');
62
+ errors.push({
63
+ code: 'GUARD_EXHAUSTIVENESS',
64
+ message: `State '${stateName}' transitions for event '${eventName}' may not be exhaustive: ${guardNames.join(', ')}`,
65
+ severity: 'warning',
66
+ location: { state: stateName, event: eventName },
67
+ suggestion: 'Ensure guards cover all possible context values',
68
+ });
69
+ }
70
+ }
71
+ }
72
+
73
+ return {
74
+ valid: errors.filter(e => e.severity === 'error').length === 0,
75
+ errors,
76
+ };
77
+ }
78
+
79
+ /**
80
+ * Check if a set of guard references are pairwise mutually exclusive.
81
+ * Uses multiple strategies:
82
+ * 1. Simple negation pairs (g and !g)
83
+ * 2. Expression-level complementary analysis
84
+ */
85
+ function areGuardsMutuallyExclusive(
86
+ guardRefs: GuardRef[],
87
+ guardDefs: Map<string, GuardDef>,
88
+ ): boolean {
89
+ // Strategy 1: Simple name-based negation pairs (g and !g)
90
+ const hasSimpleNegationPair = guardRefs.some(g1 =>
91
+ guardRefs.some(g2 => {
92
+ if (g1 === g2) return false;
93
+ return g1.name === g2.name && g1.negated !== g2.negated;
94
+ })
95
+ );
96
+ if (hasSimpleNegationPair) return true;
97
+
98
+ // Strategy 2: Resolve to expressions and check pairwise exclusivity
99
+ const resolvedExprs = guardRefs.map(ref => resolveGuardExpression(ref, guardDefs));
100
+
101
+ // If any guard couldn't be resolved, we can't verify — assume OK
102
+ if (resolvedExprs.some(e => e === null)) return true;
103
+
104
+ // Check all pairs are mutually exclusive
105
+ for (let i = 0; i < resolvedExprs.length; i++) {
106
+ for (let j = i + 1; j < resolvedExprs.length; j++) {
107
+ if (areExpressionsMutuallyExclusive(resolvedExprs[i]!, resolvedExprs[j]!)) {
108
+ return true;
109
+ }
110
+ }
111
+ }
112
+
113
+ return false;
114
+ }
115
+
116
+ /**
117
+ * Resolve a GuardRef (name + negated flag) into its full GuardExpression.
118
+ */
119
+ export function resolveGuardExpression(
120
+ ref: GuardRef,
121
+ guardDefs: Map<string, GuardDef>,
122
+ ): GuardExpression | null {
123
+ const def = guardDefs.get(ref.name);
124
+ if (!def) return null;
125
+
126
+ if (ref.negated) {
127
+ return { kind: 'not', expr: def.expression };
128
+ }
129
+ return def.expression;
130
+ }
131
+
132
+ /**
133
+ * Check if two guard expressions are mutually exclusive (can never both be true).
134
+ */
135
+ export function areExpressionsMutuallyExclusive(a: GuardExpression, b: GuardExpression): boolean {
136
+ // Unwrap negation: a and not(a) are exclusive
137
+ const aNorm = unwrapNot(a);
138
+ const bNorm = unwrapNot(b);
139
+
140
+ // If one is the negation of the other (structurally equal after unwrapping)
141
+ if (aNorm.negated !== bNorm.negated && expressionsEqual(aNorm.expr, bNorm.expr)) {
142
+ return true;
143
+ }
144
+
145
+ // true vs false
146
+ if (a.kind === 'true' && b.kind === 'false') return true;
147
+ if (a.kind === 'false' && b.kind === 'true') return true;
148
+
149
+ // Complementary comparisons on the same variable
150
+ if (a.kind === 'compare' && b.kind === 'compare') {
151
+ if (variablePathsEqual(a.left, b.left)) {
152
+ return areComparisonsExclusive(a.op, a.right, b.op, b.right);
153
+ }
154
+ }
155
+
156
+ // Complementary nullchecks on the same variable
157
+ if (a.kind === 'nullcheck' && b.kind === 'nullcheck') {
158
+ if (variablePathsEqual(a.expr, b.expr) && a.isNull !== b.isNull) {
159
+ return true;
160
+ }
161
+ }
162
+
163
+ // Compare vs nullcheck on same variable: ctx.x == value vs ctx.x is null
164
+ if (a.kind === 'compare' && b.kind === 'nullcheck') {
165
+ if (variablePathsEqual(a.left, b.expr) && b.isNull) {
166
+ return true; // if ctx.x equals a concrete value, it's not null
167
+ }
168
+ }
169
+ if (b.kind === 'compare' && a.kind === 'nullcheck') {
170
+ if (variablePathsEqual(b.left, a.expr) && a.isNull) {
171
+ return true;
172
+ }
173
+ }
174
+
175
+ // not(E) vs E at expression level
176
+ if (a.kind === 'not') {
177
+ if (areExpressionsMutuallyExclusive(a.expr, b)) return false; // don't recurse infinitely
178
+ if (expressionsEqual(a.expr, b)) return true;
179
+ }
180
+ if (b.kind === 'not') {
181
+ if (expressionsEqual(b.expr, a)) return true;
182
+ }
183
+
184
+ return false;
185
+ }
186
+
187
+ interface UnwrappedExpr {
188
+ expr: GuardExpression;
189
+ negated: boolean;
190
+ }
191
+
192
+ /**
193
+ * Unwrap layers of NOT to get the core expression and parity.
194
+ */
195
+ function unwrapNot(expr: GuardExpression): UnwrappedExpr {
196
+ let negated = false;
197
+ let current = expr;
198
+ while (current.kind === 'not') {
199
+ negated = !negated;
200
+ current = current.expr;
201
+ }
202
+ return { expr: current, negated };
203
+ }
204
+
205
+ /**
206
+ * Check if two comparison operations on the same variable are mutually exclusive.
207
+ */
208
+ function areComparisonsExclusive(
209
+ op1: ComparisonOp, val1: ValueRef,
210
+ op2: ComparisonOp, val2: ValueRef,
211
+ ): boolean {
212
+ // Different constant values with == are always exclusive
213
+ if (op1 === 'eq' && op2 === 'eq') {
214
+ return !valuesEqual(val1, val2);
215
+ }
216
+
217
+ // eq vs ne on same value are exclusive
218
+ if ((op1 === 'eq' && op2 === 'ne') || (op1 === 'ne' && op2 === 'eq')) {
219
+ if (valuesEqual(val1, val2)) return true;
220
+ }
221
+
222
+ // Complementary inequality pairs on same value: < vs >=, > vs <=, lt vs ge, gt vs le
223
+ if (valuesEqual(val1, val2)) {
224
+ const complementaryPairs: [ComparisonOp, ComparisonOp][] = [
225
+ ['lt', 'ge'],
226
+ ['ge', 'lt'],
227
+ ['gt', 'le'],
228
+ ['le', 'gt'],
229
+ ];
230
+ for (const [a, b] of complementaryPairs) {
231
+ if (op1 === a && op2 === b) return true;
232
+ }
233
+ }
234
+
235
+ // Numeric range exclusion: ctx.x < 3 vs ctx.x > 5 (always exclusive)
236
+ // ctx.x < A vs ctx.x > B where A <= B
237
+ if (val1.type === 'number' && val2.type === 'number') {
238
+ const v1 = val1.value as number;
239
+ const v2 = val2.value as number;
240
+
241
+ // lt/le A vs gt/ge B where ranges don't overlap
242
+ if ((op1 === 'lt' || op1 === 'le') && (op2 === 'gt' || op2 === 'ge')) {
243
+ if (op1 === 'lt' && op2 === 'ge' && v1 <= v2) return true;
244
+ if (op1 === 'lt' && op2 === 'gt' && v1 <= v2) return true;
245
+ if (op1 === 'le' && op2 === 'gt' && v1 <= v2) return true;
246
+ if (op1 === 'le' && op2 === 'ge' && v1 < v2) return true;
247
+ }
248
+ if ((op2 === 'lt' || op2 === 'le') && (op1 === 'gt' || op1 === 'ge')) {
249
+ if (op2 === 'lt' && op1 === 'ge' && v2 <= v1) return true;
250
+ if (op2 === 'lt' && op1 === 'gt' && v2 <= v1) return true;
251
+ if (op2 === 'le' && op1 === 'gt' && v2 <= v1) return true;
252
+ if (op2 === 'le' && op1 === 'ge' && v2 < v1) return true;
253
+ }
254
+
255
+ // eq vs lt/gt: ctx.x == 5 vs ctx.x < 3 (exclusive if 5 >= 3)
256
+ if (op1 === 'eq' && (op2 === 'lt' && v1 >= v2)) return true;
257
+ if (op1 === 'eq' && (op2 === 'le' && v1 > v2)) return true;
258
+ if (op1 === 'eq' && (op2 === 'gt' && v1 <= v2)) return true;
259
+ if (op1 === 'eq' && (op2 === 'ge' && v1 < v2)) return true;
260
+ if (op2 === 'eq' && (op1 === 'lt' && v2 >= v1)) return true;
261
+ if (op2 === 'eq' && (op1 === 'le' && v2 > v1)) return true;
262
+ if (op2 === 'eq' && (op1 === 'gt' && v2 <= v1)) return true;
263
+ if (op2 === 'eq' && (op1 === 'ge' && v2 < v1)) return true;
264
+ }
265
+
266
+ return false;
267
+ }
268
+
269
+ /**
270
+ * Structural equality of two guard expressions.
271
+ */
272
+ function expressionsEqual(a: GuardExpression, b: GuardExpression): boolean {
273
+ if (a.kind !== b.kind) return false;
274
+
275
+ switch (a.kind) {
276
+ case 'true':
277
+ case 'false':
278
+ return true;
279
+ case 'not':
280
+ return expressionsEqual(a.expr, (b as typeof a).expr);
281
+ case 'and':
282
+ case 'or':
283
+ return expressionsEqual(a.left, (b as typeof a).left) &&
284
+ expressionsEqual(a.right, (b as typeof a).right);
285
+ case 'compare': {
286
+ const bc = b as typeof a;
287
+ return a.op === bc.op &&
288
+ variablePathsEqual(a.left, bc.left) &&
289
+ valuesEqual(a.right, bc.right);
290
+ }
291
+ case 'nullcheck': {
292
+ const bn = b as typeof a;
293
+ return a.isNull === bn.isNull && variablePathsEqual(a.expr, bn.expr);
294
+ }
295
+ default:
296
+ return false;
297
+ }
298
+ }
299
+
300
+ function variablePathsEqual(a: VariableRef, b: VariableRef): boolean {
301
+ if (a.path.length !== b.path.length) return false;
302
+ return a.path.every((p, i) => p === b.path[i]);
303
+ }
304
+
305
+ function valuesEqual(a: ValueRef, b: ValueRef): boolean {
306
+ return a.type === b.type && a.value === b.value;
307
+ }
308
+
309
+ /**
310
+ * Check if a guard expression is statically always false.
311
+ * Detects: literal false, AND(x, not(x)), numeric contradictions.
312
+ */
313
+ export function isExpressionStaticallyFalse(expr: GuardExpression): boolean {
314
+ if (expr.kind === 'false') return true;
315
+
316
+ // AND with contradictory branches: (A and not(A))
317
+ if (expr.kind === 'and') {
318
+ if (areExpressionsMutuallyExclusive(expr.left, expr.right)) return true;
319
+ // Either branch being false makes the whole thing false
320
+ if (isExpressionStaticallyFalse(expr.left)) return true;
321
+ if (isExpressionStaticallyFalse(expr.right)) return true;
322
+ }
323
+
324
+ // not(true) = false
325
+ if (expr.kind === 'not' && expr.expr.kind === 'true') return true;
326
+
327
+ return false;
328
+ }