@orcalang/orca-lang 0.1.18 → 0.1.21

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 (62) hide show
  1. package/dist/compiler/dt-compiler.d.ts +26 -0
  2. package/dist/compiler/dt-compiler.d.ts.map +1 -0
  3. package/dist/compiler/dt-compiler.js +387 -0
  4. package/dist/compiler/dt-compiler.js.map +1 -0
  5. package/dist/health-check.d.ts +3 -0
  6. package/dist/health-check.d.ts.map +1 -0
  7. package/dist/health-check.js +235 -0
  8. package/dist/health-check.js.map +1 -0
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +5 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/parser/ast-to-markdown.d.ts.map +1 -1
  13. package/dist/parser/ast-to-markdown.js +3 -1
  14. package/dist/parser/ast-to-markdown.js.map +1 -1
  15. package/dist/parser/ast.d.ts +3 -0
  16. package/dist/parser/ast.d.ts.map +1 -1
  17. package/dist/parser/dt-ast.d.ts +43 -0
  18. package/dist/parser/dt-ast.d.ts.map +1 -0
  19. package/dist/parser/dt-ast.js +3 -0
  20. package/dist/parser/dt-ast.js.map +1 -0
  21. package/dist/parser/dt-parser.d.ts +40 -0
  22. package/dist/parser/dt-parser.d.ts.map +1 -0
  23. package/dist/parser/dt-parser.js +240 -0
  24. package/dist/parser/dt-parser.js.map +1 -0
  25. package/dist/parser/markdown-parser.d.ts.map +1 -1
  26. package/dist/parser/markdown-parser.js +43 -8
  27. package/dist/parser/markdown-parser.js.map +1 -1
  28. package/dist/skills.d.ts +50 -1
  29. package/dist/skills.d.ts.map +1 -1
  30. package/dist/skills.js +508 -21
  31. package/dist/skills.js.map +1 -1
  32. package/dist/tools.d.ts.map +1 -1
  33. package/dist/tools.js +49 -0
  34. package/dist/tools.js.map +1 -1
  35. package/dist/verifier/dt-verifier.d.ts +32 -0
  36. package/dist/verifier/dt-verifier.d.ts.map +1 -0
  37. package/dist/verifier/dt-verifier.js +830 -0
  38. package/dist/verifier/dt-verifier.js.map +1 -0
  39. package/dist/verifier/properties.d.ts +4 -0
  40. package/dist/verifier/properties.d.ts.map +1 -1
  41. package/dist/verifier/properties.js +56 -20
  42. package/dist/verifier/properties.js.map +1 -1
  43. package/dist/verifier/structural.d.ts.map +1 -1
  44. package/dist/verifier/structural.js +6 -1
  45. package/dist/verifier/structural.js.map +1 -1
  46. package/dist/verifier/types.d.ts +4 -0
  47. package/dist/verifier/types.d.ts.map +1 -1
  48. package/package.json +3 -2
  49. package/src/compiler/dt-compiler.ts +454 -0
  50. package/src/health-check.ts +273 -0
  51. package/src/index.ts +5 -1
  52. package/src/parser/ast-to-markdown.ts +2 -1
  53. package/src/parser/ast.ts +4 -0
  54. package/src/parser/dt-ast.ts +40 -0
  55. package/src/parser/dt-parser.ts +289 -0
  56. package/src/parser/markdown-parser.ts +43 -8
  57. package/src/skills.ts +591 -22
  58. package/src/tools.ts +53 -0
  59. package/src/verifier/dt-verifier.ts +928 -0
  60. package/src/verifier/properties.ts +78 -23
  61. package/src/verifier/structural.ts +5 -1
  62. package/src/verifier/types.ts +4 -0
package/src/skills.ts CHANGED
@@ -7,6 +7,15 @@ 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 { 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
+ compileDecisionTableToJSON,
17
+ toSnakeCase,
18
+ } from './compiler/dt-compiler.js';
10
19
  import { loadConfig, resolveConfigOverrides } from './config/index.js';
11
20
  import { createProvider } from './llm/index.js';
12
21
  import type { LLMProvider } from './llm/index.js';
@@ -45,7 +54,12 @@ export interface SkillError {
45
54
  location?: {
46
55
  state?: string;
47
56
  event?: string;
48
- transition?: string;
57
+ transition?: string | { source?: string; target?: string; event?: string }; // string for machine errors, object for DT errors
58
+ // Decision table specific
59
+ rule?: number;
60
+ condition?: string;
61
+ action?: string;
62
+ decisionTable?: string;
49
63
  };
50
64
  suggestion?: string;
51
65
  }
@@ -81,6 +95,7 @@ export interface GenerateActionsResult {
81
95
  machine: string;
82
96
  actions: ActionScaffold[];
83
97
  scaffolds: Record<string, string>;
98
+ decisionTableCode?: Record<string, string>; // DT name → compiled evaluator code
84
99
  tests?: Record<string, string>;
85
100
  }
