@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/dist/skills.js
CHANGED
|
@@ -6,8 +6,8 @@ import { checkDeterminism } from './verifier/determinism.js';
|
|
|
6
6
|
import { checkProperties } from './verifier/properties.js';
|
|
7
7
|
import { compileToXState } from './compiler/xstate.js';
|
|
8
8
|
import { compileToMermaid } from './compiler/mermaid.js';
|
|
9
|
-
import { verifyDecisionTables } from './verifier/dt-verifier.js';
|
|
10
|
-
import { compileDecisionTableToTypeScript, compileDecisionTableToJSON } from './compiler/dt-compiler.js';
|
|
9
|
+
import { verifyDecisionTables, checkFileContextAlignment, checkDTMachineIntegration, computeAlignedDTOutputDomain } from './verifier/dt-verifier.js';
|
|
10
|
+
import { compileDecisionTableToTypeScript, compileDecisionTableToPython, compileDecisionTableToGo, compileDecisionTableToRust, compileDecisionTableToJSON, toSnakeCase, } from './compiler/dt-compiler.js';
|
|
11
11
|
import { loadConfig } from './config/index.js';
|
|
12
12
|
import { createProvider } from './llm/index.js';
|
|
13
13
|
import { getCodeGenerator } from './generators/index.js';
|
|
@@ -136,8 +136,15 @@ export async function verifySkill(input) {
|
|
|
136
136
|
const source = resolveSource(input);
|
|
137
137
|
const label = resolveLabel(input);
|
|
138
138
|
let machine;
|
|
139
|
+
let fileDecisionTables = [];
|
|
139
140
|
try {
|
|
140
|
-
|
|
141
|
+
const { file } = parseMarkdown(source);
|
|
142
|
+
if (file.machines.length === 0)
|
|
143
|
+
throw new Error(`${label} contains no machine definition.`);
|
|
144
|
+
if (file.machines.length > 1)
|
|
145
|
+
throw new Error(`${label} contains multiple machines.`);
|
|
146
|
+
machine = file.machines[0];
|
|
147
|
+
fileDecisionTables = file.decisionTables;
|
|
141
148
|
}
|
|
142
149
|
catch (err) {
|
|
143
150
|
// Parse error - return as verification error
|
|
@@ -159,7 +166,16 @@ export async function verifySkill(input) {
|
|
|
159
166
|
const structural = checkStructural(machine);
|
|
160
167
|
const completeness = checkCompleteness(machine);
|
|
161
168
|
const determinism = checkDeterminism(machine);
|
|
162
|
-
|
|
169
|
+
// Check co-located decision table alignment and machine integration (single-machine files only)
|
|
170
|
+
const orcaFile = { machines: [machine], decisionTables: fileDecisionTables };
|
|
171
|
+
const dtOutputDomain = fileDecisionTables.length > 0 ? computeAlignedDTOutputDomain(orcaFile) : undefined;
|
|
172
|
+
const properties = checkProperties(machine, { dtOutputDomain });
|
|
173
|
+
const dtAlignment = fileDecisionTables.length > 0
|
|
174
|
+
? checkFileContextAlignment(orcaFile)
|
|
175
|
+
: [];
|
|
176
|
+
const dtIntegration = fileDecisionTables.length > 0
|
|
177
|
+
? checkDTMachineIntegration(orcaFile)
|
|
178
|
+
: [];
|
|
163
179
|
const mapError = (e) => ({
|
|
164
180
|
code: e.code,
|
|
165
181
|
message: e.message,
|
|
@@ -167,6 +183,9 @@ export async function verifySkill(input) {
|
|
|
167
183
|
location: e.location ? {
|
|
168
184
|
state: e.location.state,
|
|
169
185
|
event: e.location.event,
|
|
186
|
+
decisionTable: e.location.decisionTable,
|
|
187
|
+
condition: e.location.condition,
|
|
188
|
+
action: e.location.action,
|
|
170
189
|
} : undefined,
|
|
171
190
|
suggestion: e.suggestion,
|
|
172
191
|
});
|
|
@@ -175,6 +194,8 @@ export async function verifySkill(input) {
|
|
|
175
194
|
...completeness.errors.map(mapError),
|
|
176
195
|
...determinism.errors.map(mapError),
|
|
177
196
|
...properties.errors.map(mapError),
|
|
197
|
+
...dtAlignment.map(mapError),
|
|
198
|
+
...dtIntegration.map(mapError),
|
|
178
199
|
];
|
|
179
200
|
return {
|
|
180
201
|
status: allErrors.some(e => e.severity === 'error') ? 'invalid' : 'valid',
|
|
@@ -221,7 +242,17 @@ export async function compileSkill(input, target) {
|
|
|
221
242
|
}
|
|
222
243
|
export async function generateActionsSkill(input, language = 'typescript', useLLM = false, configPath, generateTests = false) {
|
|
223
244
|
const source = resolveSource(input);
|
|
224
|
-
|
|
245
|
+
// Parse the full file to get both the machine and any co-located decision tables
|
|
246
|
+
const { file } = parseMarkdown(source);
|
|
247
|
+
const label = resolveLabel(input);
|
|
248
|
+
if (file.machines.length === 0) {
|
|
249
|
+
throw new Error(`${label} contains no machine definition.`);
|
|
250
|
+
}
|
|
251
|
+
if (file.machines.length > 1) {
|
|
252
|
+
throw new Error(`${label} contains multiple machines. Use a single-machine file for action generation.`);
|
|
253
|
+
}
|
|
254
|
+
const machine = file.machines[0];
|
|
255
|
+
const decisionTables = file.decisionTables;
|
|
225
256
|
const actions = machine.actions.map(action => ({
|
|
226
257
|
name: action.name,
|
|
227
258
|
signature: `${action.name}(${action.parameters.join(', ')}) -> ${action.returnType}${action.hasEffect ? ` + Effect<${action.effectType}>` : ''}`,
|
|
@@ -231,6 +262,22 @@ export async function generateActionsSkill(input, language = 'typescript', useLL
|
|
|
231
262
|
effectType: action.effectType,
|
|
232
263
|
context_used: extractContextFields(machine, action.name),
|
|
233
264
|
}));
|
|
265
|
+
// Compile decision table evaluator code for each DT in the file
|
|
266
|
+
const decisionTableCode = {};
|
|
267
|
+
for (const dt of decisionTables) {
|
|
268
|
+
if (language === 'python') {
|
|
269
|
+
decisionTableCode[dt.name] = compileDecisionTableToPython(dt);
|
|
270
|
+
}
|
|
271
|
+
else if (language === 'go') {
|
|
272
|
+
decisionTableCode[dt.name] = compileDecisionTableToGo(dt);
|
|
273
|
+
}
|
|
274
|
+
else if (language === 'rust') {
|
|
275
|
+
decisionTableCode[dt.name] = compileDecisionTableToRust(dt);
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
decisionTableCode[dt.name] = compileDecisionTableToTypeScript(dt);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
234
281
|
let scaffolds = {};
|
|
235
282
|
let tests = {};
|
|
236
283
|
if (useLLM) {
|
|
@@ -243,7 +290,7 @@ export async function generateActionsSkill(input, language = 'typescript', useLL
|
|
|
243
290
|
max_tokens: config.max_tokens,
|
|
244
291
|
temperature: config.temperature,
|
|
245
292
|
});
|
|
246
|
-
scaffolds = await generateWithLLM(provider, actions, machine, language);
|
|
293
|
+
scaffolds = await generateWithLLM(provider, actions, machine, language, decisionTables);
|
|
247
294
|
if (generateTests) {
|
|
248
295
|
tests = await generateUnitTests(provider, actions, machine, language);
|
|
249
296
|
}
|
|
@@ -251,7 +298,14 @@ export async function generateActionsSkill(input, language = 'typescript', useLL
|
|
|
251
298
|
else {
|
|
252
299
|
// Use template-based scaffold generation
|
|
253
300
|
for (const action of machine.actions) {
|
|
254
|
-
|
|
301
|
+
const matchedDT = findMatchingDT(action.name, decisionTables);
|
|
302
|
+
if (matchedDT && !action.hasEffect && isDTFullyAligned(matchedDT, machine)) {
|
|
303
|
+
// All DT conditions and outputs exist in context — generate fully wired code
|
|
304
|
+
scaffolds[action.name] = generateFullyWiredActionScaffold(action, machine, language, matchedDT);
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
scaffolds[action.name] = generateActionScaffold(action, machine, language, matchedDT ?? undefined);
|
|
308
|
+
}
|
|
255
309
|
}
|
|
256
310
|
if (generateTests) {
|
|
257
311
|
tests = generateTemplateTests(actions, machine, language);
|
|
@@ -262,22 +316,31 @@ export async function generateActionsSkill(input, language = 'typescript', useLL
|
|
|
262
316
|
machine: machine.name,
|
|
263
317
|
actions,
|
|
264
318
|
scaffolds,
|
|
319
|
+
decisionTableCode: Object.keys(decisionTableCode).length > 0 ? decisionTableCode : undefined,
|
|
265
320
|
tests: Object.keys(tests).length > 0 ? tests : undefined,
|
|
266
321
|
};
|
|
267
322
|
}
|
|
268
|
-
async function generateWithLLM(provider, actions, machine, language) {
|
|
323
|
+
async function generateWithLLM(provider, actions, machine, language, decisionTables = []) {
|
|
269
324
|
const generator = getCodeGenerator(language);
|
|
270
325
|
const scaffolds = {};
|
|
326
|
+
const dtContext = decisionTables.length > 0
|
|
327
|
+
? `\nDecision tables available:\n${decisionTables.map(dt => `- ${dt.name}: conditions=[${dt.conditions.map(c => c.name).join(', ')}] outputs=[${dt.actions.map(a => a.name).join(', ')}]`).join('\n')}`
|
|
328
|
+
: '';
|
|
271
329
|
const systemPrompt = `You are an expert ${language} developer specializing in state machine action implementations.
|
|
272
330
|
Given a machine definition and action signatures, generate complete action implementations.
|
|
273
331
|
Follow the type signatures exactly. Use the provided context fields.
|
|
274
|
-
If an action has an effect, return [newContext, effect] tuple
|
|
332
|
+
If an action has an effect, return [newContext, effect] tuple.
|
|
333
|
+
If decision tables are listed, use their evaluator functions (e.g. evaluate${language === 'go' ? 'DtName' : 'DtName'}) when appropriate.`;
|
|
275
334
|
for (const action of actions) {
|
|
335
|
+
const matchedDT = findMatchingDT(action.name, decisionTables);
|
|
336
|
+
const dtHint = matchedDT
|
|
337
|
+
? `\nThis action should use the ${matchedDT.name} decision table evaluator.`
|
|
338
|
+
: '';
|
|
276
339
|
const userPrompt = `Machine: ${machine.name}
|
|
277
|
-
Context fields: ${machine.context.map(f => `${f.name}: ${f.type || 'unknown'}`).join(', ')}
|
|
340
|
+
Context fields: ${machine.context.map(f => `${f.name}: ${f.type || 'unknown'}`).join(', ')}${dtContext}
|
|
278
341
|
|
|
279
342
|
Action: ${action.signature}
|
|
280
|
-
Description: ${action.name}${action.hasEffect ? ` (effect type: ${action.effectType})` : ''}
|
|
343
|
+
Description: ${action.name}${action.hasEffect ? ` (effect type: ${action.effectType})` : ''}${dtHint}
|
|
281
344
|
|
|
282
345
|
Generate the implementation:`;
|
|
283
346
|
try {
|
|
@@ -295,7 +358,7 @@ Generate the implementation:`;
|
|
|
295
358
|
catch (err) {
|
|
296
359
|
console.error(`LLM error for action ${action.name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
297
360
|
// Fall back to scaffold on error
|
|
298
|
-
scaffolds[action.name] = generateActionScaffold(action, machine, language);
|
|
361
|
+
scaffolds[action.name] = generateActionScaffold(action, machine, language, matchedDT ?? undefined);
|
|
299
362
|
}
|
|
300
363
|
}
|
|
301
364
|
return scaffolds;
|
|
@@ -352,6 +415,9 @@ function generateTemplateTestsForAction(action, machine, language = 'typescript'
|
|
|
352
415
|
if (language === 'go') {
|
|
353
416
|
return generateGoTestScaffold(action, machine);
|
|
354
417
|
}
|
|
418
|
+
if (language === 'rust') {
|
|
419
|
+
return generateRustTestScaffold(action, machine);
|
|
420
|
+
}
|
|
355
421
|
return generateTypeScriptTestScaffold(action, machine);
|
|
356
422
|
}
|
|
357
423
|
function generateTypeScriptTestScaffold(action, machine) {
|
|
@@ -522,6 +588,60 @@ ${preserved.map(f => `\tif result["${f}"] != ctx["${f}"] {\n\t\tt.Errorf("${f}:
|
|
|
522
588
|
}
|
|
523
589
|
`;
|
|
524
590
|
}
|
|
591
|
+
function generateRustTestScaffold(action, machine) {
|
|
592
|
+
const ctxFields = machine.context.map(f => {
|
|
593
|
+
return ` "${f.name}": ${getDefaultValueForType(f.type, 'rust')}`;
|
|
594
|
+
}).join(',\n');
|
|
595
|
+
const contextUsed = action.context_used.length > 0 ? action.context_used : machine.context.map(f => f.name);
|
|
596
|
+
const preserved = contextUsed.filter(f => !actionModifiesField(action, f));
|
|
597
|
+
if (action.hasEffect) {
|
|
598
|
+
return `// Tests for ${action.name}
|
|
599
|
+
#[cfg(test)]
|
|
600
|
+
mod tests {
|
|
601
|
+
use super::*;
|
|
602
|
+
use serde_json::json;
|
|
603
|
+
|
|
604
|
+
#[test]
|
|
605
|
+
fn test_${action.name}_executes_effect() {
|
|
606
|
+
let ctx = json!({
|
|
607
|
+
${ctxFields}
|
|
608
|
+
});
|
|
609
|
+
let event = json!({"type": "test"});
|
|
610
|
+
let (result, effect) = ${action.name}(&ctx, &event);
|
|
611
|
+
assert!(result.is_object());
|
|
612
|
+
assert_eq!(effect.effect_type, "${action.effectType}");
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
`;
|
|
616
|
+
}
|
|
617
|
+
return `// Tests for ${action.name}
|
|
618
|
+
#[cfg(test)]
|
|
619
|
+
mod tests {
|
|
620
|
+
use super::*;
|
|
621
|
+
use serde_json::json;
|
|
622
|
+
|
|
623
|
+
#[test]
|
|
624
|
+
fn test_${action.name}_transforms_context() {
|
|
625
|
+
let ctx = json!({
|
|
626
|
+
${ctxFields}
|
|
627
|
+
});
|
|
628
|
+
let event = json!({"type": "test"});
|
|
629
|
+
let result = ${action.name}(&ctx, &event);
|
|
630
|
+
assert!(result.is_object());
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
#[test]
|
|
634
|
+
fn test_${action.name}_preserves_fields() {
|
|
635
|
+
let ctx = json!({
|
|
636
|
+
${ctxFields}
|
|
637
|
+
});
|
|
638
|
+
let event = json!({"type": "test"});
|
|
639
|
+
let result = ${action.name}(&ctx, &event);
|
|
640
|
+
${preserved.map(f => ` assert_eq!(result["${f}"], ctx["${f}"]);`).join('\n')}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
`;
|
|
644
|
+
}
|
|
525
645
|
function actionModifiesField(action, fieldName) {
|
|
526
646
|
// Heuristic: if the action name suggests modification of a field, it likely modifies it
|
|
527
647
|
const modifiers = ['increment', 'decrement', 'set', 'update', 'add', 'remove', 'clear', 'reset', 'toggle'];
|
|
@@ -539,7 +659,7 @@ function actionModifiesField(action, fieldName) {
|
|
|
539
659
|
}
|
|
540
660
|
function getDefaultValueForType(type, language = 'typescript') {
|
|
541
661
|
if (!type) {
|
|
542
|
-
return language === 'python' ? '""' : language === 'go' ? '""' : "''";
|
|
662
|
+
return language === 'python' ? '""' : language === 'go' ? '""' : language === 'rust' ? '"".to_string()' : "''";
|
|
543
663
|
}
|
|
544
664
|
if (typeof type === 'object' && 'kind' in type) {
|
|
545
665
|
if (language === 'python') {
|
|
@@ -566,6 +686,25 @@ function getDefaultValueForType(type, language = 'typescript') {
|
|
|
566
686
|
case 'custom': return 'nil';
|
|
567
687
|
}
|
|
568
688
|
}
|
|
689
|
+
else if (language === 'rust') {
|
|
690
|
+
switch (type.kind) {
|
|
691
|
+
case 'string': return '"".to_string()';
|
|
692
|
+
case 'int': return '0';
|
|
693
|
+
case 'decimal': return '0.0';
|
|
694
|
+
case 'bool': return 'false';
|
|
695
|
+
case 'optional': return 'null';
|
|
696
|
+
case 'array': return 'vec![]';
|
|
697
|
+
case 'map': return 'HashMap::new()';
|
|
698
|
+
case 'custom': {
|
|
699
|
+
// Handle common type aliases
|
|
700
|
+
if (type.name === 'float' || type.name === 'double')
|
|
701
|
+
return '0.0';
|
|
702
|
+
if (type.name === 'integer' || type.name === 'long')
|
|
703
|
+
return '0';
|
|
704
|
+
return 'null';
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
569
708
|
else {
|
|
570
709
|
switch (type.kind) {
|
|
571
710
|
case 'string': return "''";
|
|
@@ -579,7 +718,7 @@ function getDefaultValueForType(type, language = 'typescript') {
|
|
|
579
718
|
}
|
|
580
719
|
}
|
|
581
720
|
}
|
|
582
|
-
return language === 'python' ? 'None' : language === 'go' ? 'nil' : 'null';
|
|
721
|
+
return language === 'python' ? 'None' : language === 'go' ? 'nil' : language === 'rust' ? 'null' : 'null';
|
|
583
722
|
}
|
|
584
723
|
function extractContextFields(machine, actionName) {
|
|
585
724
|
const fields = [];
|
|
@@ -602,7 +741,270 @@ function extractContextFields(machine, actionName) {
|
|
|
602
741
|
function toPascalCase(snake) {
|
|
603
742
|
return snake.split('_').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join('');
|
|
604
743
|
}
|
|
605
|
-
|
|
744
|
+
/**
|
|
745
|
+
* Find a decision table whose name tokens overlap with the action name tokens.
|
|
746
|
+
* E.g. action "apply_routing_decision" matches DT "PaymentRouting" via "routing".
|
|
747
|
+
*/
|
|
748
|
+
function findMatchingDT(actionName, dts) {
|
|
749
|
+
if (dts.length === 0)
|
|
750
|
+
return null;
|
|
751
|
+
const actionTokens = new Set(actionName.toLowerCase().split('_').filter(t => t.length > 2));
|
|
752
|
+
for (const dt of dts) {
|
|
753
|
+
const dtTokens = dt.name
|
|
754
|
+
.replace(/([A-Z])/g, ' $1')
|
|
755
|
+
.trim()
|
|
756
|
+
.toLowerCase()
|
|
757
|
+
.split(/\s+/)
|
|
758
|
+
.filter(t => t.length > 2);
|
|
759
|
+
if (dtTokens.some(t => actionTokens.has(t)))
|
|
760
|
+
return dt;
|
|
761
|
+
}
|
|
762
|
+
return null;
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Generate a commented example DT call block to include in an action stub.
|
|
766
|
+
*/
|
|
767
|
+
function generateDTCallComment(dt, language) {
|
|
768
|
+
const dtName = dt.name;
|
|
769
|
+
if (language === 'python') {
|
|
770
|
+
const fnName = `evaluate_${toSnakeCase(dtName)}`;
|
|
771
|
+
const inputClass = `${toPascalCase(dtName)}Input`;
|
|
772
|
+
const inputArgs = dt.conditions.map(c => {
|
|
773
|
+
const hint = c.type === 'enum' && c.values.length > 0
|
|
774
|
+
? `str (${c.values.join(', ')})`
|
|
775
|
+
: c.type === 'bool' ? 'bool' : 'str';
|
|
776
|
+
return ` # ${c.name}=..., # ${hint} — map from ctx`;
|
|
777
|
+
}).join('\n');
|
|
778
|
+
const outputFields = dt.actions.map(a => ` # # dt_result.${a.name} → ctx['${a.name}']`).join('\n');
|
|
779
|
+
return [
|
|
780
|
+
` # Call ${fnName} to evaluate ${dtName} rules:`,
|
|
781
|
+
` # dt_result = ${fnName}(${inputClass}(`,
|
|
782
|
+
inputArgs,
|
|
783
|
+
` # ))`,
|
|
784
|
+
` # if dt_result is not None:`,
|
|
785
|
+
outputFields,
|
|
786
|
+
].join('\n');
|
|
787
|
+
}
|
|
788
|
+
if (language === 'go') {
|
|
789
|
+
const fnName = `Evaluate${toPascalCase(dtName)}`;
|
|
790
|
+
const inputStruct = `${toPascalCase(dtName)}Input`;
|
|
791
|
+
const inputArgs = dt.conditions.map(c => {
|
|
792
|
+
const goField = toPascalCase(c.name);
|
|
793
|
+
const hint = c.type === 'enum' && c.values.length > 0
|
|
794
|
+
? `string (${c.values.join(', ')})`
|
|
795
|
+
: c.type === 'bool' ? 'bool' : 'string';
|
|
796
|
+
return `\t// \t${goField}: ..., // ${hint} — map from ctx`;
|
|
797
|
+
}).join('\n');
|
|
798
|
+
const outputFields = dt.actions.map(a => `\t// \tresult["${a.name}"] = dtResult.${toPascalCase(a.name)}`).join('\n');
|
|
799
|
+
return [
|
|
800
|
+
`\t// Call ${fnName} to evaluate ${dtName} rules:`,
|
|
801
|
+
`\t// dtResult := ${fnName}(${inputStruct}{`,
|
|
802
|
+
inputArgs,
|
|
803
|
+
`\t// })`,
|
|
804
|
+
`\t// if dtResult != nil {`,
|
|
805
|
+
outputFields,
|
|
806
|
+
`\t// }`,
|
|
807
|
+
].join('\n');
|
|
808
|
+
}
|
|
809
|
+
if (language === 'rust') {
|
|
810
|
+
const fnName = `evaluate_${toSnakeCase(dtName)}`;
|
|
811
|
+
const inputStruct = `${toPascalCase(dtName)}Input`;
|
|
812
|
+
const inputArgs = dt.conditions.map(c => {
|
|
813
|
+
const hint = c.type === 'enum' && c.values.length > 0
|
|
814
|
+
? `String (${c.values.join(', ')})`
|
|
815
|
+
: c.type === 'bool' ? 'bool' : c.type === 'int_range' ? 'i64' : 'String';
|
|
816
|
+
return ` // ${c.name}: ..., // ${hint} — map from ctx`;
|
|
817
|
+
}).join('\n');
|
|
818
|
+
const outputFields = dt.actions.map(a => ` // result["${a.name}"] = Value::String(dt_result.${a.name}.clone());`).join('\n');
|
|
819
|
+
return [
|
|
820
|
+
` // Call ${fnName} to evaluate ${dtName} rules:`,
|
|
821
|
+
` // let dt_input = ${inputStruct} {`,
|
|
822
|
+
inputArgs,
|
|
823
|
+
` // };`,
|
|
824
|
+
` // if let Some(dt_result) = ${fnName}(&dt_input) {`,
|
|
825
|
+
outputFields,
|
|
826
|
+
` // }`,
|
|
827
|
+
].join('\n');
|
|
828
|
+
}
|
|
829
|
+
// TypeScript (default)
|
|
830
|
+
const fnName = `evaluate${toPascalCase(dtName)}`;
|
|
831
|
+
const inputType = `${toPascalCase(dtName)}Input`;
|
|
832
|
+
const inputArgs = dt.conditions.map(c => {
|
|
833
|
+
const hint = c.type === 'enum' && c.values.length > 0
|
|
834
|
+
? `enum: ${c.values.join(', ')}`
|
|
835
|
+
: c.type;
|
|
836
|
+
return ` // ${c.name}: /* ctx.? */ as ${inputType}['${c.name}'], // ${hint} — map from ctx`;
|
|
837
|
+
}).join('\n');
|
|
838
|
+
const outputFields = dt.actions.map(a => ` // ${a.name}: dtResult.${a.name},`).join('\n');
|
|
839
|
+
return [
|
|
840
|
+
` // Call ${fnName} to evaluate ${dtName} rules:`,
|
|
841
|
+
` // const dtResult = ${fnName}({`,
|
|
842
|
+
inputArgs,
|
|
843
|
+
` // });`,
|
|
844
|
+
` // if (dtResult !== null) {`,
|
|
845
|
+
` // return { ...ctx,`,
|
|
846
|
+
outputFields,
|
|
847
|
+
` // };`,
|
|
848
|
+
` // }`,
|
|
849
|
+
].join('\n');
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* Returns true when every DT condition name and output name exists as a
|
|
853
|
+
* context field in the machine — the full co-location contract is satisfied.
|
|
854
|
+
*/
|
|
855
|
+
function isDTFullyAligned(dt, machine) {
|
|
856
|
+
const contextNames = new Set(machine.context.map(f => f.name));
|
|
857
|
+
return dt.conditions.every(c => contextNames.has(c.name)) &&
|
|
858
|
+
dt.actions.every(a => contextNames.has(a.name));
|
|
859
|
+
}
|
|
860
|
+
/** Generate a Go type assertion for reading a context value. */
|
|
861
|
+
function goCtxRead(fieldName, condType) {
|
|
862
|
+
if (condType === 'bool')
|
|
863
|
+
return `ctx["${fieldName}"].(bool)`;
|
|
864
|
+
if (condType === 'int_range')
|
|
865
|
+
return `ctx["${fieldName}"].(int)`;
|
|
866
|
+
return `ctx["${fieldName}"].(string)`;
|
|
867
|
+
}
|
|
868
|
+
/** Generate a Rust serde_json value extraction for reading a context value. */
|
|
869
|
+
function rustCtxRead(fieldName, condType) {
|
|
870
|
+
if (condType === 'bool')
|
|
871
|
+
return `ctx["${fieldName}"].as_bool().unwrap_or_default()`;
|
|
872
|
+
if (condType === 'int_range')
|
|
873
|
+
return `ctx["${fieldName}"].as_i64().unwrap_or_default()`;
|
|
874
|
+
if (condType === 'decimal_range')
|
|
875
|
+
return `ctx["${fieldName}"].as_f64().unwrap_or_default()`;
|
|
876
|
+
return `ctx["${fieldName}"].as_str().unwrap_or_default().to_string()`;
|
|
877
|
+
}
|
|
878
|
+
function generateFullyWiredActionScaffold(action, machine, language, dt) {
|
|
879
|
+
if (language === 'python') {
|
|
880
|
+
const dtFnName = `evaluate_${toSnakeCase(dt.name)}`;
|
|
881
|
+
const inputClass = `${toPascalCase(dt.name)}Input`;
|
|
882
|
+
const inputArgs = dt.conditions
|
|
883
|
+
.map(c => ` ${c.name}=ctx['${c.name}'],`)
|
|
884
|
+
.join('\n');
|
|
885
|
+
const outputAssigns = dt.actions
|
|
886
|
+
.map(a => ` '${a.name}': dt_result.${a.name},`)
|
|
887
|
+
.join('\n');
|
|
888
|
+
return `# Action: ${action.name}
|
|
889
|
+
# Decision table: ${dt.name}
|
|
890
|
+
# Register via: machine.register_action("${action.name}", ${action.name})
|
|
891
|
+
|
|
892
|
+
from typing import Any
|
|
893
|
+
|
|
894
|
+
async def ${action.name}(ctx: dict[str, Any], event: Any = None) -> dict[str, Any]:
|
|
895
|
+
dt_result = ${dtFnName}(${inputClass}(
|
|
896
|
+
${inputArgs}
|
|
897
|
+
))
|
|
898
|
+
if dt_result is not None:
|
|
899
|
+
return {**ctx,
|
|
900
|
+
${outputAssigns}
|
|
901
|
+
}
|
|
902
|
+
return dict(ctx)
|
|
903
|
+
`;
|
|
904
|
+
}
|
|
905
|
+
if (language === 'go') {
|
|
906
|
+
const fnName = toPascalCase(action.name);
|
|
907
|
+
const dtFnName = `Evaluate${toPascalCase(dt.name)}`;
|
|
908
|
+
const inputStruct = `${toPascalCase(dt.name)}Input`;
|
|
909
|
+
const ctxReads = dt.conditions.map(c => {
|
|
910
|
+
const varName = toPascalCase(c.name).charAt(0).toLowerCase() + toPascalCase(c.name).slice(1);
|
|
911
|
+
return `\t${varName}, _ := ${goCtxRead(c.name, c.type)}`;
|
|
912
|
+
}).join('\n');
|
|
913
|
+
const inputFields = dt.conditions.map(c => {
|
|
914
|
+
const goField = toPascalCase(c.name);
|
|
915
|
+
const varName = goField.charAt(0).toLowerCase() + goField.slice(1);
|
|
916
|
+
return `\t\t${goField}: ${varName},`;
|
|
917
|
+
}).join('\n');
|
|
918
|
+
const outputAssigns = dt.actions
|
|
919
|
+
.map(a => `\t\tresult["${a.name}"] = dtResult.${toPascalCase(a.name)}`)
|
|
920
|
+
.join('\n');
|
|
921
|
+
return `// Action: ${action.name}
|
|
922
|
+
// Decision table: ${dt.name}
|
|
923
|
+
// Register via: machine.RegisterAction("${action.name}", ${fnName})
|
|
924
|
+
|
|
925
|
+
func ${fnName}(ctx orca.Context, event map[string]any) map[string]any {
|
|
926
|
+
${ctxReads}
|
|
927
|
+
\tdtResult := ${dtFnName}(${inputStruct}{
|
|
928
|
+
${inputFields}
|
|
929
|
+
\t})
|
|
930
|
+
\tresult := make(orca.Context)
|
|
931
|
+
\tfor k, v := range ctx {
|
|
932
|
+
\t\tresult[k] = v
|
|
933
|
+
\t}
|
|
934
|
+
\tif dtResult != nil {
|
|
935
|
+
${outputAssigns}
|
|
936
|
+
\t}
|
|
937
|
+
\treturn result
|
|
938
|
+
}
|
|
939
|
+
`;
|
|
940
|
+
}
|
|
941
|
+
if (language === 'rust') {
|
|
942
|
+
const dtFnName = `evaluate_${toSnakeCase(dt.name)}`;
|
|
943
|
+
const inputStruct = `${toPascalCase(dt.name)}Input`;
|
|
944
|
+
const ctxReads = dt.conditions
|
|
945
|
+
.map(c => ` let ${c.name} = ${rustCtxRead(c.name, c.type)};`)
|
|
946
|
+
.join('\n');
|
|
947
|
+
const inputFields = dt.conditions
|
|
948
|
+
.map(c => ` ${c.name},`)
|
|
949
|
+
.join('\n');
|
|
950
|
+
const outputAssigns = dt.actions
|
|
951
|
+
.map(a => {
|
|
952
|
+
const aType = a.type;
|
|
953
|
+
if (aType === 'bool')
|
|
954
|
+
return ` result["${a.name}"] = Value::Bool(dt_result.${a.name});`;
|
|
955
|
+
if (aType === 'int_range')
|
|
956
|
+
return ` result["${a.name}"] = json!(dt_result.${a.name});`;
|
|
957
|
+
if (aType === 'decimal_range')
|
|
958
|
+
return ` result["${a.name}"] = json!(dt_result.${a.name});`;
|
|
959
|
+
return ` result["${a.name}"] = Value::String(dt_result.${a.name}.clone());`;
|
|
960
|
+
})
|
|
961
|
+
.join('\n');
|
|
962
|
+
return `// Action: ${action.name}
|
|
963
|
+
// Decision table: ${dt.name}
|
|
964
|
+
// Register via: machine.register_action_rust("${action.name}", Box::new(${action.name}))
|
|
965
|
+
|
|
966
|
+
use serde_json::{Value, json};
|
|
967
|
+
|
|
968
|
+
fn ${action.name}(ctx: &Value, event: &Value) -> Value {
|
|
969
|
+
${ctxReads}
|
|
970
|
+
let dt_input = ${inputStruct} {
|
|
971
|
+
${inputFields}
|
|
972
|
+
};
|
|
973
|
+
if let Some(dt_result) = ${dtFnName}(&dt_input) {
|
|
974
|
+
let mut result = ctx.clone();
|
|
975
|
+
${outputAssigns}
|
|
976
|
+
return result;
|
|
977
|
+
}
|
|
978
|
+
ctx.clone()
|
|
979
|
+
}
|
|
980
|
+
`;
|
|
981
|
+
}
|
|
982
|
+
// TypeScript (default)
|
|
983
|
+
const dtFnName = `evaluate${toPascalCase(dt.name)}`;
|
|
984
|
+
const inputArgs = dt.conditions
|
|
985
|
+
.map(c => ` ${c.name}: ctx.${c.name},`)
|
|
986
|
+
.join('\n');
|
|
987
|
+
const outputAssigns = dt.actions
|
|
988
|
+
.map(a => ` ${a.name}: dtResult.${a.name},`)
|
|
989
|
+
.join('\n');
|
|
990
|
+
return `// Action: ${action.name}
|
|
991
|
+
// Decision table: ${dt.name}
|
|
992
|
+
|
|
993
|
+
export function ${action.name}(ctx: Context): Context {
|
|
994
|
+
const dtResult = ${dtFnName}({
|
|
995
|
+
${inputArgs}
|
|
996
|
+
});
|
|
997
|
+
if (dtResult !== null) {
|
|
998
|
+
return {
|
|
999
|
+
...ctx,
|
|
1000
|
+
${outputAssigns}
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
return { ...ctx };
|
|
1004
|
+
}
|
|
1005
|
+
`;
|
|
1006
|
+
}
|
|
1007
|
+
function generateActionScaffold(action, machine, language, matchedDT) {
|
|
606
1008
|
if (language === 'python') {
|
|
607
1009
|
if (action.hasEffect) {
|
|
608
1010
|
return `# Action: ${action.name}
|
|
@@ -619,13 +1021,17 @@ async def ${action.name}(effect: Effect) -> EffectResult:
|
|
|
619
1021
|
return EffectResult(status=EffectStatus.SUCCESS, data={})
|
|
620
1022
|
`;
|
|
621
1023
|
}
|
|
622
|
-
|
|
1024
|
+
const pyDTComment = matchedDT ? '\n' + generateDTCallComment(matchedDT, 'python') + '\n' : '';
|
|
1025
|
+
const pyTodo = matchedDT
|
|
1026
|
+
? ` # TODO: Implement action using ${matchedDT.name} decision table`
|
|
1027
|
+
: ' # TODO: Implement action';
|
|
1028
|
+
return `# Action: ${action.name}${matchedDT ? `\n# Decision table: ${matchedDT.name}` : ''}
|
|
623
1029
|
# Register via: machine.register_action("${action.name}", ${action.name})
|
|
624
1030
|
|
|
625
1031
|
from typing import Any
|
|
626
1032
|
|
|
627
|
-
async def ${action.name}(ctx: dict[str, Any], event: Any = None) -> dict[str, Any]
|
|
628
|
-
|
|
1033
|
+
async def ${action.name}(ctx: dict[str, Any], event: Any = None) -> dict[str, Any]:${pyDTComment}
|
|
1034
|
+
${pyTodo}
|
|
629
1035
|
return dict(ctx)
|
|
630
1036
|
`;
|
|
631
1037
|
}
|
|
@@ -645,17 +1051,50 @@ func ${fnName}(effect orca.Effect) orca.EffectResult {
|
|
|
645
1051
|
}
|
|
646
1052
|
`;
|
|
647
1053
|
}
|
|
648
|
-
|
|
1054
|
+
const goDTComment = matchedDT ? '\n' + generateDTCallComment(matchedDT, 'go') + '\n' : '';
|
|
1055
|
+
const goTodo = matchedDT
|
|
1056
|
+
? `\t// TODO: Implement action using ${matchedDT.name} decision table`
|
|
1057
|
+
: '\t// TODO: Implement action';
|
|
1058
|
+
return `// Action: ${action.name}${matchedDT ? `\n// Decision table: ${matchedDT.name}` : ''}
|
|
649
1059
|
// Register via: machine.RegisterAction("${action.name}", ${fnName})
|
|
650
1060
|
|
|
651
|
-
func ${fnName}(ctx orca.Context, event map[string]any) map[string]any {
|
|
652
|
-
|
|
1061
|
+
func ${fnName}(ctx orca.Context, event map[string]any) map[string]any {${goDTComment}
|
|
1062
|
+
${goTodo}
|
|
653
1063
|
\tresult := make(orca.Context)
|
|
654
1064
|
\tfor k, v := range ctx {
|
|
655
1065
|
\t\tresult[k] = v
|
|
656
1066
|
\t}
|
|
657
1067
|
\treturn result
|
|
658
1068
|
}
|
|
1069
|
+
`;
|
|
1070
|
+
}
|
|
1071
|
+
if (language === 'rust') {
|
|
1072
|
+
if (action.hasEffect) {
|
|
1073
|
+
return `// Action: ${action.name}
|
|
1074
|
+
// Effect: ${action.effectType}
|
|
1075
|
+
// Register via: machine.register_action_rust("${action.name}", Box::new(${action.name}))
|
|
1076
|
+
|
|
1077
|
+
use serde_json::Value;
|
|
1078
|
+
|
|
1079
|
+
fn ${action.name}(ctx: &Value, event: &Value) -> (Value, Effect) {
|
|
1080
|
+
// TODO: Implement effect
|
|
1081
|
+
(ctx.clone(), Effect { effect_type: "${action.effectType}".to_string(), payload: Value::Null })
|
|
1082
|
+
}
|
|
1083
|
+
`;
|
|
1084
|
+
}
|
|
1085
|
+
const rustDTComment = matchedDT ? '\n' + generateDTCallComment(matchedDT, 'rust') + '\n' : '';
|
|
1086
|
+
const rustTodo = matchedDT
|
|
1087
|
+
? ` // TODO: Implement action using ${matchedDT.name} decision table`
|
|
1088
|
+
: ' // TODO: Implement action';
|
|
1089
|
+
return `// Action: ${action.name}${matchedDT ? `\n// Decision table: ${matchedDT.name}` : ''}
|
|
1090
|
+
// Register via: machine.register_action_rust("${action.name}", Box::new(${action.name}))
|
|
1091
|
+
|
|
1092
|
+
use serde_json::Value;
|
|
1093
|
+
|
|
1094
|
+
fn ${action.name}(ctx: &Value, event: &Value) -> Value {${rustDTComment}
|
|
1095
|
+
${rustTodo}
|
|
1096
|
+
ctx.clone()
|
|
1097
|
+
}
|
|
659
1098
|
`;
|
|
660
1099
|
}
|
|
661
1100
|
// TypeScript (default)
|
|
@@ -681,10 +1120,14 @@ export function ${action.name}(${params}): [Context, Effect<${action.effectType}
|
|
|
681
1120
|
// }
|
|
682
1121
|
`;
|
|
683
1122
|
}
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
// TODO: Implement action
|
|
1123
|
+
const tsDTComment = matchedDT ? '\n' + generateDTCallComment(matchedDT, 'typescript') + '\n' : '';
|
|
1124
|
+
const tsTodo = matchedDT
|
|
1125
|
+
? ` // TODO: Implement action using ${matchedDT.name} decision table`
|
|
1126
|
+
: ' // TODO: Implement action';
|
|
1127
|
+
return `// Action: ${action.name}${matchedDT ? `\n// Decision table: ${matchedDT.name}` : ''}
|
|
1128
|
+
|
|
1129
|
+
export function ${action.name}(ctx: Context): Context {${tsDTComment}
|
|
1130
|
+
${tsTodo}
|
|
688
1131
|
return { ...ctx };
|
|
689
1132
|
}
|
|
690
1133
|
`;
|
|
@@ -1502,9 +1945,24 @@ export function compileDTSkill(input, target = 'typescript') {
|
|
|
1502
1945
|
};
|
|
1503
1946
|
}
|
|
1504
1947
|
const dt = file.decisionTables[0];
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
:
|
|
1948
|
+
let output;
|
|
1949
|
+
switch (target) {
|
|
1950
|
+
case 'json':
|
|
1951
|
+
output = compileDecisionTableToJSON(dt);
|
|
1952
|
+
break;
|
|
1953
|
+
case 'python':
|
|
1954
|
+
output = compileDecisionTableToPython(dt);
|
|
1955
|
+
break;
|
|
1956
|
+
case 'go':
|
|
1957
|
+
output = compileDecisionTableToGo(dt);
|
|
1958
|
+
break;
|
|
1959
|
+
case 'rust':
|
|
1960
|
+
output = compileDecisionTableToRust(dt);
|
|
1961
|
+
break;
|
|
1962
|
+
default:
|
|
1963
|
+
output = compileDecisionTableToTypeScript(dt);
|
|
1964
|
+
break;
|
|
1965
|
+
}
|
|
1508
1966
|
return { status: 'success', target, output, warnings: [] };
|
|
1509
1967
|
}
|
|
1510
1968
|
catch (err) {
|