@orcalang/orca-lang 0.1.21 → 0.1.27

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 (43) hide show
  1. package/dist/compiler/dt-compiler.d.ts +1 -0
  2. package/dist/compiler/dt-compiler.d.ts.map +1 -1
  3. package/dist/compiler/dt-compiler.js +160 -3
  4. package/dist/compiler/dt-compiler.js.map +1 -1
  5. package/dist/compiler/xstate.d.ts.map +1 -1
  6. package/dist/compiler/xstate.js +13 -1
  7. package/dist/compiler/xstate.js.map +1 -1
  8. package/dist/health-check.js +75 -0
  9. package/dist/health-check.js.map +1 -1
  10. package/dist/parser/dt-ast.d.ts +11 -1
  11. package/dist/parser/dt-ast.d.ts.map +1 -1
  12. package/dist/parser/dt-parser.d.ts.map +1 -1
  13. package/dist/parser/dt-parser.js +40 -8
  14. package/dist/parser/dt-parser.js.map +1 -1
  15. package/dist/parser/markdown-parser.d.ts.map +1 -1
  16. package/dist/parser/markdown-parser.js +6 -0
  17. package/dist/parser/markdown-parser.js.map +1 -1
  18. package/dist/skills.d.ts +2 -2
  19. package/dist/skills.d.ts.map +1 -1
  20. package/dist/skills.js +200 -6
  21. package/dist/skills.js.map +1 -1
  22. package/dist/tools.js +4 -4
  23. package/dist/tools.js.map +1 -1
  24. package/dist/verifier/determinism.js +5 -5
  25. package/dist/verifier/determinism.js.map +1 -1
  26. package/dist/verifier/dt-verifier.d.ts.map +1 -1
  27. package/dist/verifier/dt-verifier.js +259 -31
  28. package/dist/verifier/dt-verifier.js.map +1 -1
  29. package/dist/verifier/structural.d.ts.map +1 -1
  30. package/dist/verifier/structural.js +11 -5
  31. package/dist/verifier/structural.js.map +1 -1
  32. package/package.json +1 -1
  33. package/src/compiler/dt-compiler.ts +160 -3
  34. package/src/compiler/xstate.ts +15 -1
  35. package/src/health-check.ts +79 -0
  36. package/src/parser/dt-ast.ts +4 -2
  37. package/src/parser/dt-parser.ts +46 -8
  38. package/src/parser/markdown-parser.ts +6 -0
  39. package/src/skills.ts +201 -7
  40. package/src/tools.ts +4 -4
  41. package/src/verifier/determinism.ts +5 -5
  42. package/src/verifier/dt-verifier.ts +272 -29
  43. package/src/verifier/structural.ts +12 -5
package/src/skills.ts CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  compileDecisionTableToTypeScript,
14
14
  compileDecisionTableToPython,
15
15
  compileDecisionTableToGo,
16
+ compileDecisionTableToRust,
16
17
  compileDecisionTableToJSON,
17
18
  toSnakeCase,
18
19
  } from './compiler/dt-compiler.js';
@@ -405,6 +406,8 @@ export async function generateActionsSkill(
405
406
  decisionTableCode[dt.name] = compileDecisionTableToPython(dt);
406
407
  } else if (language === 'go') {
407
408
  decisionTableCode[dt.name] = compileDecisionTableToGo(dt);
409
+ } else if (language === 'rust') {
410
+ decisionTableCode[dt.name] = compileDecisionTableToRust(dt);
408
411
  } else {
409
412
  decisionTableCode[dt.name] = compileDecisionTableToTypeScript(dt);
410
413
  }
@@ -578,6 +581,9 @@ function generateTemplateTestsForAction(action: ActionScaffold, machine: Machine
578
581
  if (language === 'go') {
579
582
  return generateGoTestScaffold(action, machine);
580
583
  }
584
+ if (language === 'rust') {
585
+ return generateRustTestScaffold(action, machine);
586
+ }
581
587
  return generateTypeScriptTestScaffold(action, machine);
582
588
  }
583
589
 
