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