@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.
- package/dist/compiler/dt-compiler.d.ts +26 -0
- package/dist/compiler/dt-compiler.d.ts.map +1 -0
- package/dist/compiler/dt-compiler.js +387 -0
- package/dist/compiler/dt-compiler.js.map +1 -0
- package/dist/health-check.d.ts +3 -0
- package/dist/health-check.d.ts.map +1 -0
- package/dist/health-check.js +235 -0
- package/dist/health-check.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/parser/ast-to-markdown.d.ts.map +1 -1
- package/dist/parser/ast-to-markdown.js +3 -1
- package/dist/parser/ast-to-markdown.js.map +1 -1
- package/dist/parser/ast.d.ts +3 -0
- package/dist/parser/ast.d.ts.map +1 -1
- package/dist/parser/dt-ast.d.ts +43 -0
- package/dist/parser/dt-ast.d.ts.map +1 -0
- package/dist/parser/dt-ast.js +3 -0
- package/dist/parser/dt-ast.js.map +1 -0
- package/dist/parser/dt-parser.d.ts +40 -0
- package/dist/parser/dt-parser.d.ts.map +1 -0
- package/dist/parser/dt-parser.js +240 -0
- package/dist/parser/dt-parser.js.map +1 -0
- package/dist/parser/markdown-parser.d.ts.map +1 -1
- package/dist/parser/markdown-parser.js +43 -8
- package/dist/parser/markdown-parser.js.map +1 -1
- package/dist/skills.d.ts +50 -1
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +508 -21
- package/dist/skills.js.map +1 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +49 -0
- package/dist/tools.js.map +1 -1
- package/dist/verifier/dt-verifier.d.ts +32 -0
- package/dist/verifier/dt-verifier.d.ts.map +1 -0
- package/dist/verifier/dt-verifier.js +830 -0
- package/dist/verifier/dt-verifier.js.map +1 -0
- package/dist/verifier/properties.d.ts +4 -0
- package/dist/verifier/properties.d.ts.map +1 -1
- package/dist/verifier/properties.js +56 -20
- package/dist/verifier/properties.js.map +1 -1
- package/dist/verifier/structural.d.ts.map +1 -1
- package/dist/verifier/structural.js +6 -1
- package/dist/verifier/structural.js.map +1 -1
- package/dist/verifier/types.d.ts +4 -0
- package/dist/verifier/types.d.ts.map +1 -1
- package/package.json +3 -2
- package/src/compiler/dt-compiler.ts +454 -0
- package/src/health-check.ts +273 -0
- package/src/index.ts +5 -1
- package/src/parser/ast-to-markdown.ts +2 -1
- package/src/parser/ast.ts +4 -0
- package/src/parser/dt-ast.ts +40 -0
- package/src/parser/dt-parser.ts +289 -0
- package/src/parser/markdown-parser.ts +43 -8
- package/src/skills.ts +591 -22
- package/src/tools.ts +53 -0
- package/src/verifier/dt-verifier.ts +928 -0
- package/src/verifier/properties.ts +78 -23
- package/src/verifier/structural.ts +5 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
862
|
-
|
|
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
|
+
|