@@ -762,6 +768,63 @@ ${preserved.map(f => `\tif result["${f}"] != ctx["${f}"] {\n\t\tt.Errorf("${f}:
762
768
  `;
763
769
  }
764
770
 
771
+ function generateRustTestScaffold(action: ActionScaffold, machine: MachineDef): string {
772
+ const ctxFields = machine.context.map(f => {
773
+ return ` "${f.name}": ${getDefaultValueForType(f.type, 'rust')}`;
774
+ }).join(',\n');
775
+ const contextUsed = action.context_used.length > 0 ? action.context_used : machine.context.map(f => f.name);
776
+ const preserved = contextUsed.filter(f => !actionModifiesField(action, f));
777
+
778
+ if (action.hasEffect) {
779
+ return `// Tests for ${action.name}
780
+ #[cfg(test)]
781
+ mod tests {
782
+ use super::*;
783
+ use serde_json::json;
784
+
785
+ #[test]
786
+ fn test_${action.name}_executes_effect() {
787
+ let ctx = json!({
788
+ ${ctxFields}
789
+ });
790
+ let event = json!({"type": "test"});
791
+ let (result, effect) = ${action.name}(&ctx, &event);
792
+ assert!(result.is_object());
793
+ assert_eq!(effect.effect_type, "${action.effectType}");
794
+ }
795
+ }
796
+ `;
797
+ }
798
+
799
+ return `// Tests for ${action.name}
800
+ #[cfg(test)]
801
+ mod tests {
802
+ use super::*;
803
+ use serde_json::json;
804
+
805
+ #[test]
806
+ fn test_${action.name}_transforms_context() {
807
+ let ctx = json!({
808
+ ${ctxFields}
809
+ });
810
+ let event = json!({"type": "test"});
811
+ let result = ${action.name}(&ctx, &event);
812
+ assert!(result.is_object());
813
+ }
814
+
815
+ #[test]
816
+ fn test_${action.name}_preserves_fields() {
817
+ let ctx = json!({
818
+ ${ctxFields}
819
+ });
820
+ let event = json!({"type": "test"});
821
+ let result = ${action.name}(&ctx, &event);
822
+ ${preserved.map(f => ` assert_eq!(result["${f}"], ctx["${f}"]);`).join('\n')}
823
+ }
824
+ }
825
+ `;
826
+ }
827
+
765
828
  function actionModifiesField(action: ActionScaffold, fieldName: string): boolean {
766
829
  // Heuristic: if the action name suggests modification of a field, it likely modifies it
767
830
  const modifiers = ['increment', 'decrement', 'set', 'update', 'add', 'remove', 'clear', 'reset', 'toggle'];
@@ -783,7 +846,7 @@ function actionModifiesField(action: ActionScaffold, fieldName: string): boolean
783
846
 
784
847
  function getDefaultValueForType(type: any, language: string = 'typescript'): string {
785
848
  if (!type) {
786
- return language === 'python' ? '""' : language === 'go' ? '""' : "''";
849
+ return language === 'python' ? '""' : language === 'go' ? '""' : language === 'rust' ? '"".to_string()' : "''";
787
850
  }
788
851
  if (typeof type === 'object' && 'kind' in type) {
789
852
  if (language === 'python') {
@@ -808,6 +871,22 @@ function getDefaultValueForType(type: any, language: string = 'typescript'): str
808
871
  case 'map': return 'nil';
809
872
  case 'custom': return 'nil';
810
873
  }
874
+ } else if (language === 'rust') {
875
+ switch (type.kind) {
876
+ case 'string': return '"".to_string()';
877
+ case 'int': return '0';
878
+ case 'decimal': return '0.0';
879
+ case 'bool': return 'false';
880
+ case 'optional': return 'null';
881
+ case 'array': return 'vec![]';
882
+ case 'map': return 'HashMap::new()';
883
+ case 'custom': {
884
+ // Handle common type aliases
885
+ if (type.name === 'float' || type.name === 'double') return '0.0';
886
+ if (type.name === 'integer' || type.name === 'long') return '0';
887
+ return 'null';
888
+ }
889
+ }
811
890
  } else {
812
891
  switch (type.kind) {
813
892
  case 'string': return "''";
@@ -821,7 +900,7 @@ function getDefaultValueForType(type: any, language: string = 'typescript'): str
821
900
  }
822
901
  }
823
902
  }
824
- return language === 'python' ? 'None' : language === 'go' ? 'nil' : 'null';
903
+ return language === 'python' ? 'None' : language === 'go' ? 'nil' : language === 'rust' ? 'null' : 'null';
825
904
  }
826
905
 
827
906
  function extractContextFields(machine: MachineDef, actionName: string): string[] {
@@ -923,6 +1002,29 @@ function generateDTCallComment(dt: DecisionTableDef, language: string): string {
923
1002
  ].join('\n');
924
1003
  }
925
1004
 
1005
+ if (language === 'rust') {
1006
+ const fnName = `evaluate_${toSnakeCase(dtName)}`;
1007
+ const inputStruct = `${toPascalCase(dtName)}Input`;
1008
+ const inputArgs = dt.conditions.map(c => {
1009
+ const hint = c.type === 'enum' && c.values.length > 0
1010
+ ? `String (${c.values.join(', ')})`
1011
+ : c.type === 'bool' ? 'bool' : c.type === 'int_range' ? 'i64' : 'String';
1012
+ return ` // ${c.name}: ..., // ${hint} — map from ctx`;
1013
+ }).join('\n');
1014
+ const outputFields = dt.actions.map(a =>
1015
+ ` // result["${a.name}"] = Value::String(dt_result.${a.name}.clone());`
1016
+ ).join('\n');
1017
+ return [
1018
+ ` // Call ${fnName} to evaluate ${dtName} rules:`,
1019
+ ` // let dt_input = ${inputStruct} {`,
1020
+ inputArgs,
1021
+ ` // };`,
1022
+ ` // if let Some(dt_result) = ${fnName}(&dt_input) {`,
1023
+ outputFields,
1024
+ ` // }`,
1025
+ ].join('\n');
1026
+ }
1027
+
926
1028
  // TypeScript (default)
927
1029
  const fnName = `evaluate${toPascalCase(dtName)}`;
928
1030
  const inputType = `${toPascalCase(dtName)}Input`;
@@ -965,6 +1067,14 @@ function goCtxRead(fieldName: string, condType: string): string {
965
1067
  return `ctx["${fieldName}"].(string)`;
966
1068
  }
967
1069
 
1070
+ /** Generate a Rust serde_json value extraction for reading a context value. */
1071
+ function rustCtxRead(fieldName: string, condType: string): string {
1072
+ if (condType === 'bool') return `ctx["${fieldName}"].as_bool().unwrap_or_default()`;
1073
+ if (condType === 'int_range') return `ctx["${fieldName}"].as_i64().unwrap_or_default()`;
1074
+ if (condType === 'decimal_range') return `ctx["${fieldName}"].as_f64().unwrap_or_default()`;
1075
+ return `ctx["${fieldName}"].as_str().unwrap_or_default().to_string()`;
1076
+ }
1077
+
968
1078
  function generateFullyWiredActionScaffold(
969
1079
  action: { name: string; parameters: string[]; returnType: string; hasEffect: boolean; effectType?: string },
970
1080
  machine: MachineDef,
@@ -1035,6 +1145,45 @@ ${outputAssigns}
1035
1145
  `;
1036
1146
  }
1037
1147
 
1148
+ if (language === 'rust') {
1149
+ const dtFnName = `evaluate_${toSnakeCase(dt.name)}`;
1150
+ const inputStruct = `${toPascalCase(dt.name)}Input`;
1151
+ const ctxReads = dt.conditions
1152
+ .map(c => ` let ${c.name} = ${rustCtxRead(c.name, c.type)};`)
1153
+ .join('\n');
1154
+ const inputFields = dt.conditions
1155
+ .map(c => ` ${c.name},`)
1156
+ .join('\n');
1157
+ const outputAssigns = dt.actions
1158
+ .map(a => {
1159
+ const aType = a.type as string;
1160
+ if (aType === 'bool') return ` result["${a.name}"] = Value::Bool(dt_result.${a.name});`;
1161
+ if (aType === 'int_range') return ` result["${a.name}"] = json!(dt_result.${a.name});`;
1162
+ if (aType === 'decimal_range') return ` result["${a.name}"] = json!(dt_result.${a.name});`;
1163
+ return ` result["${a.name}"] = Value::String(dt_result.${a.name}.clone());`;
1164
+ })
1165
+ .join('\n');
1166
+ return `// Action: ${action.name}
1167
+ // Decision table: ${dt.name}
1168
+ // Register via: machine.register_action_rust("${action.name}", Box::new(${action.name}))
1169
+
1170
+ use serde_json::{Value, json};
1171
+
1172
+ fn ${action.name}(ctx: &Value, event: &Value) -> Value {
1173
+ ${ctxReads}
1174
+ let dt_input = ${inputStruct} {
1175
+ ${inputFields}
1176
+ };
1177
+ if let Some(dt_result) = ${dtFnName}(&dt_input) {
1178
+ let mut result = ctx.clone();
1179
+ ${outputAssigns}
1180
+ return result;
1181
+ }
1182
+ ctx.clone()
1183
+ }
1184
+ `;
1185
+ }
1186
+
1038
1187
  // TypeScript (default)
1039
1188
  const dtFnName = `evaluate${toPascalCase(dt.name)}`;
1040
1189
  const inputArgs = dt.conditions
@@ -1132,6 +1281,36 @@ ${goTodo}
1132
1281
  `;
1133
1282
  }
1134
1283
 
1284
+ if (language === 'rust') {
1285
+ if (action.hasEffect) {
1286
+ return `// Action: ${action.name}
1287
+ // Effect: ${action.effectType}
1288
+ // Register via: machine.register_action_rust("${action.name}", Box::new(${action.name}))
1289
+
1290
+ use serde_json::Value;
1291
+
1292
+ fn ${action.name}(ctx: &Value, event: &Value) -> (Value, Effect) {
1293
+ // TODO: Implement effect
1294
+ (ctx.clone(), Effect { effect_type: "${action.effectType}".to_string(), payload: Value::Null })
1295
+ }
1296
+ `;
1297
+ }
1298
+ const rustDTComment = matchedDT ? '\n' + generateDTCallComment(matchedDT, 'rust') + '\n' : '';
1299
+ const rustTodo = matchedDT
1300
+ ? ` // TODO: Implement action using ${matchedDT.name} decision table`
1301
+ : ' // TODO: Implement action';
1302
+ return `// Action: ${action.name}${matchedDT ? `\n// Decision table: ${matchedDT.name}` : ''}
1303
+ // Register via: machine.register_action_rust("${action.name}", Box::new(${action.name}))
1304
+
1305
+ use serde_json::Value;
1306
+
1307
+ fn ${action.name}(ctx: &Value, event: &Value) -> Value {${rustDTComment}
1308
+ ${rustTodo}
1309
+ ctx.clone()
1310
+ }
1311
+ `;
1312
+ }
1313
+
1135
1314
  // TypeScript (default)
1136
1315
  const params = action.parameters.map(p => {
1137
1316
  if (p === 'ctx' || p === 'Context') return 'ctx: Context';
@@ -1996,7 +2175,7 @@ export interface VerifyDTSkillResult {
1996
2175
 
1997
2176
  export interface CompileDTSkillResult {
1998
2177
  status: 'success' | 'error';
1999
- target: 'typescript' | 'json';
2178
+ target: string;
2000
2179
  output: string;
2001
2180
  warnings: SkillError[];
2002
2181
  }
@@ -2109,7 +2288,7 @@ export function verifyDTSkill(input: SkillInput): VerifyDTSkillResult {
2109
2288
  /**
2110
2289
  * Compile a decision table to TypeScript or JSON.
2111
2290
  */
2112
- export function compileDTSkill(input: SkillInput, target: 'typescript' | 'json' = 'typescript'): CompileDTSkillResult {
2291
+ export function compileDTSkill(input: SkillInput, target: string = 'typescript'): CompileDTSkillResult {
2113
2292
  try {
2114
2293
  const source = resolveSource(input);
2115
2294
  const { file } = parseMarkdown(source);
@@ -2124,9 +2303,24 @@ export function compileDTSkill(input: SkillInput, target: 'typescript' | 'json'
2124
2303
  }
2125
2304
 
2126
2305
  const dt = file.decisionTables[0];
2127
- const output = target === 'json'
2128
- ? compileDecisionTableToJSON(dt)
2129
- : compileDecisionTableToTypeScript(dt);
2306
+ let output: string;
2307
+ switch (target) {
2308
+ case 'json':
2309
+ output = compileDecisionTableToJSON(dt);
2310
+ break;
2311
+ case 'python':
2312
+ output = compileDecisionTableToPython(dt);
2313
+ break;
2314
+ case 'go':
2315
+ output = compileDecisionTableToGo(dt);
2316
+ break;
2317
+ case 'rust':
2318
+ output = compileDecisionTableToRust(dt);
2319
+ break;
2320
+ default:
2321
+ output = compileDecisionTableToTypeScript(dt);
2322
+ break;
2323
+ }
2130
2324
 
2131
2325
  return { status: 'success', target, output, warnings: [] };
2132
2326
  } catch (err) {
package/src/tools.ts CHANGED
@@ -72,14 +72,14 @@ export const ORCA_TOOLS: ToolDef[] = [
72
72
  {
73
73
  name: 'generate_actions',
74
74
  description:
75
- 'Generate action scaffold code from verified machine. lang: typescript (default), python, or go. Pass verified .orca.md source. use_llm: true for implementations vs templates. generate_tests: true for test scaffolds.',
75
+ 'Generate action scaffold code from verified machine. lang: typescript (default), python, go, or rust. Pass verified .orca.md source. use_llm: true for implementations vs templates. generate_tests: true for test scaffolds.',
76
76
  inputSchema: {
77
77
  type: 'object',
78
78
  properties: {
79
79
  source: { type: 'string', description: 'Raw .orca.md content of a valid machine. Actions in the "## actions" table (| Name | Signature |) will have stubs generated.' },
80
80
  lang: {
81
81
  type: 'string',
82
- enum: ['typescript', 'python', 'go'],
82
+ enum: ['typescript', 'python', 'go', 'rust'],
83
83
  description: 'Target language (default: typescript)',
84
84
  },
85
85
  use_llm: {
@@ -170,14 +170,14 @@ export const ORCA_TOOLS: ToolDef[] = [
170
170
  {
171
171
  name: 'compile_decision_table',
172
172
  description:
173
- 'Compile verified decision table to TypeScript evaluator function or portable JSON. Run verify_decision_table first. target: "typescript" (default) or "json".',
173
+ 'Compile verified decision table to TypeScript, Python, Go, or Rust evaluator function, or portable JSON. Run verify_decision_table first.',
174
174
  inputSchema: {
175
175
  type: 'object',
176
176
  properties: {
177
177
  source: { type: 'string', description: 'Raw .orca.md content containing a # decision_table heading.' },
178
178
  target: {
179
179
  type: 'string',
180
- enum: ['typescript', 'json'],
180
+ enum: ['typescript', 'python', 'go', 'rust', 'json'],
181
181
  description: 'Compilation target (default: typescript)',
182
182
  },
183
183
  },
@@ -98,19 +98,19 @@ function areGuardsMutuallyExclusive(
98
98
  // Strategy 2: Resolve to expressions and check pairwise exclusivity
99
99
  const resolvedExprs = guardRefs.map(ref => resolveGuardExpression(ref, guardDefs));
100
100
 
101
- // If any guard couldn't be resolved, we can't verify — assume OK
102
- if (resolvedExprs.some(e => e === null)) return true;
101
+ // If any guard couldn't be resolved, we can't verify — assume not exclusive
102
+ if (resolvedExprs.some(e => e === null)) return false;
103
103
 
104
104
  // Check all pairs are mutually exclusive
105
105
  for (let i = 0; i < resolvedExprs.length; i++) {
106
106
  for (let j = i + 1; j < resolvedExprs.length; j++) {
107
- if (areExpressionsMutuallyExclusive(resolvedExprs[i]!, resolvedExprs[j]!)) {
108
- return true;
107
+ if (!areExpressionsMutuallyExclusive(resolvedExprs[i]!, resolvedExprs[j]!)) {
108
+ return false;
109
109
  }
110
110
  }
111
111
  }
112
112
 
113
- return false;
113
+ return true;
114
114
  }
115
115
 
116
116
  /**