@orcalang/orca-lang 0.1.19 → 0.1.24

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 (53) hide show
  1. package/dist/compiler/dt-compiler.d.ts +4 -0
  2. package/dist/compiler/dt-compiler.d.ts.map +1 -1
  3. package/dist/compiler/dt-compiler.js +354 -4
  4. package/dist/compiler/dt-compiler.js.map +1 -1
  5. package/dist/health-check.js +75 -0
  6. package/dist/health-check.js.map +1 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +5 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/parser/ast-to-markdown.d.ts.map +1 -1
  11. package/dist/parser/ast-to-markdown.js +3 -1
  12. package/dist/parser/ast-to-markdown.js.map +1 -1
  13. package/dist/parser/ast.d.ts +1 -0
  14. package/dist/parser/ast.d.ts.map +1 -1
  15. package/dist/parser/dt-ast.d.ts +11 -1
  16. package/dist/parser/dt-ast.d.ts.map +1 -1
  17. package/dist/parser/dt-parser.d.ts.map +1 -1
  18. package/dist/parser/dt-parser.js +40 -8
  19. package/dist/parser/dt-parser.js.map +1 -1
  20. package/dist/parser/markdown-parser.d.ts.map +1 -1
  21. package/dist/parser/markdown-parser.js +14 -4
  22. package/dist/parser/markdown-parser.js.map +1 -1
  23. package/dist/skills.d.ts +3 -2
  24. package/dist/skills.d.ts.map +1 -1
  25. package/dist/skills.js +486 -28
  26. package/dist/skills.js.map +1 -1
  27. package/dist/tools.js +4 -4
  28. package/dist/tools.js.map +1 -1
  29. package/dist/verifier/dt-verifier.d.ts +28 -1
  30. package/dist/verifier/dt-verifier.d.ts.map +1 -1
  31. package/dist/verifier/dt-verifier.js +591 -32
  32. package/dist/verifier/dt-verifier.js.map +1 -1
  33. package/dist/verifier/properties.d.ts +4 -0
  34. package/dist/verifier/properties.d.ts.map +1 -1
  35. package/dist/verifier/properties.js +56 -20
  36. package/dist/verifier/properties.js.map +1 -1
  37. package/dist/verifier/structural.d.ts.map +1 -1
  38. package/dist/verifier/structural.js +6 -1
  39. package/dist/verifier/structural.js.map +1 -1
  40. package/package.json +1 -1
  41. package/src/compiler/dt-compiler.ts +374 -4
  42. package/src/health-check.ts +79 -0
  43. package/src/index.ts +5 -1
  44. package/src/parser/ast-to-markdown.ts +2 -1
  45. package/src/parser/ast.ts +1 -0
  46. package/src/parser/dt-ast.ts +4 -2
  47. package/src/parser/dt-parser.ts +46 -8
  48. package/src/parser/markdown-parser.ts +11 -3
  49. package/src/skills.ts +520 -30
  50. package/src/tools.ts +4 -4
  51. package/src/verifier/dt-verifier.ts +639 -30
  52. package/src/verifier/properties.ts +78 -23
  53. package/src/verifier/structural.ts +5 -1
package/src/skills.ts CHANGED
@@ -7,8 +7,16 @@ import { checkProperties } from './verifier/properties.js';
7
7
  import { compileToXState } from './compiler/xstate.js';
8
8
  import { compileToMermaid } from './compiler/mermaid.js';
9
9
  import { MachineDef, StateDef, GuardExpression, Type } from './parser/ast.js';
10
- import { verifyDecisionTable, verifyDecisionTables } from './verifier/dt-verifier.js';
11
- import { compileDecisionTableToTypeScript, compileDecisionTableToJSON } from './compiler/dt-compiler.js';
10
+ import { DecisionTableDef } from './parser/dt-ast.js';
11
+ import { verifyDecisionTable, verifyDecisionTables, checkFileContextAlignment, checkDTMachineIntegration, computeAlignedDTOutputDomain } from './verifier/dt-verifier.js';
12
+ import {
13
+ compileDecisionTableToTypeScript,
14
+ compileDecisionTableToPython,
15
+ compileDecisionTableToGo,
16
+ compileDecisionTableToRust,
17
+ compileDecisionTableToJSON,
18
+ toSnakeCase,
19
+ } from './compiler/dt-compiler.js';
12
20
  import { loadConfig, resolveConfigOverrides } from './config/index.js';
