@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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Decision Table Compiler
|
|
2
|
-
// Compiles verified decision tables to TypeScript evaluator functions or JSON
|
|
2
|
+
// Compiles verified decision tables to TypeScript, Python, Go evaluator functions or JSON
|
|
3
3
|
|
|
4
4
|
import { DecisionTableDef, CellValue } from '../parser/dt-ast.js';
|
|
5
5
|
|
|
@@ -11,6 +11,14 @@ function toPascalCase(name: string): string {
|
|
|
11
11
|
.join('');
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
// Convert PascalCase or camelCase to snake_case
|
|
15
|
+
export function toSnakeCase(name: string): string {
|
|
16
|
+
return name
|
|
17
|
+
.replace(/([A-Z])/g, '_$1')
|
|
18
|
+
.toLowerCase()
|
|
19
|
+
.replace(/^_/, '');
|
|
20
|
+
}
|
|
21
|
+
|
|
14
22
|
// Generate TypeScript type for input interface
|
|
15
23
|
function generateInputType(dt: DecisionTableDef): string {
|
|
16
24
|
const lines: string[] = [];
|
|
@@ -47,7 +55,7 @@ function generateTypeScriptType(type: string, values: string[]): string {
|
|
|
47
55
|
if (type === 'enum') {
|
|
48
56
|
return values.length > 0 ? values.map(v => `'${v}'`).join(' | ') : 'string';
|
|
49
57
|
}
|
|
50
|
-
if (type === 'int_range') {
|
|
58
|
+
if (type === 'int_range' || type === 'decimal_range') {
|
|
51
59
|
return 'number';
|
|
52
60
|
}
|
|
53
61
|
return 'string';
|
|
@@ -60,10 +68,12 @@ function generateConditionCheck(condName: string, condType: string, cell: CellVa
|
|
|
60
68
|
return ''; // No condition needed
|
|
61
69
|
|
|
62
70
|
case 'exact':
|
|
63
|
-
// For bool type, compare to boolean; for others, compare to string
|
|
64
71
|
if (condType === 'bool') {
|
|
65
72
|
return `input.${condName} === ${cell.value}`;
|
|
66
73
|
}
|
|
74
|
+
if (condType === 'int_range' || condType === 'decimal_range') {
|
|
75
|
+
return `input.${condName} === ${cell.value}`;
|
|
76
|
+
}
|
|
67
77
|
return `input.${condName} === '${cell.value}'`;
|
|
68
78
|
|
|
69
79
|
case 'negated':
|
|
@@ -72,7 +82,7 @@ function generateConditionCheck(condName: string, condType: string, cell: CellVa
|
|
|
72
82
|
}
|
|
73
83
|
return `input.${condName} !== '${cell.value}'`;
|
|
74
84
|
|
|
75
|
-
case 'set':
|
|
85
|
+
case 'set': {
|
|
76
86
|
const checks = cell.values.map(v => {
|
|
77
87
|
if (condType === 'bool') {
|
|
78
88
|
return `input.${condName} === ${v}`;
|
|
@@ -80,6 +90,16 @@ function generateConditionCheck(condName: string, condType: string, cell: CellVa
|
|
|
80
90
|
return `input.${condName} === '${v}'`;
|
|
81
91
|
}).join(' || ');
|
|
82
92
|
return `(${checks})`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
case 'compare':
|
|
96
|
+
return `input.${condName} ${cell.op} ${cell.value}`;
|
|
97
|
+
|
|
98
|
+
case 'range': {
|
|
99
|
+
const lowCheck = cell.lowInc ? `input.${condName} >= ${cell.low}` : `input.${condName} > ${cell.low}`;
|
|
100
|
+
const highCheck = cell.highInc ? `input.${condName} <= ${cell.high}` : `input.${condName} < ${cell.high}`;
|
|
101
|
+
return `(${lowCheck} && ${highCheck})`;
|
|
102
|
+
}
|
|
83
103
|
|
|
84
104
|
default:
|
|
85
105
|
return '';
|
|
@@ -230,3 +250,353 @@ export function compileDecisionTableToJSON(dt: DecisionTableDef): string {
|
|
|
230
250
|
|
|
231
251
|
return JSON.stringify(json, null, 2);
|
|
232
252
|
}
|
|
253
|
+
|
|
254
|
+
// ============================================================
|
|
255
|
+
// Python Compiler
|
|
256
|
+
// ============================================================
|
|
257
|
+
|
|
258
|
+
function generatePythonType(type: string): string {
|
|
259
|
+
if (type === 'bool') return 'bool';
|
|
260
|
+
if (type === 'int_range') return 'int';
|
|
261
|
+
if (type === 'decimal_range') return 'float';
|
|
262
|
+
return 'str';
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function generatePythonConditionCheck(condName: string, condType: string, cell: CellValue): string {
|
|
266
|
+
switch (cell.kind) {
|
|
267
|
+
case 'any':
|
|
268
|
+
return '';
|
|
269
|
+
case 'exact':
|
|
270
|
+
if (condType === 'bool') return `input.${condName} == ${cell.value}`;
|
|
271
|
+
if (condType === 'int_range' || condType === 'decimal_range') return `input.${condName} == ${cell.value}`;
|
|
272
|
+
return `input.${condName} == '${cell.value}'`;
|
|
273
|
+
case 'negated':
|
|
274
|
+
if (condType === 'bool') return `input.${condName} != ${cell.value}`;
|
|
275
|
+
return `input.${condName} != '${cell.value}'`;
|
|
276
|
+
case 'set': {
|
|
277
|
+
const checks = cell.values.map(v =>
|
|
278
|
+
condType === 'bool' ? `input.${condName} == ${v}` : `input.${condName} == '${v}'`
|
|
279
|
+
).join(' or ');
|
|
280
|
+
return `(${checks})`;
|
|
281
|
+
}
|
|
282
|
+
case 'compare':
|
|
283
|
+
return `input.${condName} ${cell.op} ${cell.value}`;
|
|
284
|
+
case 'range': {
|
|
285
|
+
const lowCheck = cell.lowInc ? `input.${condName} >= ${cell.low}` : `input.${condName} > ${cell.low}`;
|
|
286
|
+
const highCheck = cell.highInc ? `input.${condName} <= ${cell.high}` : `input.${condName} < ${cell.high}`;
|
|
287
|
+
return `(${lowCheck} and ${highCheck})`;
|
|
288
|
+
}
|
|
289
|
+
default:
|
|
290
|
+
return '';
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function compileDecisionTableToPython(dt: DecisionTableDef): string {
|
|
295
|
+
const inputClassName = `${toPascalCase(dt.name)}Input`;
|
|
296
|
+
const outputClassName = `${toPascalCase(dt.name)}Output`;
|
|
297
|
+
const fnName = `evaluate_${toSnakeCase(dt.name)}`;
|
|
298
|
+
|
|
299
|
+
const lines: string[] = [
|
|
300
|
+
'from typing import Optional',
|
|
301
|
+
'from dataclasses import dataclass',
|
|
302
|
+
'',
|
|
303
|
+
'',
|
|
304
|
+
'@dataclass',
|
|
305
|
+
`class ${inputClassName}:`,
|
|
306
|
+
];
|
|
307
|
+
|
|
308
|
+
for (const cond of dt.conditions) {
|
|
309
|
+
const typeStr = generatePythonType(cond.type);
|
|
310
|
+
const comment = cond.type === 'enum' && cond.values.length > 0
|
|
311
|
+
? ` # ${cond.values.join(', ')}`
|
|
312
|
+
: '';
|
|
313
|
+
lines.push(` ${cond.name}: ${typeStr}${comment}`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
lines.push('');
|
|
317
|
+
lines.push('');
|
|
318
|
+
lines.push('@dataclass');
|
|
319
|
+
lines.push(`class ${outputClassName}:`);
|
|
320
|
+
|
|
321
|
+
for (const action of dt.actions) {
|
|
322
|
+
const typeStr = generatePythonType(action.type);
|
|
323
|
+
const comment = action.type === 'enum' && action.values && action.values.length > 0
|
|
324
|
+
? ` # ${action.values.join(', ')}`
|
|
325
|
+
: '';
|
|
326
|
+
lines.push(` ${action.name}: ${typeStr}${comment}`);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
lines.push('');
|
|
330
|
+
lines.push('');
|
|
331
|
+
lines.push(`def ${fnName}(input: ${inputClassName}) -> Optional[${outputClassName}]:`);
|
|
332
|
+
|
|
333
|
+
for (let ruleIdx = 0; ruleIdx < dt.rules.length; ruleIdx++) {
|
|
334
|
+
const rule = dt.rules[ruleIdx];
|
|
335
|
+
const ruleNum = rule.number ?? ruleIdx + 1;
|
|
336
|
+
|
|
337
|
+
const checks: string[] = [];
|
|
338
|
+
for (const [condName, cell] of rule.conditions) {
|
|
339
|
+
const condDef = dt.conditions.find(c => c.name === condName);
|
|
340
|
+
const condType = condDef?.type ?? 'string';
|
|
341
|
+
const check = generatePythonConditionCheck(condName, condType, cell);
|
|
342
|
+
if (check) checks.push(check);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const actionArgs = dt.actions
|
|
346
|
+
.map(a => {
|
|
347
|
+
const value = rule.actions.get(a.name);
|
|
348
|
+
if (value === undefined) return null;
|
|
349
|
+
if (a.type === 'bool') return `${a.name}=${value}`;
|
|
350
|
+
return `${a.name}='${value}'`;
|
|
351
|
+
})
|
|
352
|
+
.filter((v): v is string => v !== null)
|
|
353
|
+
.join(', ');
|
|
354
|
+
|
|
355
|
+
lines.push(` # Rule ${ruleNum}`);
|
|
356
|
+
if (checks.length === 0) {
|
|
357
|
+
lines.push(` return ${outputClassName}(${actionArgs})`);
|
|
358
|
+
} else {
|
|
359
|
+
lines.push(` if ${checks.join(' and ')}:`);
|
|
360
|
+
lines.push(` return ${outputClassName}(${actionArgs})`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
lines.push(' return None # no rule matched');
|
|
365
|
+
|
|
366
|
+
return lines.join('\n');
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ============================================================
|
|
370
|
+
// Go Compiler
|
|
371
|
+
// ============================================================
|
|
372
|
+
|
|
373
|
+
function generateGoType(type: string): string {
|
|
374
|
+
if (type === 'bool') return 'bool';
|
|
375
|
+
if (type === 'int_range') return 'int';
|
|
376
|
+
if (type === 'decimal_range') return 'float64';
|
|
377
|
+
return 'string';
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function generateGoConditionCheck(condName: string, condType: string, cell: CellValue): string {
|
|
381
|
+
const goField = toPascalCase(condName);
|
|
382
|
+
switch (cell.kind) {
|
|
383
|
+
case 'any':
|
|
384
|
+
return '';
|
|
385
|
+
case 'exact':
|
|
386
|
+
if (condType === 'bool') return `input.${goField} == ${cell.value}`;
|
|
387
|
+
if (condType === 'int_range' || condType === 'decimal_range') return `input.${goField} == ${cell.value}`;
|
|
388
|
+
return `input.${goField} == "${cell.value}"`;
|
|
389
|
+
case 'negated':
|
|
390
|
+
if (condType === 'bool') return `input.${goField} != ${cell.value}`;
|
|
391
|
+
return `input.${goField} != "${cell.value}"`;
|
|
392
|
+
case 'set': {
|
|
393
|
+
const checks = cell.values.map(v =>
|
|
394
|
+
condType === 'bool' ? `input.${goField} == ${v}` : `input.${goField} == "${v}"`
|
|
395
|
+
).join(' || ');
|
|
396
|
+
return `(${checks})`;
|
|
397
|
+
}
|
|
398
|
+
case 'compare':
|
|
399
|
+
return `input.${goField} ${cell.op} ${cell.value}`;
|
|
400
|
+
case 'range': {
|
|
401
|
+
const lowCheck = cell.lowInc ? `input.${goField} >= ${cell.low}` : `input.${goField} > ${cell.low}`;
|
|
402
|
+
const highCheck = cell.highInc ? `input.${goField} <= ${cell.high}` : `input.${goField} < ${cell.high}`;
|
|
403
|
+
return `(${lowCheck} && ${highCheck})`;
|
|
404
|
+
}
|
|
405
|
+
default:
|
|
406
|
+
return '';
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export function compileDecisionTableToGo(dt: DecisionTableDef): string {
|
|
411
|
+
const inputTypeName = `${toPascalCase(dt.name)}Input`;
|
|
412
|
+
const outputTypeName = `${toPascalCase(dt.name)}Output`;
|
|
413
|
+
const fnName = `Evaluate${toPascalCase(dt.name)}`;
|
|
414
|
+
|
|
415
|
+
const lines: string[] = [];
|
|
416
|
+
|
|
417
|
+
lines.push(`// ${inputTypeName} defines the input conditions for the ${dt.name} decision table`);
|
|
418
|
+
lines.push(`type ${inputTypeName} struct {`);
|
|
419
|
+
for (const cond of dt.conditions) {
|
|
420
|
+
const goType = generateGoType(cond.type);
|
|
421
|
+
const fieldName = toPascalCase(cond.name);
|
|
422
|
+
const comment = cond.type === 'enum' && cond.values.length > 0
|
|
423
|
+
? ` // ${cond.values.join(', ')}`
|
|
424
|
+
: '';
|
|
425
|
+
lines.push(`\t${fieldName} ${goType}${comment}`);
|
|
426
|
+
}
|
|
427
|
+
lines.push('}');
|
|
428
|
+
lines.push('');
|
|
429
|
+
|
|
430
|
+
lines.push(`// ${outputTypeName} defines the decision outputs for the ${dt.name} decision table`);
|
|
431
|
+
lines.push(`type ${outputTypeName} struct {`);
|
|
432
|
+
for (const action of dt.actions) {
|
|
433
|
+
const goType = generateGoType(action.type);
|
|
434
|
+
const fieldName = toPascalCase(action.name);
|
|
435
|
+
const comment = action.type === 'enum' && action.values && action.values.length > 0
|
|
436
|
+
? ` // ${action.values.join(', ')}`
|
|
437
|
+
: '';
|
|
438
|
+
lines.push(`\t${fieldName} ${goType}${comment}`);
|
|
439
|
+
}
|
|
440
|
+
lines.push('}');
|
|
441
|
+
lines.push('');
|
|
442
|
+
|
|
443
|
+
const policy = dt.policy ?? 'first-match';
|
|
444
|
+
lines.push(`// ${fnName} evaluates the ${dt.name} decision table (${policy} policy)`);
|
|
445
|
+
lines.push(`func ${fnName}(input ${inputTypeName}) *${outputTypeName} {`);
|
|
446
|
+
|
|
447
|
+
for (let ruleIdx = 0; ruleIdx < dt.rules.length; ruleIdx++) {
|
|
448
|
+
const rule = dt.rules[ruleIdx];
|
|
449
|
+
const ruleNum = rule.number ?? ruleIdx + 1;
|
|
450
|
+
|
|
451
|
+
const checks: string[] = [];
|
|
452
|
+
for (const [condName, cell] of rule.conditions) {
|
|
453
|
+
const condDef = dt.conditions.find(c => c.name === condName);
|
|
454
|
+
const condType = condDef?.type ?? 'string';
|
|
455
|
+
const check = generateGoConditionCheck(condName, condType, cell);
|
|
456
|
+
if (check) checks.push(check);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const actionFields = dt.actions
|
|
460
|
+
.map(a => {
|
|
461
|
+
const value = rule.actions.get(a.name);
|
|
462
|
+
if (value === undefined) return null;
|
|
463
|
+
const fieldName = toPascalCase(a.name);
|
|
464
|
+
if (a.type === 'bool') return `${fieldName}: ${value}`;
|
|
465
|
+
return `${fieldName}: "${value}"`;
|
|
466
|
+
})
|
|
467
|
+
.filter((v): v is string => v !== null)
|
|
468
|
+
.join(', ');
|
|
469
|
+
|
|
470
|
+
lines.push(`\t// Rule ${ruleNum}`);
|
|
471
|
+
if (checks.length === 0) {
|
|
472
|
+
lines.push(`\treturn &${outputTypeName}{${actionFields}}`);
|
|
473
|
+
} else {
|
|
474
|
+
lines.push(`\tif ${checks.join(' && ')} {`);
|
|
475
|
+
lines.push(`\t\treturn &${outputTypeName}{${actionFields}}`);
|
|
476
|
+
lines.push(`\t}`);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
lines.push('\treturn nil // no rule matched');
|
|
481
|
+
lines.push('}');
|
|
482
|
+
|
|
483
|
+
return lines.join('\n');
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// ============================================================
|
|
487
|
+
// Rust Compiler
|
|
488
|
+
// ============================================================
|
|
489
|
+
|
|
490
|
+
function generateRustType(type: string): string {
|
|
491
|
+
if (type === 'bool') return 'bool';
|
|
492
|
+
if (type === 'int_range') return 'i64';
|
|
493
|
+
if (type === 'decimal_range') return 'f64';
|
|
494
|
+
return 'String';
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function generateRustConditionCheck(condName: string, condType: string, cell: CellValue): string {
|
|
498
|
+
switch (cell.kind) {
|
|
499
|
+
case 'any':
|
|
500
|
+
return '';
|
|
501
|
+
case 'exact':
|
|
502
|
+
if (condType === 'bool') return `input.${condName} == ${cell.value}`;
|
|
503
|
+
if (condType === 'int_range' || condType === 'decimal_range') return `input.${condName} == ${cell.value}`;
|
|
504
|
+
return `input.${condName} == "${cell.value}"`;
|
|
505
|
+
case 'negated':
|
|
506
|
+
if (condType === 'bool') return `input.${condName} != ${cell.value}`;
|
|
507
|
+
return `input.${condName} != "${cell.value}"`;
|
|
508
|
+
case 'set': {
|
|
509
|
+
const checks = cell.values.map(v =>
|
|
510
|
+
condType === 'bool' ? `input.${condName} == ${v}` : `input.${condName} == "${v}"`
|
|
511
|
+
).join(' || ');
|
|
512
|
+
return `(${checks})`;
|
|
513
|
+
}
|
|
514
|
+
case 'compare':
|
|
515
|
+
return `input.${condName} ${cell.op} ${cell.value}`;
|
|
516
|
+
case 'range': {
|
|
517
|
+
const lowCheck = cell.lowInc ? `input.${condName} >= ${cell.low}` : `input.${condName} > ${cell.low}`;
|
|
518
|
+
const highCheck = cell.highInc ? `input.${condName} <= ${cell.high}` : `input.${condName} < ${cell.high}`;
|
|
519
|
+
return `(${lowCheck} && ${highCheck})`;
|
|
520
|
+
}
|
|
521
|
+
default:
|
|
522
|
+
return '';
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
export function compileDecisionTableToRust(dt: DecisionTableDef): string {
|
|
527
|
+
const inputTypeName = `${toPascalCase(dt.name)}Input`;
|
|
528
|
+
const outputTypeName = `${toPascalCase(dt.name)}Output`;
|
|
529
|
+
const fnName = `evaluate_${toSnakeCase(dt.name)}`;
|
|
530
|
+
|
|
531
|
+
const lines: string[] = [];
|
|
532
|
+
|
|
533
|
+
lines.push(`/// Input conditions for the ${dt.name} decision table`);
|
|
534
|
+
lines.push('#[derive(Debug, Clone)]');
|
|
535
|
+
lines.push(`pub struct ${inputTypeName} {`);
|
|
536
|
+
for (const cond of dt.conditions) {
|
|
537
|
+
const rustType = generateRustType(cond.type);
|
|
538
|
+
const comment = cond.type === 'enum' && cond.values.length > 0
|
|
539
|
+
? ` // ${cond.values.join(', ')}`
|
|
540
|
+
: '';
|
|
541
|
+
lines.push(` pub ${cond.name}: ${rustType},${comment}`);
|
|
542
|
+
}
|
|
543
|
+
lines.push('}');
|
|
544
|
+
lines.push('');
|
|
545
|
+
|
|
546
|
+
lines.push(`/// Decision outputs for the ${dt.name} decision table`);
|
|
547
|
+
lines.push('#[derive(Debug, Clone)]');
|
|
548
|
+
lines.push(`pub struct ${outputTypeName} {`);
|
|
549
|
+
for (const action of dt.actions) {
|
|
550
|
+
const rustType = generateRustType(action.type);
|
|
551
|
+
const comment = action.type === 'enum' && action.values && action.values.length > 0
|
|
552
|
+
? ` // ${action.values.join(', ')}`
|
|
553
|
+
: '';
|
|
554
|
+
lines.push(` pub ${action.name}: ${rustType},${comment}`);
|
|
555
|
+
}
|
|
556
|
+
lines.push('}');
|
|
557
|
+
lines.push('');
|
|
558
|
+
|
|
559
|
+
const policy = dt.policy ?? 'first-match';
|
|
560
|
+
lines.push(`/// Evaluate the ${dt.name} decision table (${policy} policy)`);
|
|
561
|
+
lines.push(`pub fn ${fnName}(input: &${inputTypeName}) -> Option<${outputTypeName}> {`);
|
|
562
|
+
|
|
563
|
+
for (let ruleIdx = 0; ruleIdx < dt.rules.length; ruleIdx++) {
|
|
564
|
+
const rule = dt.rules[ruleIdx];
|
|
565
|
+
const ruleNum = rule.number ?? ruleIdx + 1;
|
|
566
|
+
|
|
567
|
+
const checks: string[] = [];
|
|
568
|
+
for (const [condName, cell] of rule.conditions) {
|
|
569
|
+
const condDef = dt.conditions.find(c => c.name === condName);
|
|
570
|
+
const condType = condDef?.type ?? 'string';
|
|
571
|
+
const check = generateRustConditionCheck(condName, condType, cell);
|
|
572
|
+
if (check) checks.push(check);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const actionFields = dt.actions
|
|
576
|
+
.map(a => {
|
|
577
|
+
const value = rule.actions.get(a.name);
|
|
578
|
+
if (value === undefined) return null;
|
|
579
|
+
const aType = a.type as string;
|
|
580
|
+
if (aType === 'bool') return `${a.name}: ${value}`;
|
|
581
|
+
if (aType === 'int_range') return `${a.name}: ${value}`;
|
|
582
|
+
if (aType === 'decimal_range') return `${a.name}: ${value}`;
|
|
583
|
+
return `${a.name}: "${value}".to_string()`;
|
|
584
|
+
})
|
|
585
|
+
.filter((v): v is string => v !== null)
|
|
586
|
+
.join(', ');
|
|
587
|
+
|
|
588
|
+
lines.push(` // Rule ${ruleNum}`);
|
|
589
|
+
if (checks.length === 0) {
|
|
590
|
+
lines.push(` return Some(${outputTypeName} { ${actionFields} });`);
|
|
591
|
+
} else {
|
|
592
|
+
lines.push(` if ${checks.join(' && ')} {`);
|
|
593
|
+
lines.push(` return Some(${outputTypeName} { ${actionFields} });`);
|
|
594
|
+
lines.push(` }`);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
lines.push(' None // no rule matched');
|
|
599
|
+
lines.push('}');
|
|
600
|
+
|
|
601
|
+
return lines.join('\n');
|
|
602
|
+
}
|
package/src/health-check.ts
CHANGED
|
@@ -186,6 +186,85 @@ async function runHealthCheck(): Promise<HealthReport> {
|
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
// ── Step 7: runtime-rust tests ──────────────────────────────────
|
|
190
|
+
{
|
|
191
|
+
const step: StepResult = { name: 'runtime-rust:test', status: 'pending', output: '', duration: 0 };
|
|
192
|
+
const start = Date.now();
|
|
193
|
+
console.log('━━━ Running runtime-rust tests ━━━');
|
|
194
|
+
const result = runCommand('cd packages/runtime-rust && cargo test 2>&1', REPO_ROOT);
|
|
195
|
+
step.duration = Date.now() - start;
|
|
196
|
+
step.status = result.status === 0 ? 'success' : 'failed';
|
|
197
|
+
step.output = result.status === 0 ? 'Tests passed' : result.stdout;
|
|
198
|
+
report.steps.push(step);
|
|
199
|
+
console.log(` ${step.status === 'success' ? '✓' : '✗'} runtime-rust tests ${step.status} (${step.duration}ms)\n`);
|
|
200
|
+
if (step.status === 'failed') {
|
|
201
|
+
report.endTime = Date.now();
|
|
202
|
+
return report;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ── Step 7b: demo-fortran ─────────────────────────────────────
|
|
207
|
+
{
|
|
208
|
+
const step: StepResult = { name: 'demo-fortran', status: 'pending', output: '', duration: 0 };
|
|
209
|
+
const start = Date.now();
|
|
210
|
+
|
|
211
|
+
// Check if gfortran is available
|
|
212
|
+
const gfortranCheck = runCommand('which gfortran 2>/dev/null', REPO_ROOT);
|
|
213
|
+
if (gfortranCheck.status !== 0) {
|
|
214
|
+
step.duration = Date.now() - start;
|
|
215
|
+
step.status = 'success';
|
|
216
|
+
step.output = 'Skipped (gfortran not installed)';
|
|
217
|
+
report.steps.push(step);
|
|
218
|
+
console.log(` ○ demo-fortran skipped (gfortran not installed) (${step.duration}ms)\n`);
|
|
219
|
+
} else {
|
|
220
|
+
console.log('━━━ Building and running demo-fortran ━━━');
|
|
221
|
+
const result = runCommand('cd packages/demo-fortran && make run 2>&1', REPO_ROOT);
|
|
222
|
+
step.duration = Date.now() - start;
|
|
223
|
+
step.status = result.status === 0 ? 'success' : 'failed';
|
|
224
|
+
step.output = result.status === 0 ? 'Demo passed' : result.stdout;
|
|
225
|
+
report.steps.push(step);
|
|
226
|
+
console.log(` ${step.status === 'success' ? '✓' : '✗'} demo-fortran ${step.status} (${step.duration}ms)\n`);
|
|
227
|
+
if (step.status === 'failed') {
|
|
228
|
+
report.endTime = Date.now();
|
|
229
|
+
return report;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ── Step 7c: demo-rust ─────────────────────────────────────────
|
|
235
|
+
{
|
|
236
|
+
const step: StepResult = { name: 'demo-rust', status: 'pending', output: '', duration: 0 };
|
|
237
|
+
const start = Date.now();
|
|
238
|
+
console.log('━━━ Running demo-rust tests ━━━');
|
|
239
|
+
const result = runCommand('cd packages/demo-rust && cargo test 2>&1', REPO_ROOT);
|
|
240
|
+
step.duration = Date.now() - start;
|
|
241
|
+
step.status = result.status === 0 ? 'success' : 'failed';
|
|
242
|
+
step.output = result.status === 0 ? 'Tests passed' : result.stdout;
|
|
243
|
+
report.steps.push(step);
|
|
244
|
+
console.log(` ${step.status === 'success' ? '✓' : '✗'} demo-rust tests ${step.status} (${step.duration}ms)\n`);
|
|
245
|
+
if (step.status === 'failed') {
|
|
246
|
+
report.endTime = Date.now();
|
|
247
|
+
return report;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ── Step 7d: demo-rust-event ───────────────────────────────────
|
|
252
|
+
{
|
|
253
|
+
const step: StepResult = { name: 'demo-rust-event', status: 'pending', output: '', duration: 0 };
|
|
254
|
+
const start = Date.now();
|
|
255
|
+
console.log('━━━ Running demo-rust-event ━━━');
|
|
256
|
+
const result = runCommand('cd packages/demo-rust-event && cargo run 2>&1', REPO_ROOT);
|
|
257
|
+
step.duration = Date.now() - start;
|
|
258
|
+
step.status = result.status === 0 ? 'success' : 'failed';
|
|
259
|
+
step.output = result.status === 0 ? 'Demo passed' : result.stdout;
|
|
260
|
+
report.steps.push(step);
|
|
261
|
+
console.log(` ${step.status === 'success' ? '✓' : '✗'} demo-rust-event ${step.status} (${step.duration}ms)\n`);
|
|
262
|
+
if (step.status === 'failed') {
|
|
263
|
+
report.endTime = Date.now();
|
|
264
|
+
return report;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
189
268
|
// ── Step 8: demo-nanolab tests ─────────────────────────────────
|
|
190
269
|
{
|
|
191
270
|
const step: StepResult = { name: 'demo-nanolab:test', status: 'pending', output: '', duration: 0 };
|
package/src/index.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { checkStructural, analyzeFile } from './verifier/structural.js';
|
|
|
22
22
|
import { checkCompleteness } from './verifier/completeness.js';
|
|
23
23
|
import { checkDeterminism } from './verifier/determinism.js';
|
|
24
24
|
import { checkProperties } from './verifier/properties.js';
|
|
25
|
+
import { computeAlignedDTOutputDomain } from './verifier/dt-verifier.js';
|
|
25
26
|
import { compileToXState, compileToXStateMachine } from './compiler/xstate.js';
|
|
26
27
|
import { compileToMermaid } from './compiler/mermaid.js';
|
|
27
28
|
import { verifySkill, compileSkill, generateActionsSkill, refineSkill, generateOrcaSkill, generateOrcaMultiSkill, parseSkill, type SkillInput } from './skills.js';
|
|
@@ -232,7 +233,10 @@ async function verify(input: SkillInput, json: boolean = false): Promise<void> {
|
|
|
232
233
|
const structural = checkStructural(machine);
|
|
233
234
|
const completeness = checkCompleteness(machine);
|
|
234
235
|
const determinism = checkDeterminism(machine);
|
|
235
|
-
const
|
|
236
|
+
const dtOutputDomain = file.decisionTables.length > 0
|
|
237
|
+
? computeAlignedDTOutputDomain({ machines: [machine], decisionTables: file.decisionTables })
|
|
238
|
+
: undefined;
|
|
239
|
+
const properties = checkProperties(machine, { dtOutputDomain });
|
|
236
240
|
|
|
237
241
|
const allErrors = [
|
|
238
242
|
...structural.errors,
|
|
@@ -101,7 +101,8 @@ function emitStates(states: StateDef[], level: number): string[] {
|
|
|
101
101
|
if (state.onEntry) lines.push(`- on_entry: ${state.onEntry}`);
|
|
102
102
|
if (state.onExit) lines.push(`- on_exit: ${state.onExit}`);
|
|
103
103
|
if (state.timeout) lines.push(`- timeout: ${state.timeout.duration} -> ${state.timeout.target}`);
|
|
104
|
-
if (state.
|
|
104
|
+
if (state.ignoredAll) lines.push(`- ignore: *`);
|
|
105
|
+
else if (state.ignoredEvents?.length) lines.push(`- ignore: ${state.ignoredEvents.join(', ')}`);
|
|
105
106
|
if (state.onDone) lines.push(`- on_done: -> ${state.onDone}`);
|
|
106
107
|
|
|
107
108
|
if (state.parallel) {
|
package/src/parser/ast.ts
CHANGED
|
@@ -120,6 +120,7 @@ export interface StateDef {
|
|
|
120
120
|
parent?: string; // Parent state name for hierarchical states
|
|
121
121
|
transitions?: Transition[];
|
|
122
122
|
ignoredEvents?: string[];
|
|
123
|
+
ignoredAll?: boolean; // true when "- ignore: *" — all unhandled events discarded
|
|
123
124
|
}
|
|
124
125
|
|
|
125
126
|
export interface Transition {
|
package/src/parser/dt-ast.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Decision Table AST Types
|
|
2
2
|
|
|
3
|
-
export type ConditionType = 'bool' | 'enum' | 'int_range' | 'string';
|
|
3
|
+
export type ConditionType = 'bool' | 'enum' | 'int_range' | 'decimal_range' | 'string';
|
|
4
4
|
|
|
5
5
|
export interface ConditionDef {
|
|
6
6
|
name: string;
|
|
@@ -22,7 +22,9 @@ export type CellValue =
|
|
|
22
22
|
| { kind: 'any' } // "-" wildcard
|
|
23
23
|
| { kind: 'exact'; value: string } // exact match
|
|
24
24
|
| { kind: 'negated'; value: string } // "!value"
|
|
25
|
-
| { kind: 'set'; values: string[] }
|
|
25
|
+
| { kind: 'set'; values: string[] } // "a,b" (match any in set)
|
|
26
|
+
| { kind: 'compare'; op: '>' | '>=' | '<' | '<='; value: number } // >=750, <0.3
|
|
27
|
+
| { kind: 'range'; low: number; high: number; lowInc: boolean; highInc: boolean }; // 700..749, 0.3-0.4
|
|
26
28
|
|
|
27
29
|
export interface Rule {
|
|
28
30
|
number?: number; // optional rule # from the # column
|
package/src/parser/dt-parser.ts
CHANGED
|
@@ -21,7 +21,37 @@ function findColumnIndex(headers: string[], name: string): number {
|
|
|
21
21
|
|
|
22
22
|
// --- Cell Value Parsing ---
|
|
23
23
|
|
|
24
|
-
function
|
|
24
|
+
function parseNumericCell(text: string): CellValue | null {
|
|
25
|
+
// Suffix form: 750+ means >=750
|
|
26
|
+
const suffixPlus = text.match(/^(-?\d+(?:\.\d+)?)\+$/);
|
|
27
|
+
if (suffixPlus) {
|
|
28
|
+
return { kind: 'compare', op: '>=', value: parseFloat(suffixPlus[1]) };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Comparison operators: >=750, <=0.3, >100, <600
|
|
32
|
+
const cmpMatch = text.match(/^(>=|<=|>|<)\s*(-?\d+(?:\.\d+)?)$/);
|
|
33
|
+
if (cmpMatch) {
|
|
34
|
+
return { kind: 'compare', op: cmpMatch[1] as '>' | '>=' | '<' | '<=', value: parseFloat(cmpMatch[2]) };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Range with .. separator: 1..50 (inclusive both ends)
|
|
38
|
+
const dotRange = text.match(/^(-?\d+(?:\.\d+)?)\s*\.\.\s*(-?\d+(?:\.\d+)?)$/);
|
|
39
|
+
if (dotRange) {
|
|
40
|
+
return { kind: 'range', low: parseFloat(dotRange[1]), high: parseFloat(dotRange[2]), lowInc: true, highInc: true };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Range with - separator for numbers: 700-749, 0.3-0.4
|
|
44
|
+
// Must have at least one side that looks numeric (digit-starting).
|
|
45
|
+
// Negative numbers on the left are ambiguous with subtraction — require left >= 0.
|
|
46
|
+
const dashRange = text.match(/^(\d+(?:\.\d+)?)\s*-\s*(\d+(?:\.\d+)?)$/);
|
|
47
|
+
if (dashRange) {
|
|
48
|
+
return { kind: 'range', low: parseFloat(dashRange[1]), high: parseFloat(dashRange[2]), lowInc: true, highInc: true };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseCellValue(text: string | undefined, condType?: ConditionType): CellValue {
|
|
25
55
|
if (!text || text.trim() === '' || text.trim() === '-') {
|
|
26
56
|
return { kind: 'any' };
|
|
27
57
|
}
|
|
@@ -36,6 +66,12 @@ function parseCellValue(text: string | undefined): CellValue {
|
|
|
36
66
|
}
|
|
37
67
|
}
|
|
38
68
|
|
|
69
|
+
// Numeric range/compare patterns — only attempt for numeric condition types
|
|
70
|
+
if (condType === 'int_range' || condType === 'decimal_range') {
|
|
71
|
+
const numeric = parseNumericCell(trimmed);
|
|
72
|
+
if (numeric) return numeric;
|
|
73
|
+
}
|
|
74
|
+
|
|
39
75
|
// Set: a,b,c
|
|
40
76
|
if (trimmed.includes(',')) {
|
|
41
77
|
return { kind: 'set', values: trimmed.split(',').map(v => v.trim()).filter(Boolean) };
|
|
@@ -65,11 +101,11 @@ function parseConditionsTable(table: MdTable): ConditionDef[] {
|
|
|
65
101
|
|
|
66
102
|
if (type === 'bool') {
|
|
67
103
|
values = valuesStr.trim() ? valuesStr.split(',').map(v => v.trim()) : ['true', 'false'];
|
|
68
|
-
} else if (type === 'int_range') {
|
|
69
|
-
// Parse min..max format
|
|
70
|
-
const rangeMatch = valuesStr.match(/(
|
|
104
|
+
} else if (type === 'int_range' || type === 'decimal_range') {
|
|
105
|
+
// Parse min..max format (integers or decimals)
|
|
106
|
+
const rangeMatch = valuesStr.match(/(-?\d+(?:\.\d+)?)\s*\.\.\s*(-?\d+(?:\.\d+)?)/);
|
|
71
107
|
if (rangeMatch) {
|
|
72
|
-
range = { min:
|
|
108
|
+
range = { min: parseFloat(rangeMatch[1]), max: parseFloat(rangeMatch[2]) };
|
|
73
109
|
values = [];
|
|
74
110
|
} else {
|
|
75
111
|
values = [];
|
|
@@ -116,7 +152,8 @@ function parseActionsTable(table: MdTable): ActionOutputDef[] {
|
|
|
116
152
|
function parseRulesTable(
|
|
117
153
|
table: MdTable,
|
|
118
154
|
conditionNames: Set<string>,
|
|
119
|
-
actionNames: Set<string
|
|
155
|
+
actionNames: Set<string>,
|
|
156
|
+
conditionDefs?: ConditionDef[]
|
|
120
157
|
): { rules: Rule[]; warnings: string[] } {
|
|
121
158
|
const warnings: string[] = [];
|
|
122
159
|
const rules: Rule[] = [];
|
|
@@ -167,7 +204,8 @@ function parseRulesTable(
|
|
|
167
204
|
rule.number = num;
|
|
168
205
|
}
|
|
169
206
|
} else if (col.type === 'condition') {
|
|
170
|
-
const
|
|
207
|
+
const condDef = conditionDefs?.find(c => c.name === col.name);
|
|
208
|
+
const cellValue = parseCellValue(cell, condDef?.type);
|
|
171
209
|
rule.conditions.set(col.name, cellValue);
|
|
172
210
|
} else if (col.type === 'action') {
|
|
173
211
|
const value = cell?.trim() || '';
|
|
@@ -254,7 +292,7 @@ export function parseDecisionTable(elements: MdElement[]): { decisionTable: Deci
|
|
|
254
292
|
} else if (currentSection === 'actions') {
|
|
255
293
|
actions = parseActionsTable(el);
|
|
256
294
|
} else if (currentSection === 'rules') {
|
|
257
|
-
const result = parseRulesTable(el, new Set(conditions.map(c => c.name)), new Set(actions.map(a => a.name)));
|
|
295
|
+
const result = parseRulesTable(el, new Set(conditions.map(c => c.name)), new Set(actions.map(a => a.name)), conditions);
|
|
258
296
|
rules = result.rules;
|
|
259
297
|
warnings.push(...result.warnings);
|
|
260
298
|
}
|