@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.
- package/dist/compiler/dt-compiler.d.ts +4 -0
- package/dist/compiler/dt-compiler.d.ts.map +1 -1
- package/dist/compiler/dt-compiler.js +354 -4
- package/dist/compiler/dt-compiler.js.map +1 -1
- package/dist/health-check.js +75 -0
- package/dist/health-check.js.map +1 -1
- 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 +1 -0
- package/dist/parser/ast.d.ts.map +1 -1
- package/dist/parser/dt-ast.d.ts +11 -1
- package/dist/parser/dt-ast.d.ts.map +1 -1
- package/dist/parser/dt-parser.d.ts.map +1 -1
- package/dist/parser/dt-parser.js +40 -8
- package/dist/parser/dt-parser.js.map +1 -1
- package/dist/parser/markdown-parser.d.ts.map +1 -1
- package/dist/parser/markdown-parser.js +14 -4
- package/dist/parser/markdown-parser.js.map +1 -1
- package/dist/skills.d.ts +3 -2
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +486 -28
- package/dist/skills.js.map +1 -1
- package/dist/tools.js +4 -4
- package/dist/tools.js.map +1 -1
- package/dist/verifier/dt-verifier.d.ts +28 -1
- package/dist/verifier/dt-verifier.d.ts.map +1 -1
- package/dist/verifier/dt-verifier.js +591 -32
- package/dist/verifier/dt-verifier.js.map +1 -1
- 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/package.json +1 -1
- package/src/compiler/dt-compiler.ts +374 -4
- package/src/health-check.ts +79 -0
- package/src/index.ts +5 -1
- package/src/parser/ast-to-markdown.ts +2 -1
- package/src/parser/ast.ts +1 -0
- package/src/parser/dt-ast.ts +4 -2
- package/src/parser/dt-parser.ts +46 -8
- package/src/parser/markdown-parser.ts +11 -3
- package/src/skills.ts +520 -30
- package/src/tools.ts +4 -4
- package/src/verifier/dt-verifier.ts +639 -30
- package/src/verifier/properties.ts +78 -23
- 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 {
|
|
11
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
869
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
1832
|
-
|
|
1833
|
-
:
|
|
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) {
|