13
21
  import { createProvider } from './llm/index.js';
14
22
  import type { LLMProvider } from './llm/index.js';
@@ -88,6 +96,7 @@ export interface GenerateActionsResult {
88
96
  machine: string;
89
97
  actions: ActionScaffold[];
90
98
  scaffolds: Record<string, string>;
99
+ decisionTableCode?: Record<string, string>; // DT name → compiled evaluator code
91
100
  tests?: Record<string, string>;
92
101
  }
93
102
 
@@ -247,8 +256,13 @@ export async function verifySkill(input: SkillInput): Promise<VerifySkillResult>
247
256
  const label = resolveLabel(input);
248
257
 
249
258
  let machine: MachineDef;
259
+ let fileDecisionTables: import('./parser/dt-ast.js').DecisionTableDef[] = [];
250
260
  try {
251
- machine = parseSource(label, source);
261
+ const { file } = parseMarkdown(source);
262
+ if (file.machines.length === 0) throw new Error(`${label} contains no machine definition.`);
263
+ if (file.machines.length > 1) throw new Error(`${label} contains multiple machines.`);
264
+ machine = file.machines[0];
265
+ fileDecisionTables = file.decisionTables;
252
266
  } catch (err) {
253
267
  // Parse error - return as verification error
254
268
  const message = err instanceof Error ? err.message : String(err);
@@ -270,15 +284,28 @@ export async function verifySkill(input: SkillInput): Promise<VerifySkillResult>
270
284
  const structural = checkStructural(machine);
271
285
  const completeness = checkCompleteness(machine);
272
286
  const determinism = checkDeterminism(machine);
273
- const properties = checkProperties(machine);
274
287
 
275
- const mapError = (e: { code: string; message: string; severity: 'error' | 'warning'; location?: { state?: string; event?: string }; suggestion?: string }): SkillError => ({
288
+ // Check co-located decision table alignment and machine integration (single-machine files only)
289
+ const orcaFile = { machines: [machine], decisionTables: fileDecisionTables };
290
+ const dtOutputDomain = fileDecisionTables.length > 0 ? computeAlignedDTOutputDomain(orcaFile) : undefined;
291
+ const properties = checkProperties(machine, { dtOutputDomain });
292
+ const dtAlignment = fileDecisionTables.length > 0
293
+ ? checkFileContextAlignment(orcaFile)
294
+ : [];
295
+ const dtIntegration = fileDecisionTables.length > 0
296
+ ? checkDTMachineIntegration(orcaFile)
297
+ : [];
298
+
299
+ const mapError = (e: { code: string; message: string; severity: 'error' | 'warning'; location?: { state?: string; event?: string; decisionTable?: string; condition?: string; action?: string }; suggestion?: string }): SkillError => ({
276
300
  code: e.code,
277
301
  message: e.message,
278
302
  severity: e.severity,
279
303
  location: e.location ? {
280
304
  state: e.location.state,
281
305
  event: e.location.event,
306
+ decisionTable: e.location.decisionTable,
307
+ condition: e.location.condition,
308
+ action: e.location.action,
282
309
  } : undefined,
283
310
  suggestion: e.suggestion,
284
311
  });
@@ -288,6 +315,8 @@ export async function verifySkill(input: SkillInput): Promise<VerifySkillResult>
288
315
  ...completeness.errors.map(mapError),
289
316
  ...determinism.errors.map(mapError),
290
317
  ...properties.errors.map(mapError),
318
+ ...dtAlignment.map(mapError),
319
+ ...dtIntegration.map(mapError),
291
320
  ];
292
321
 
293
322
  return {
@@ -347,7 +376,18 @@ export async function generateActionsSkill(
347
376
  generateTests: boolean = false
348
377
  ): Promise<GenerateActionsResult> {
349
378
  const source = resolveSource(input);
350
- const machine = parseSource(resolveLabel(input), source);
379
+
380
+ // Parse the full file to get both the machine and any co-located decision tables
381
+ const { file } = parseMarkdown(source);
382
+ const label = resolveLabel(input);
383
+ if (file.machines.length === 0) {
384
+ throw new Error(`${label} contains no machine definition.`);
385
+ }
386
+ if (file.machines.length > 1) {
387
+ throw new Error(`${label} contains multiple machines. Use a single-machine file for action generation.`);
388
+ }
389
+ const machine = file.machines[0];
390
+ const decisionTables = file.decisionTables;
351
391
 
352
392
  const actions: ActionScaffold[] = machine.actions.map(action => ({
353
393
  name: action.name,
@@ -359,6 +399,20 @@ export async function generateActionsSkill(
359
399
  context_used: extractContextFields(machine, action.name),
360
400
  }));
361
401
 
402
+ // Compile decision table evaluator code for each DT in the file
403
+ const decisionTableCode: Record<string, string> = {};
404
+ for (const dt of decisionTables) {
405
+ if (language === 'python') {
406
+ decisionTableCode[dt.name] = compileDecisionTableToPython(dt);
407
+ } else if (language === 'go') {
408
+ decisionTableCode[dt.name] = compileDecisionTableToGo(dt);
409
+ } else if (language === 'rust') {
410
+ decisionTableCode[dt.name] = compileDecisionTableToRust(dt);
411
+ } else {
412
+ decisionTableCode[dt.name] = compileDecisionTableToTypeScript(dt);
413
+ }
414
+ }
415
+
362
416
  let scaffolds: Record<string, string> = {};
363
417
  let tests: Record<string, string> = {};
364
418
 
@@ -373,7 +427,7 @@ export async function generateActionsSkill(
373
427
  temperature: config.temperature,
374
428
  });
375
429
 
376
- scaffolds = await generateWithLLM(provider, actions, machine, language as CodeGeneratorType);
430
+ scaffolds = await generateWithLLM(provider, actions, machine, language as CodeGeneratorType, decisionTables);
377
431
 
378
432
  if (generateTests) {
379
433
  tests = await generateUnitTests(provider, actions, machine, language as CodeGeneratorType);
@@ -381,7 +435,13 @@ export async function generateActionsSkill(
381
435
  } else {
382
436
  // Use template-based scaffold generation
383
437
  for (const action of machine.actions) {
384
- scaffolds[action.name] = generateActionScaffold(action, machine, language);
438
+ const matchedDT = findMatchingDT(action.name, decisionTables);
439
+ if (matchedDT && !action.hasEffect && isDTFullyAligned(matchedDT, machine)) {
440
+ // All DT conditions and outputs exist in context — generate fully wired code
441
+ scaffolds[action.name] = generateFullyWiredActionScaffold(action, machine, language, matchedDT);
442
+ } else {
443
+ scaffolds[action.name] = generateActionScaffold(action, machine, language, matchedDT ?? undefined);
444
+ }
385
445
  }
386
446
 
387
447
  if (generateTests) {
@@ -394,6 +454,7 @@ export async function generateActionsSkill(
394
454
  machine: machine.name,
395
455
  actions,
396
456
  scaffolds,
457
+ decisionTableCode: Object.keys(decisionTableCode).length > 0 ? decisionTableCode : undefined,
397
458
  tests: Object.keys(tests).length > 0 ? tests : undefined,
398
459
  };
399
460
  }
@@ -402,22 +463,35 @@ async function generateWithLLM(
402
463
  provider: LLMProvider,
403
464
  actions: ActionScaffold[],
404
465
  machine: MachineDef,
405
- language: CodeGeneratorType
466
+ language: CodeGeneratorType,
467
+ decisionTables: DecisionTableDef[] = []
406
468
  ): Promise<Record<string, string>> {
407
469
  const generator = getCodeGenerator(language);
408
470
  const scaffolds: Record<string, string> = {};
409
471
 
472
+ const dtContext = decisionTables.length > 0
473
+ ? `\nDecision tables available:\n${decisionTables.map(dt =>
474
+ `- ${dt.name}: conditions=[${dt.conditions.map(c => c.name).join(', ')}] outputs=[${dt.actions.map(a => a.name).join(', ')}]`
475
+ ).join('\n')}`
476
+ : '';
477
+
410
478
  const systemPrompt = `You are an expert ${language} developer specializing in state machine action implementations.
411
479
  Given a machine definition and action signatures, generate complete action implementations.
412
480
  Follow the type signatures exactly. Use the provided context fields.
413
- If an action has an effect, return [newContext, effect] tuple.`;
481
+ If an action has an effect, return [newContext, effect] tuple.
482
+ If decision tables are listed, use their evaluator functions (e.g. evaluate${language === 'go' ? 'DtName' : 'DtName'}) when appropriate.`;
414
483
 
415
484
  for (const action of actions) {
485
+ const matchedDT = findMatchingDT(action.name, decisionTables);
486
+ const dtHint = matchedDT
487
+ ? `\nThis action should use the ${matchedDT.name} decision table evaluator.`
488
+ : '';
489
+
416
490
  const userPrompt = `Machine: ${machine.name}
417
- Context fields: ${machine.context.map(f => `${f.name}: ${f.type || 'unknown'}`).join(', ')}
491
+ Context fields: ${machine.context.map(f => `${f.name}: ${f.type || 'unknown'}`).join(', ')}${dtContext}
418
492
 
419
493
  Action: ${action.signature}
420
- Description: ${action.name}${action.hasEffect ? ` (effect type: ${action.effectType})` : ''}
494
+ Description: ${action.name}${action.hasEffect ? ` (effect type: ${action.effectType})` : ''}${dtHint}
421
495
 
422
496
  Generate the implementation:`;
423
497
 
@@ -436,7 +510,7 @@ Generate the implementation:`;
436
510
  } catch (err) {
437
511
  console.error(`LLM error for action ${action.name}: ${err instanceof Error ? err.message : String(err)}`);
438
512
  // Fall back to scaffold on error
439
- scaffolds[action.name] = generateActionScaffold(action, machine, language);
513
+ scaffolds[action.name] = generateActionScaffold(action, machine, language, matchedDT ?? undefined);
440
514
  }
441
515
  }
442
516
 
@@ -507,6 +581,9 @@ function generateTemplateTestsForAction(action: ActionScaffold, machine: Machine
507
581
  if (language === 'go') {
508
582
  return generateGoTestScaffold(action, machine);
509
583
  }
584
+ if (language === 'rust') {
585
+ return generateRustTestScaffold(action, machine);
586
+ }
510
587
  return generateTypeScriptTestScaffold(action, machine);
511
588
  }
512
589
 
@@ -691,6 +768,63 @@ ${preserved.map(f => `\tif result["${f}"] != ctx["${f}"] {\n\t\tt.Errorf("${f}:
691
768
  `;
692
769
  }
693
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
+
694
828
  function actionModifiesField(action: ActionScaffold, fieldName: string): boolean {
695
829
  // Heuristic: if the action name suggests modification of a field, it likely modifies it
696
830
  const modifiers = ['increment', 'decrement', 'set', 'update', 'add', 'remove', 'clear', 'reset', 'toggle'];
@@ -712,7 +846,7 @@ function actionModifiesField(action: ActionScaffold, fieldName: string): boolean
712
846
 
713
847
  function getDefaultValueForType(type: any, language: string = 'typescript'): string {
714
848
  if (!type) {
715
- return language === 'python' ? '""' : language === 'go' ? '""' : "''";
849
+ return language === 'python' ? '""' : language === 'go' ? '""' : language === 'rust' ? '"".to_string()' : "''";
716
850
  }
717
851
  if (typeof type === 'object' && 'kind' in type) {
718
852
  if (language === 'python') {
@@ -737,6 +871,22 @@ function getDefaultValueForType(type: any, language: string = 'typescript'): str
737
871
  case 'map': return 'nil';
738
872
  case 'custom': return 'nil';
739
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
+ }
740
890
  } else {
741
891
  switch (type.kind) {
742
892
  case 'string': return "''";
@@ -750,7 +900,7 @@ function getDefaultValueForType(type: any, language: string = 'typescript'): str
750
900
  }
751
901
  }
752
902
  }
753
- return language === 'python' ? 'None' : language === 'go' ? 'nil' : 'null';
903
+ return language === 'python' ? 'None' : language === 'go' ? 'nil' : language === 'rust' ? 'null' : 'null';
754
904
  }
755
905
 
756
906
  function extractContextFields(machine: MachineDef, actionName: string): string[] {
@@ -779,10 +929,292 @@ function toPascalCase(snake: string): string {
779
929
  return snake.split('_').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join('');
780
930
  }
781
931
 
932
+ /**
933
+ * Find a decision table whose name tokens overlap with the action name tokens.
934
+ * E.g. action "apply_routing_decision" matches DT "PaymentRouting" via "routing".
935
+ */
936
+ function findMatchingDT(actionName: string, dts: DecisionTableDef[]): DecisionTableDef | null {
937
+ if (dts.length === 0) return null;
938
+ const actionTokens = new Set(
939
+ actionName.toLowerCase().split('_').filter(t => t.length > 2)
940
+ );
941
+ for (const dt of dts) {
942
+ const dtTokens = dt.name
943
+ .replace(/([A-Z])/g, ' $1')
944
+ .trim()
945
+ .toLowerCase()
946
+ .split(/\s+/)
947
+ .filter(t => t.length > 2);
948
+ if (dtTokens.some(t => actionTokens.has(t))) return dt;
949
+ }
950
+ return null;
951
+ }
952
+
953
+ /**
954
+ * Generate a commented example DT call block to include in an action stub.
955
+ */
956
+ function generateDTCallComment(dt: DecisionTableDef, language: string): string {
957
+ const dtName = dt.name;
958
+
959
+ if (language === 'python') {
960
+ const fnName = `evaluate_${toSnakeCase(dtName)}`;
961
+ const inputClass = `${toPascalCase(dtName)}Input`;
962
+ const inputArgs = dt.conditions.map(c => {
963
+ const hint = c.type === 'enum' && c.values.length > 0
964
+ ? `str (${c.values.join(', ')})`
965
+ : c.type === 'bool' ? 'bool' : 'str';
966
+ return ` # ${c.name}=..., # ${hint} — map from ctx`;
967
+ }).join('\n');
968
+ const outputFields = dt.actions.map(a =>
969
+ ` # # dt_result.${a.name} → ctx['${a.name}']`
970
+ ).join('\n');
971
+ return [
972
+ ` # Call ${fnName} to evaluate ${dtName} rules:`,
973
+ ` # dt_result = ${fnName}(${inputClass}(`,
974
+ inputArgs,
975
+ ` # ))`,
976
+ ` # if dt_result is not None:`,
977
+ outputFields,
978
+ ].join('\n');
979
+ }
980
+
981
+ if (language === 'go') {
982
+ const fnName = `Evaluate${toPascalCase(dtName)}`;
983
+ const inputStruct = `${toPascalCase(dtName)}Input`;
984
+ const inputArgs = dt.conditions.map(c => {
985
+ const goField = toPascalCase(c.name);
986
+ const hint = c.type === 'enum' && c.values.length > 0
987
+ ? `string (${c.values.join(', ')})`
988
+ : c.type === 'bool' ? 'bool' : 'string';
989
+ return `\t// \t${goField}: ..., // ${hint} — map from ctx`;
990
+ }).join('\n');
991
+ const outputFields = dt.actions.map(a =>
992
+ `\t// \tresult["${a.name}"] = dtResult.${toPascalCase(a.name)}`
993
+ ).join('\n');
994
+ return [
995
+ `\t// Call ${fnName} to evaluate ${dtName} rules:`,
996
+ `\t// dtResult := ${fnName}(${inputStruct}{`,
997
+ inputArgs,
998
+ `\t// })`,
999
+ `\t// if dtResult != nil {`,
1000
+ outputFields,
1001
+ `\t// }`,
1002
+ ].join('\n');
1003
+ }
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
+
1028
+ // TypeScript (default)
1029
+ const fnName = `evaluate${toPascalCase(dtName)}`;
1030
+ const inputType = `${toPascalCase(dtName)}Input`;
1031
+ const inputArgs = dt.conditions.map(c => {
1032
+ const hint = c.type === 'enum' && c.values.length > 0
1033
+ ? `enum: ${c.values.join(', ')}`
1034
+ : c.type;
1035
+ return ` // ${c.name}: /* ctx.? */ as ${inputType}['${c.name}'], // ${hint} — map from ctx`;
1036
+ }).join('\n');
1037
+ const outputFields = dt.actions.map(a =>
1038
+ ` // ${a.name}: dtResult.${a.name},`
1039
+ ).join('\n');
1040
+ return [
1041
+ ` // Call ${fnName} to evaluate ${dtName} rules:`,
1042
+ ` // const dtResult = ${fnName}({`,
1043
+ inputArgs,
1044
+ ` // });`,
1045
+ ` // if (dtResult !== null) {`,
1046
+ ` // return { ...ctx,`,
1047
+ outputFields,
1048
+ ` // };`,
1049
+ ` // }`,
1050
+ ].join('\n');
1051
+ }
1052
+
1053
+ /**
1054
+ * Returns true when every DT condition name and output name exists as a
1055
+ * context field in the machine — the full co-location contract is satisfied.
1056
+ */
1057
+ function isDTFullyAligned(dt: DecisionTableDef, machine: MachineDef): boolean {
1058
+ const contextNames = new Set(machine.context.map(f => f.name));
1059
+ return dt.conditions.every(c => contextNames.has(c.name)) &&
1060
+ dt.actions.every(a => contextNames.has(a.name));
1061
+ }
1062
+
1063
+ /** Generate a Go type assertion for reading a context value. */
1064
+ function goCtxRead(fieldName: string, condType: string): string {
1065
+ if (condType === 'bool') return `ctx["${fieldName}"].(bool)`;
1066
+ if (condType === 'int_range') return `ctx["${fieldName}"].(int)`;
1067
+ return `ctx["${fieldName}"].(string)`;
1068
+ }
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
+
1078
+ function generateFullyWiredActionScaffold(
1079
+ action: { name: string; parameters: string[]; returnType: string; hasEffect: boolean; effectType?: string },
1080
+ machine: MachineDef,
1081
+ language: string,
1082
+ dt: DecisionTableDef
1083
+ ): string {
1084
+ if (language === 'python') {
1085
+ const dtFnName = `evaluate_${toSnakeCase(dt.name)}`;
1086
+ const inputClass = `${toPascalCase(dt.name)}Input`;
1087
+ const inputArgs = dt.conditions
1088
+ .map(c => ` ${c.name}=ctx['${c.name}'],`)
1089
+ .join('\n');
1090
+ const outputAssigns = dt.actions
1091
+ .map(a => ` '${a.name}': dt_result.${a.name},`)
1092
+ .join('\n');
1093
+ return `# Action: ${action.name}
1094
+ # Decision table: ${dt.name}
1095
+ # Register via: machine.register_action("${action.name}", ${action.name})
1096
+
1097
+ from typing import Any
1098
+
1099
+ async def ${action.name}(ctx: dict[str, Any], event: Any = None) -> dict[str, Any]:
1100
+ dt_result = ${dtFnName}(${inputClass}(
1101
+ ${inputArgs}
1102
+ ))
1103
+ if dt_result is not None:
1104
+ return {**ctx,
1105
+ ${outputAssigns}
1106
+ }
1107
+ return dict(ctx)
1108
+ `;
1109
+ }
1110
+
1111
+ if (language === 'go') {
1112
+ const fnName = toPascalCase(action.name);
1113
+ const dtFnName = `Evaluate${toPascalCase(dt.name)}`;
1114
+ const inputStruct = `${toPascalCase(dt.name)}Input`;
1115
+ const ctxReads = dt.conditions.map(c => {
1116
+ const varName = toPascalCase(c.name).charAt(0).toLowerCase() + toPascalCase(c.name).slice(1);
1117
+ return `\t${varName}, _ := ${goCtxRead(c.name, c.type)}`;
1118
+ }).join('\n');
1119
+ const inputFields = dt.conditions.map(c => {
1120
+ const goField = toPascalCase(c.name);
1121
+ const varName = goField.charAt(0).toLowerCase() + goField.slice(1);
1122
+ return `\t\t${goField}: ${varName},`;
1123
+ }).join('\n');
1124
+ const outputAssigns = dt.actions
1125
+ .map(a => `\t\tresult["${a.name}"] = dtResult.${toPascalCase(a.name)}`)
1126
+ .join('\n');
1127
+ return `// Action: ${action.name}
1128
+ // Decision table: ${dt.name}
1129
+ // Register via: machine.RegisterAction("${action.name}", ${fnName})
1130
+
1131
+ func ${fnName}(ctx orca.Context, event map[string]any) map[string]any {
1132
+ ${ctxReads}
1133
+ \tdtResult := ${dtFnName}(${inputStruct}{
1134
+ ${inputFields}
1135
+ \t})
1136
+ \tresult := make(orca.Context)
1137
+ \tfor k, v := range ctx {
1138
+ \t\tresult[k] = v
1139
+ \t}
1140
+ \tif dtResult != nil {
1141
+ ${outputAssigns}
1142
+ \t}
1143
+ \treturn result
1144
+ }
1145
+ `;
1146
+ }
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
+
1187
+ // TypeScript (default)
1188
+ const dtFnName = `evaluate${toPascalCase(dt.name)}`;
1189
+ const inputArgs = dt.conditions
1190
+ .map(c => ` ${c.name}: ctx.${c.name},`)
1191
+ .join('\n');
1192
+ const outputAssigns = dt.actions
1193
+ .map(a => ` ${a.name}: dtResult.${a.name},`)
1194
+ .join('\n');
1195
+ return `// Action: ${action.name}
1196
+ // Decision table: ${dt.name}
1197
+
1198
+ export function ${action.name}(ctx: Context): Context {
1199
+ const dtResult = ${dtFnName}({
1200
+ ${inputArgs}
1201
+ });
1202
+ if (dtResult !== null) {
1203
+ return {
1204
+ ...ctx,
1205
+ ${outputAssigns}
1206
+ };
1207
+ }
1208
+ return { ...ctx };
1209
+ }
1210
+ `;
1211
+ }
1212
+
782
1213
  function generateActionScaffold(
783
1214
  action: { name: string; parameters: string[]; returnType: string; hasEffect: boolean; effectType?: string },
784
1215
  machine: MachineDef,
785
- language: string
1216
+ language: string,
1217
+ matchedDT?: DecisionTableDef
786
1218
  ): string {
787
1219
  if (language === 'python') {
788
1220
  if (action.hasEffect) {
@@ -800,13 +1232,17 @@ async def ${action.name}(effect: Effect) -> EffectResult:
800
1232
  return EffectResult(status=EffectStatus.SUCCESS, data={})
801
1233
  `;
802
1234
  }
803
- return `# Action: ${action.name}
1235
+ const pyDTComment = matchedDT ? '\n' + generateDTCallComment(matchedDT, 'python') + '\n' : '';
1236
+ const pyTodo = matchedDT
1237
+ ? ` # TODO: Implement action using ${matchedDT.name} decision table`
1238
+ : ' # TODO: Implement action';
1239
+ return `# Action: ${action.name}${matchedDT ? `\n# Decision table: ${matchedDT.name}` : ''}
804
1240
  # Register via: machine.register_action("${action.name}", ${action.name})
805
1241
 
806
1242
  from typing import Any
807
1243
 
808
- async def ${action.name}(ctx: dict[str, Any], event: Any = None) -> dict[str, Any]:
809
- # TODO: Implement action
1244
+ async def ${action.name}(ctx: dict[str, Any], event: Any = None) -> dict[str, Any]:${pyDTComment}
1245
+ ${pyTodo}
810
1246
  return dict(ctx)
811
1247
  `;
812
1248
  }
@@ -827,11 +1263,15 @@ func ${fnName}(effect orca.Effect) orca.EffectResult {
827
1263
  }
828
1264
  `;
829
1265
  }
830
- return `// Action: ${action.name}
1266
+ const goDTComment = matchedDT ? '\n' + generateDTCallComment(matchedDT, 'go') + '\n' : '';
1267
+ const goTodo = matchedDT
1268
+ ? `\t// TODO: Implement action using ${matchedDT.name} decision table`
1269
+ : '\t// TODO: Implement action';
1270
+ return `// Action: ${action.name}${matchedDT ? `\n// Decision table: ${matchedDT.name}` : ''}
831
1271
  // Register via: machine.RegisterAction("${action.name}", ${fnName})
832
1272
 
833
- func ${fnName}(ctx orca.Context, event map[string]any) map[string]any {
834
- \t// TODO: Implement action
1273
+ func ${fnName}(ctx orca.Context, event map[string]any) map[string]any {${goDTComment}
1274
+ ${goTodo}
835
1275
  \tresult := make(orca.Context)
836
1276
  \tfor k, v := range ctx {
837
1277
  \t\tresult[k] = v
@@ -841,6 +1281,36 @@ func ${fnName}(ctx orca.Context, event map[string]any) map[string]any {
841
1281
  `;
842
1282
  }
843
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
+
844
1314
  // TypeScript (default)
845
1315
  const params = action.parameters.map(p => {
846
1316
  if (p === 'ctx' || p === 'Context') return 'ctx: Context';
@@ -863,10 +1333,15 @@ export function ${action.name}(${params}): [Context, Effect<${action.effectType}
863
1333
  // }
864
1334
  `;
865
1335
  }
866
- return `// Action: ${action.name}
867
1336
 
868
- export function ${action.name}(ctx: Context): Context {
869
- // TODO: Implement action
1337
+ const tsDTComment = matchedDT ? '\n' + generateDTCallComment(matchedDT, 'typescript') + '\n' : '';
1338
+ const tsTodo = matchedDT
1339
+ ? ` // TODO: Implement action using ${matchedDT.name} decision table`
1340
+ : ' // TODO: Implement action';
1341
+ return `// Action: ${action.name}${matchedDT ? `\n// Decision table: ${matchedDT.name}` : ''}
1342
+
1343
+ export function ${action.name}(ctx: Context): Context {${tsDTComment}
1344
+ ${tsTodo}
870
1345
  return { ...ctx };
871
1346
  }
872
1347
  `;
@@ -1700,7 +2175,7 @@ export interface VerifyDTSkillResult {
1700
2175
 
1701
2176
  export interface CompileDTSkillResult {
1702
2177
  status: 'success' | 'error';
1703
- target: 'typescript' | 'json';
2178
+ target: string;
1704
2179
  output: string;
1705
2180
  warnings: SkillError[];
1706
2181
  }
@@ -1813,7 +2288,7 @@ export function verifyDTSkill(input: SkillInput): VerifyDTSkillResult {
1813
2288
  /**
1814
2289
  * Compile a decision table to TypeScript or JSON.
1815
2290
  */
1816
- export function compileDTSkill(input: SkillInput, target: 'typescript' | 'json' = 'typescript'): CompileDTSkillResult {
2291
+ export function compileDTSkill(input: SkillInput, target: string = 'typescript'): CompileDTSkillResult {
1817
2292
  try {
1818
2293
  const source = resolveSource(input);
1819
2294
  const { file } = parseMarkdown(source);
@@ -1828,9 +2303,24 @@ export function compileDTSkill(input: SkillInput, target: 'typescript' | 'json'
1828
2303
  }
1829
2304
 
1830
2305
  const dt = file.decisionTables[0];
1831
- const output = target === 'json'
1832
- ? compileDecisionTableToJSON(dt)
1833
- : 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
+ }
1834
2324
 
1835
2325
  return { status: 'success', target, output, warnings: [] };
1836
2326
  } catch (err) {