86
101
 
@@ -240,8 +255,13 @@ export async function verifySkill(input: SkillInput): Promise<VerifySkillResult>
240
255
  const label = resolveLabel(input);
241
256
 
242
257
  let machine: MachineDef;
258
+ let fileDecisionTables: import('./parser/dt-ast.js').DecisionTableDef[] = [];
243
259
  try {
244
- machine = parseSource(label, source);
260
+ const { file } = parseMarkdown(source);
261
+ if (file.machines.length === 0) throw new Error(`${label} contains no machine definition.`);
262
+ if (file.machines.length > 1) throw new Error(`${label} contains multiple machines.`);
263
+ machine = file.machines[0];
264
+ fileDecisionTables = file.decisionTables;
245
265
  } catch (err) {
246
266
  // Parse error - return as verification error
247
267
  const message = err instanceof Error ? err.message : String(err);
@@ -263,15 +283,28 @@ export async function verifySkill(input: SkillInput): Promise<VerifySkillResult>
263
283
  const structural = checkStructural(machine);
264
284
  const completeness = checkCompleteness(machine);
265
285
  const determinism = checkDeterminism(machine);
266
- const properties = checkProperties(machine);
267
286
 
268
- const mapError = (e: { code: string; message: string; severity: 'error' | 'warning'; location?: { state?: string; event?: string }; suggestion?: string }): SkillError => ({
287
+ // Check co-located decision table alignment and machine integration (single-machine files only)
288
+ const orcaFile = { machines: [machine], decisionTables: fileDecisionTables };
289
+ const dtOutputDomain = fileDecisionTables.length > 0 ? computeAlignedDTOutputDomain(orcaFile) : undefined;
290
+ const properties = checkProperties(machine, { dtOutputDomain });
291
+ const dtAlignment = fileDecisionTables.length > 0
292
+ ? checkFileContextAlignment(orcaFile)
293
+ : [];
294
+ const dtIntegration = fileDecisionTables.length > 0
295
+ ? checkDTMachineIntegration(orcaFile)
296
+ : [];
297
+
298
+ const mapError = (e: { code: string; message: string; severity: 'error' | 'warning'; location?: { state?: string; event?: string; decisionTable?: string; condition?: string; action?: string }; suggestion?: string }): SkillError => ({
269
299
  code: e.code,
270
300
  message: e.message,
271
301
  severity: e.severity,
272
302
  location: e.location ? {
273
303
  state: e.location.state,
274
304
  event: e.location.event,
305
+ decisionTable: e.location.decisionTable,
306
+ condition: e.location.condition,
307
+ action: e.location.action,
275
308
  } : undefined,
276
309
  suggestion: e.suggestion,
277
310
  });
@@ -281,6 +314,8 @@ export async function verifySkill(input: SkillInput): Promise<VerifySkillResult>
281
314
  ...completeness.errors.map(mapError),
282
315
  ...determinism.errors.map(mapError),
283
316
  ...properties.errors.map(mapError),
317
+ ...dtAlignment.map(mapError),
318
+ ...dtIntegration.map(mapError),
284
319
  ];
285
320
 
286
321
  return {
@@ -340,7 +375,18 @@ export async function generateActionsSkill(
340
375
  generateTests: boolean = false
341
376
  ): Promise<GenerateActionsResult> {
342
377
  const source = resolveSource(input);
343
- const machine = parseSource(resolveLabel(input), source);
378
+
379
+ // Parse the full file to get both the machine and any co-located decision tables
380
+ const { file } = parseMarkdown(source);
381
+ const label = resolveLabel(input);
382
+ if (file.machines.length === 0) {
383
+ throw new Error(`${label} contains no machine definition.`);
384
+ }
385
+ if (file.machines.length > 1) {
386
+ throw new Error(`${label} contains multiple machines. Use a single-machine file for action generation.`);
387
+ }
388
+ const machine = file.machines[0];
389
+ const decisionTables = file.decisionTables;
344
390
 
345
391
  const actions: ActionScaffold[] = machine.actions.map(action => ({
346
392
  name: action.name,
@@ -352,6 +398,18 @@ export async function generateActionsSkill(
352
398
  context_used: extractContextFields(machine, action.name),
353
399
  }));
354
400
 
401
+ // Compile decision table evaluator code for each DT in the file
402
+ const decisionTableCode: Record<string, string> = {};
403
+ for (const dt of decisionTables) {
404
+ if (language === 'python') {
405
+ decisionTableCode[dt.name] = compileDecisionTableToPython(dt);
406
+ } else if (language === 'go') {
407
+ decisionTableCode[dt.name] = compileDecisionTableToGo(dt);
408
+ } else {
409
+ decisionTableCode[dt.name] = compileDecisionTableToTypeScript(dt);
410
+ }
411
+ }
412
+
355
413
  let scaffolds: Record<string, string> = {};
356
414
  let tests: Record<string, string> = {};
357
415
 
@@ -366,7 +424,7 @@ export async function generateActionsSkill(
366
424
  temperature: config.temperature,
367
425
  });
368
426
 
369
- scaffolds = await generateWithLLM(provider, actions, machine, language as CodeGeneratorType);
427
+ scaffolds = await generateWithLLM(provider, actions, machine, language as CodeGeneratorType, decisionTables);
370
428
 
371
429
  if (generateTests) {
372
430
  tests = await generateUnitTests(provider, actions, machine, language as CodeGeneratorType);
@@ -374,7 +432,13 @@ export async function generateActionsSkill(
374
432
  } else {
375
433
  // Use template-based scaffold generation
376
434
  for (const action of machine.actions) {
377
- scaffolds[action.name] = generateActionScaffold(action, machine, language);
435
+ const matchedDT = findMatchingDT(action.name, decisionTables);
436
+ if (matchedDT && !action.hasEffect && isDTFullyAligned(matchedDT, machine)) {
437
+ // All DT conditions and outputs exist in context — generate fully wired code
438
+ scaffolds[action.name] = generateFullyWiredActionScaffold(action, machine, language, matchedDT);
439
+ } else {
440
+ scaffolds[action.name] = generateActionScaffold(action, machine, language, matchedDT ?? undefined);
441
+ }
378
442
  }
379
443
 
380
444
  if (generateTests) {
@@ -387,6 +451,7 @@ export async function generateActionsSkill(
387
451
  machine: machine.name,
388
452
  actions,
389
453
  scaffolds,
454
+ decisionTableCode: Object.keys(decisionTableCode).length > 0 ? decisionTableCode : undefined,
390
455
  tests: Object.keys(tests).length > 0 ? tests : undefined,
391
456
  };
392
457
  }
@@ -395,22 +460,35 @@ async function generateWithLLM(
395
460
  provider: LLMProvider,
396
461
  actions: ActionScaffold[],
397
462
  machine: MachineDef,
398
- language: CodeGeneratorType
463
+ language: CodeGeneratorType,
464
+ decisionTables: DecisionTableDef[] = []
399
465
  ): Promise<Record<string, string>> {
400
466
  const generator = getCodeGenerator(language);
401
467
  const scaffolds: Record<string, string> = {};
402
468
 
469
+ const dtContext = decisionTables.length > 0
470
+ ? `\nDecision tables available:\n${decisionTables.map(dt =>
471
+ `- ${dt.name}: conditions=[${dt.conditions.map(c => c.name).join(', ')}] outputs=[${dt.actions.map(a => a.name).join(', ')}]`
472
+ ).join('\n')}`
473
+ : '';
474
+
403
475
  const systemPrompt = `You are an expert ${language} developer specializing in state machine action implementations.
404
476
  Given a machine definition and action signatures, generate complete action implementations.
405
477
  Follow the type signatures exactly. Use the provided context fields.
406
- If an action has an effect, return [newContext, effect] tuple.`;
478
+ If an action has an effect, return [newContext, effect] tuple.
479
+ If decision tables are listed, use their evaluator functions (e.g. evaluate${language === 'go' ? 'DtName' : 'DtName'}) when appropriate.`;
407
480
 
408
481
  for (const action of actions) {
482
+ const matchedDT = findMatchingDT(action.name, decisionTables);
483
+ const dtHint = matchedDT
484
+ ? `\nThis action should use the ${matchedDT.name} decision table evaluator.`
485
+ : '';
486
+
409
487
  const userPrompt = `Machine: ${machine.name}
410
- Context fields: ${machine.context.map(f => `${f.name}: ${f.type || 'unknown'}`).join(', ')}
488
+ Context fields: ${machine.context.map(f => `${f.name}: ${f.type || 'unknown'}`).join(', ')}${dtContext}
411
489
 
412
490
  Action: ${action.signature}
413
- Description: ${action.name}${action.hasEffect ? ` (effect type: ${action.effectType})` : ''}
491
+ Description: ${action.name}${action.hasEffect ? ` (effect type: ${action.effectType})` : ''}${dtHint}
414
492
 
415
493
  Generate the implementation:`;
416
494
 
@@ -429,7 +507,7 @@ Generate the implementation:`;
429
507
  } catch (err) {
430
508
  console.error(`LLM error for action ${action.name}: ${err instanceof Error ? err.message : String(err)}`);
431
509
  // Fall back to scaffold on error
432
- scaffolds[action.name] = generateActionScaffold(action, machine, language);
510
+ scaffolds[action.name] = generateActionScaffold(action, machine, language, matchedDT ?? undefined);
433
511
  }
434
512
  }
435
513
 
@@ -772,10 +850,222 @@ function toPascalCase(snake: string): string {
772
850
  return snake.split('_').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join('');
773
851
  }
774
852
 
853
+ /**
854
+ * Find a decision table whose name tokens overlap with the action name tokens.
855
+ * E.g. action "apply_routing_decision" matches DT "PaymentRouting" via "routing".
856
+ */
857
+ function findMatchingDT(actionName: string, dts: DecisionTableDef[]): DecisionTableDef | null {
858
+ if (dts.length === 0) return null;
859
+ const actionTokens = new Set(
860
+ actionName.toLowerCase().split('_').filter(t => t.length > 2)
861
+ );
862
+ for (const dt of dts) {
863
+ const dtTokens = dt.name
864
+ .replace(/([A-Z])/g, ' $1')
865
+ .trim()
866
+ .toLowerCase()
867
+ .split(/\s+/)
868
+ .filter(t => t.length > 2);
869
+ if (dtTokens.some(t => actionTokens.has(t))) return dt;
870
+ }
871
+ return null;
872
+ }
873
+
874
+ /**
875
+ * Generate a commented example DT call block to include in an action stub.
876
+ */
877
+ function generateDTCallComment(dt: DecisionTableDef, language: string): string {
878
+ const dtName = dt.name;
879
+
880
+ if (language === 'python') {
881
+ const fnName = `evaluate_${toSnakeCase(dtName)}`;
882
+ const inputClass = `${toPascalCase(dtName)}Input`;
883
+ const inputArgs = dt.conditions.map(c => {
884
+ const hint = c.type === 'enum' && c.values.length > 0
885
+ ? `str (${c.values.join(', ')})`
886
+ : c.type === 'bool' ? 'bool' : 'str';
887
+ return ` # ${c.name}=..., # ${hint} — map from ctx`;
888
+ }).join('\n');
889
+ const outputFields = dt.actions.map(a =>
890
+ ` # # dt_result.${a.name} → ctx['${a.name}']`
891
+ ).join('\n');
892
+ return [
893
+ ` # Call ${fnName} to evaluate ${dtName} rules:`,
894
+ ` # dt_result = ${fnName}(${inputClass}(`,
895
+ inputArgs,
896
+ ` # ))`,
897
+ ` # if dt_result is not None:`,
898
+ outputFields,
899
+ ].join('\n');
900
+ }
901
+
902
+ if (language === 'go') {
903
+ const fnName = `Evaluate${toPascalCase(dtName)}`;
904
+ const inputStruct = `${toPascalCase(dtName)}Input`;
905
+ const inputArgs = dt.conditions.map(c => {
906
+ const goField = toPascalCase(c.name);
907
+ const hint = c.type === 'enum' && c.values.length > 0
908
+ ? `string (${c.values.join(', ')})`
909
+ : c.type === 'bool' ? 'bool' : 'string';
910
+ return `\t// \t${goField}: ..., // ${hint} — map from ctx`;
911
+ }).join('\n');
912
+ const outputFields = dt.actions.map(a =>
913
+ `\t// \tresult["${a.name}"] = dtResult.${toPascalCase(a.name)}`
914
+ ).join('\n');
915
+ return [
916
+ `\t// Call ${fnName} to evaluate ${dtName} rules:`,
917
+ `\t// dtResult := ${fnName}(${inputStruct}{`,
918
+ inputArgs,
919
+ `\t// })`,
920
+ `\t// if dtResult != nil {`,
921
+ outputFields,
922
+ `\t// }`,
923
+ ].join('\n');
924
+ }
925
+
926
+ // TypeScript (default)
927
+ const fnName = `evaluate${toPascalCase(dtName)}`;
928
+ const inputType = `${toPascalCase(dtName)}Input`;
929
+ const inputArgs = dt.conditions.map(c => {
930
+ const hint = c.type === 'enum' && c.values.length > 0
931
+ ? `enum: ${c.values.join(', ')}`
932
+ : c.type;
933
+ return ` // ${c.name}: /* ctx.? */ as ${inputType}['${c.name}'], // ${hint} — map from ctx`;
934
+ }).join('\n');
935
+ const outputFields = dt.actions.map(a =>
936
+ ` // ${a.name}: dtResult.${a.name},`
937
+ ).join('\n');
938
+ return [
939
+ ` // Call ${fnName} to evaluate ${dtName} rules:`,
940
+ ` // const dtResult = ${fnName}({`,
941
+ inputArgs,
942
+ ` // });`,
943
+ ` // if (dtResult !== null) {`,
944
+ ` // return { ...ctx,`,
945
+ outputFields,
946
+ ` // };`,
947
+ ` // }`,
948
+ ].join('\n');
949
+ }
950
+
951
+ /**
952
+ * Returns true when every DT condition name and output name exists as a
953
+ * context field in the machine — the full co-location contract is satisfied.
954
+ */
955
+ function isDTFullyAligned(dt: DecisionTableDef, machine: MachineDef): boolean {
956
+ const contextNames = new Set(machine.context.map(f => f.name));
957
+ return dt.conditions.every(c => contextNames.has(c.name)) &&
958
+ dt.actions.every(a => contextNames.has(a.name));
959
+ }
960
+
961
+ /** Generate a Go type assertion for reading a context value. */
962
+ function goCtxRead(fieldName: string, condType: string): string {
963
+ if (condType === 'bool') return `ctx["${fieldName}"].(bool)`;
964
+ if (condType === 'int_range') return `ctx["${fieldName}"].(int)`;
965
+ return `ctx["${fieldName}"].(string)`;
966
+ }
967
+
968
+ function generateFullyWiredActionScaffold(
969
+ action: { name: string; parameters: string[]; returnType: string; hasEffect: boolean; effectType?: string },
970
+ machine: MachineDef,
971
+ language: string,
972
+ dt: DecisionTableDef
973
+ ): string {
974
+ if (language === 'python') {
975
+ const dtFnName = `evaluate_${toSnakeCase(dt.name)}`;
976
+ const inputClass = `${toPascalCase(dt.name)}Input`;
977
+ const inputArgs = dt.conditions
978
+ .map(c => ` ${c.name}=ctx['${c.name}'],`)
979
+ .join('\n');
980
+ const outputAssigns = dt.actions
981
+ .map(a => ` '${a.name}': dt_result.${a.name},`)
982
+ .join('\n');
983
+ return `# Action: ${action.name}
984
+ # Decision table: ${dt.name}
985
+ # Register via: machine.register_action("${action.name}", ${action.name})
986
+
987
+ from typing import Any
988
+
989
+ async def ${action.name}(ctx: dict[str, Any], event: Any = None) -> dict[str, Any]:
990
+ dt_result = ${dtFnName}(${inputClass}(
991
+ ${inputArgs}
992
+ ))
993
+ if dt_result is not None:
994
+ return {**ctx,
995
+ ${outputAssigns}
996
+ }
997
+ return dict(ctx)
998
+ `;
999
+ }
1000
+
1001
+ if (language === 'go') {
1002
+ const fnName = toPascalCase(action.name);
1003
+ const dtFnName = `Evaluate${toPascalCase(dt.name)}`;
1004
+ const inputStruct = `${toPascalCase(dt.name)}Input`;
1005
+ const ctxReads = dt.conditions.map(c => {
1006
+ const varName = toPascalCase(c.name).charAt(0).toLowerCase() + toPascalCase(c.name).slice(1);
1007
+ return `\t${varName}, _ := ${goCtxRead(c.name, c.type)}`;
1008
+ }).join('\n');
1009
+ const inputFields = dt.conditions.map(c => {
1010
+ const goField = toPascalCase(c.name);
1011
+ const varName = goField.charAt(0).toLowerCase() + goField.slice(1);
1012
+ return `\t\t${goField}: ${varName},`;
1013
+ }).join('\n');
1014
+ const outputAssigns = dt.actions
1015
+ .map(a => `\t\tresult["${a.name}"] = dtResult.${toPascalCase(a.name)}`)
1016
+ .join('\n');
1017
+ return `// Action: ${action.name}
1018
+ // Decision table: ${dt.name}
1019
+ // Register via: machine.RegisterAction("${action.name}", ${fnName})
1020
+
1021
+ func ${fnName}(ctx orca.Context, event map[string]any) map[string]any {
1022
+ ${ctxReads}
1023
+ \tdtResult := ${dtFnName}(${inputStruct}{
1024
+ ${inputFields}
1025
+ \t})
1026
+ \tresult := make(orca.Context)
1027
+ \tfor k, v := range ctx {
1028
+ \t\tresult[k] = v
1029
+ \t}
1030
+ \tif dtResult != nil {
1031
+ ${outputAssigns}
1032
+ \t}
1033
+ \treturn result
1034
+ }
1035
+ `;
1036
+ }
1037
+
1038
+ // TypeScript (default)
1039
+ const dtFnName = `evaluate${toPascalCase(dt.name)}`;
1040
+ const inputArgs = dt.conditions
1041
+ .map(c => ` ${c.name}: ctx.${c.name},`)
1042
+ .join('\n');
1043
+ const outputAssigns = dt.actions
1044
+ .map(a => ` ${a.name}: dtResult.${a.name},`)
1045
+ .join('\n');
1046
+ return `// Action: ${action.name}
1047
+ // Decision table: ${dt.name}
1048
+
1049
+ export function ${action.name}(ctx: Context): Context {
1050
+ const dtResult = ${dtFnName}({
1051
+ ${inputArgs}
1052
+ });
1053
+ if (dtResult !== null) {
1054
+ return {
1055
+ ...ctx,
1056
+ ${outputAssigns}
1057
+ };
1058
+ }
1059
+ return { ...ctx };
1060
+ }
1061
+ `;
1062
+ }
1063
+
775
1064
  function generateActionScaffold(
776
1065
  action: { name: string; parameters: string[]; returnType: string; hasEffect: boolean; effectType?: string },
777
1066
  machine: MachineDef,
778
- language: string
1067
+ language: string,
1068
+ matchedDT?: DecisionTableDef
779
1069
  ): string {
780
1070
  if (language === 'python') {
781
1071
  if (action.hasEffect) {
@@ -793,13 +1083,17 @@ async def ${action.name}(effect: Effect) -> EffectResult:
793
1083
  return EffectResult(status=EffectStatus.SUCCESS, data={})
794
1084
  `;
795
1085
  }
796
- return `# Action: ${action.name}
1086
+ const pyDTComment = matchedDT ? '\n' + generateDTCallComment(matchedDT, 'python') + '\n' : '';
1087
+ const pyTodo = matchedDT
1088
+ ? ` # TODO: Implement action using ${matchedDT.name} decision table`
1089
+ : ' # TODO: Implement action';
1090
+ return `# Action: ${action.name}${matchedDT ? `\n# Decision table: ${matchedDT.name}` : ''}
797
1091
  # Register via: machine.register_action("${action.name}", ${action.name})
798
1092
 
799
1093
  from typing import Any
800
1094
 
801
- async def ${action.name}(ctx: dict[str, Any], event: Any = None) -> dict[str, Any]:
802
- # TODO: Implement action
1095
+ async def ${action.name}(ctx: dict[str, Any], event: Any = None) -> dict[str, Any]:${pyDTComment}
1096
+ ${pyTodo}
803
1097
  return dict(ctx)
804
1098
  `;
805
1099
  }
@@ -820,11 +1114,15 @@ func ${fnName}(effect orca.Effect) orca.EffectResult {
820
1114
  }
821
1115
  `;
822
1116
  }
823
- return `// Action: ${action.name}
1117
+ const goDTComment = matchedDT ? '\n' + generateDTCallComment(matchedDT, 'go') + '\n' : '';
1118
+ const goTodo = matchedDT
1119
+ ? `\t// TODO: Implement action using ${matchedDT.name} decision table`
1120
+ : '\t// TODO: Implement action';
1121
+ return `// Action: ${action.name}${matchedDT ? `\n// Decision table: ${matchedDT.name}` : ''}
824
1122
  // Register via: machine.RegisterAction("${action.name}", ${fnName})
825
1123
 
826
- func ${fnName}(ctx orca.Context, event map[string]any) map[string]any {
827
- \t// TODO: Implement action
1124
+ func ${fnName}(ctx orca.Context, event map[string]any) map[string]any {${goDTComment}
1125
+ ${goTodo}
828
1126
  \tresult := make(orca.Context)
829
1127
  \tfor k, v := range ctx {
830
1128
  \t\tresult[k] = v
@@ -856,10 +1154,15 @@ export function ${action.name}(${params}): [Context, Effect<${action.effectType}
856
1154
  // }
857
1155
  `;
858
1156
  }
859
- return `// Action: ${action.name}
860
1157
 
861
- export function ${action.name}(ctx: Context): Context {
862
- // TODO: Implement action
1158
+ const tsDTComment = matchedDT ? '\n' + generateDTCallComment(matchedDT, 'typescript') + '\n' : '';
1159
+ const tsTodo = matchedDT
1160
+ ? ` // TODO: Implement action using ${matchedDT.name} decision table`
1161
+ : ' // TODO: Implement action';
1162
+ return `// Action: ${action.name}${matchedDT ? `\n// Decision table: ${matchedDT.name}` : ''}
1163
+
1164
+ export function ${action.name}(ctx: Context): Context {${tsDTComment}
1165
+ ${tsTodo}
863
1166
  return { ...ctx };
864
1167
  }
865
1168
  `;
@@ -1677,3 +1980,269 @@ function extractMachineNameFromSource(orca: string): string {
1677
1980
  const match = orca.match(/^(?:#\s+)?machine\s+(\w+)/m);
1678
1981
  return match ? match[1] : 'Unknown';
1679
1982
  }
1983
+
1984
+ // ============================================================
1985
+ // Decision Table Skills
1986
+ // ============================================================
1987
+
1988
+ export interface VerifyDTSkillResult {
1989
+ status: 'valid' | 'invalid';
1990
+ decisionTable: string;
1991
+ conditions: number;
1992
+ actions: number;
1993
+ rules: number;
1994
+ errors: SkillError[];
1995
+ }
1996
+
1997
+ export interface CompileDTSkillResult {
1998
+ status: 'success' | 'error';
1999
+ target: 'typescript' | 'json';
2000
+ output: string;
2001
+ warnings: SkillError[];
2002
+ }
2003
+
2004
+ /**
2005
+ * Parse a decision table from source.
2006
+ */
2007
+ export function parseDTSkill(input: SkillInput): { status: 'success' | 'error'; decisionTables?: object[]; error?: string } {
2008
+ try {
2009
+ const source = resolveSource(input);
2010
+ const { file } = parseMarkdown(source);
2011
+
2012
+ if (file.decisionTables.length === 0) {
2013
+ return { status: 'error', error: 'No decision table found in source' };
2014
+ }
2015
+
2016
+ // Return all decision tables as plain objects
2017
+ const tables = file.decisionTables.map(dt => ({
2018
+ name: dt.name,
2019
+ description: dt.description,
2020
+ conditions: dt.conditions.map(c => ({
2021
+ name: c.name,
2022
+ type: c.type,
2023
+ values: c.values,
2024
+ range: c.range,
2025
+ })),
2026
+ actions: dt.actions.map(a => ({
2027
+ name: a.name,
2028
+ type: a.type,
2029
+ description: a.description,
2030
+ values: a.values,
2031
+ })),
2032
+ rules: dt.rules.map(r => ({
2033
+ number: r.number,
2034
+ conditions: Object.fromEntries(r.conditions),
2035
+ actions: Object.fromEntries(r.actions),
2036
+ })),
2037
+ policy: dt.policy,
2038
+ }));
2039
+
2040
+ return { status: 'success', decisionTables: tables };
2041
+ } catch (err) {
2042
+ return { status: 'error', error: err instanceof Error ? err.message : String(err) };
2043
+ }
2044
+ }
2045
+
2046
+ /**
2047
+ * Verify a decision table.
2048
+ */
2049
+ export function verifyDTSkill(input: SkillInput): VerifyDTSkillResult {
2050
+ try {
2051
+ const source = resolveSource(input);
2052
+ const { file } = parseMarkdown(source);
2053
+
2054
+ if (file.decisionTables.length === 0) {
2055
+ return {
2056
+ status: 'invalid',
2057
+ decisionTable: '',
2058
+ conditions: 0,
2059
+ actions: 0,
2060
+ rules: 0,
2061
+ errors: [{ code: 'DT_NOT_FOUND', message: 'No decision table found in source', severity: 'error' }],
2062
+ };
2063
+ }
2064
+
2065
+ // Verify all decision tables
2066
+ const verification = verifyDecisionTables(file.decisionTables);
2067
+
2068
+ // Return result for the first decision table
2069
+ const dt = file.decisionTables[0];
2070
+ const errors: SkillError[] = [];
2071
+ for (const e of verification.errors) {
2072
+ const loc = e.location;
2073
+ errors.push({
2074
+ code: e.code,
2075
+ message: e.message,
2076
+ severity: e.severity as 'error' | 'warning',
2077
+ location: loc ? {
2078
+ state: loc.state,
2079
+ event: loc.event,
2080
+ rule: loc.rule,
2081
+ condition: loc.condition,
2082
+ action: loc.action,
2083
+ decisionTable: loc.decisionTable,
2084
+ } : undefined,
2085
+ suggestion: e.suggestion,
2086
+ });
2087
+ }
2088
+
2089
+ return {
2090
+ status: verification.valid ? 'valid' : 'invalid',
2091
+ decisionTable: dt.name,
2092
+ conditions: dt.conditions.length,
2093
+ actions: dt.actions.length,
2094
+ rules: dt.rules.length,
2095
+ errors,
2096
+ };
2097
+ } catch (err) {
2098
+ return {
2099
+ status: 'invalid',
2100
+ decisionTable: '',
2101
+ conditions: 0,
2102
+ actions: 0,
2103
+ rules: 0,
2104
+ errors: [{ code: 'PARSE_ERROR', message: err instanceof Error ? err.message : String(err), severity: 'error' }],
2105
+ };
2106
+ }
2107
+ }
2108
+
2109
+ /**
2110
+ * Compile a decision table to TypeScript or JSON.
2111
+ */
2112
+ export function compileDTSkill(input: SkillInput, target: 'typescript' | 'json' = 'typescript'): CompileDTSkillResult {
2113
+ try {
2114
+ const source = resolveSource(input);
2115
+ const { file } = parseMarkdown(source);
2116
+
2117
+ if (file.decisionTables.length === 0) {
2118
+ return {
2119
+ status: 'error',
2120
+ target,
2121
+ output: '',
2122
+ warnings: [{ code: 'DT_NOT_FOUND', message: 'No decision table found in source', severity: 'error' }],
2123
+ };
2124
+ }
2125
+
2126
+ const dt = file.decisionTables[0];
2127
+ const output = target === 'json'
2128
+ ? compileDecisionTableToJSON(dt)
2129
+ : compileDecisionTableToTypeScript(dt);
2130
+
2131
+ return { status: 'success', target, output, warnings: [] };
2132
+ } catch (err) {
2133
+ return {
2134
+ status: 'error',
2135
+ target,
2136
+ output: '',
2137
+ warnings: [{ code: 'COMPILE_ERROR', message: err instanceof Error ? err.message : String(err), severity: 'error' }],
2138
+ };
2139
+ }
2140
+ }
2141
+
2142
+ // Decision table syntax reference for LLM generation
2143
+ const DT_SYNTAX_REFERENCE = `
2144
+ # decision_table Name
2145
+
2146
+ ## conditions
2147
+
2148
+ | Name | Type | Values |
2149
+ |------|------|--------|
2150
+ | field_name | enum | value1, value2 |
2151
+ | is_active | bool | |
2152
+ | count | int_range | 1..100 |
2153
+
2154
+ ## actions
2155
+
2156
+ | Name | Type | Description |
2157
+ |------|------|-------------|
2158
+ | result | enum | Result description |
2159
+ | flag | bool | Whether something |
2160
+
2161
+ ## rules
2162
+
2163
+ | condition1 | condition2 | → action1 | → action2 |
2164
+ |------------|-----------|----------|-----------|
2165
+ | value1 | - | result1 | true |
2166
+ | value2 | !value3 | result2 | false |
2167
+
2168
+ Notes:
2169
+ - "-" in a condition cell means "any" (wildcard)
2170
+ - "!value" negates a value
2171
+ - "a,b" matches any of the values (OR semantics)
2172
+ - Rules are evaluated top-to-bottom; first match wins
2173
+ `;
2174
+
2175
+ /**
2176
+ * Generate a decision table from natural language spec.
2177
+ */
2178
+ export async function generateDTSkill(spec: string, configPath?: string): Promise<{
2179
+ status: 'success' | 'error' | 'requires_refinement';
2180
+ decisionTable?: string;
2181
+ orca?: string;
2182
+ verification?: VerifyDTSkillResult;
2183
+ error?: string;
2184
+ }> {
2185
+ const config = loadConfig(configPath);
2186
+ const provider = createProvider(config.provider, {
2187
+ api_key: config.api_key,
2188
+ base_url: config.base_url,
2189
+ model: config.model,
2190
+ max_tokens: config.max_tokens,
2191
+ temperature: config.temperature,
2192
+ });
2193
+
2194
+ if (!provider) {
2195
+ return { status: 'error', error: 'No LLM provider configured' };
2196
+ }
2197
+
2198
+ const prompt = `Generate a decision table in .orca.md format based on the following specification.
2199
+
2200
+ ${DT_SYNTAX_REFERENCE}
2201
+
2202
+ Specification:
2203
+ ${spec}
2204
+
2205
+ Respond with the decision table only, no explanation.`;
2206
+
2207
+ try {
2208
+ const response = await provider.complete({
2209
+ messages: [{ role: 'user', content: prompt }],
2210
+ model: '',
2211
+ max_tokens: config.max_tokens || 2048,
2212
+ temperature: 0.7,
2213
+ });
2214
+
2215
+ const orca = stripCodeFence(response.content);
2216
+
2217
+ // Verify the generated decision table
2218
+ const { file } = parseMarkdown(orca);
2219
+ if (file.decisionTables.length === 0) {
2220
+ return { status: 'error', error: 'Generated content does not contain a valid decision table', orca };
2221
+ }
2222
+
2223
+ const verification = verifyDecisionTables(file.decisionTables);
2224
+
2225
+ return {
2226
+ status: verification.valid ? 'success' : 'requires_refinement',
2227
+ decisionTable: file.decisionTables[0].name,
2228
+ orca,
2229
+ verification: verification.valid ? undefined : {
2230
+ status: 'invalid',
2231
+ decisionTable: file.decisionTables[0].name,
2232
+ conditions: file.decisionTables[0].conditions.length,
2233
+ actions: file.decisionTables[0].actions.length,
2234
+ rules: file.decisionTables[0].rules.length,
2235
+ errors: verification.errors.map(e => ({
2236
+ code: e.code,
2237
+ message: e.message,
2238
+ severity: e.severity,
2239
+ location: e.location,
2240
+ suggestion: e.suggestion,
2241
+ })),
2242
+ },
2243
+ };
2244
+ } catch (err) {
2245
+ return { status: 'error', error: err instanceof Error ? err.message : String(err) };
2246
+ }
2247
+ }
2248
+