@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.
Files changed (53) hide show
  1. package/dist/compiler/dt-compiler.d.ts +4 -0
  2. package/dist/compiler/dt-compiler.d.ts.map +1 -1
  3. package/dist/compiler/dt-compiler.js +354 -4
  4. package/dist/compiler/dt-compiler.js.map +1 -1
  5. package/dist/health-check.js +75 -0
  6. package/dist/health-check.js.map +1 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +5 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/parser/ast-to-markdown.d.ts.map +1 -1
  11. package/dist/parser/ast-to-markdown.js +3 -1
  12. package/dist/parser/ast-to-markdown.js.map +1 -1
  13. package/dist/parser/ast.d.ts +1 -0
  14. package/dist/parser/ast.d.ts.map +1 -1
  15. package/dist/parser/dt-ast.d.ts +11 -1
  16. package/dist/parser/dt-ast.d.ts.map +1 -1
  17. package/dist/parser/dt-parser.d.ts.map +1 -1
  18. package/dist/parser/dt-parser.js +40 -8
  19. package/dist/parser/dt-parser.js.map +1 -1
  20. package/dist/parser/markdown-parser.d.ts.map +1 -1
  21. package/dist/parser/markdown-parser.js +14 -4
  22. package/dist/parser/markdown-parser.js.map +1 -1
  23. package/dist/skills.d.ts +3 -2
  24. package/dist/skills.d.ts.map +1 -1
  25. package/dist/skills.js +486 -28
  26. package/dist/skills.js.map +1 -1
  27. package/dist/tools.js +4 -4
  28. package/dist/tools.js.map +1 -1
  29. package/dist/verifier/dt-verifier.d.ts +28 -1
  30. package/dist/verifier/dt-verifier.d.ts.map +1 -1
  31. package/dist/verifier/dt-verifier.js +591 -32
  32. package/dist/verifier/dt-verifier.js.map +1 -1
  33. package/dist/verifier/properties.d.ts +4 -0
  34. package/dist/verifier/properties.d.ts.map +1 -1
  35. package/dist/verifier/properties.js +56 -20
  36. package/dist/verifier/properties.js.map +1 -1
  37. package/dist/verifier/structural.d.ts.map +1 -1
  38. package/dist/verifier/structural.js +6 -1
  39. package/dist/verifier/structural.js.map +1 -1
  40. package/package.json +1 -1
  41. package/src/compiler/dt-compiler.ts +374 -4
  42. package/src/health-check.ts +79 -0
  43. package/src/index.ts +5 -1
  44. package/src/parser/ast-to-markdown.ts +2 -1
  45. package/src/parser/ast.ts +1 -0
  46. package/src/parser/dt-ast.ts +4 -2
  47. package/src/parser/dt-parser.ts +46 -8
  48. package/src/parser/markdown-parser.ts +11 -3
  49. package/src/skills.ts +520 -30
  50. package/src/tools.ts +4 -4
  51. package/src/verifier/dt-verifier.ts +639 -30
  52. package/src/verifier/properties.ts +78 -23
  53. 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
+ }
@@ -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 properties = checkProperties(machine);
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.ignoredEvents?.length) lines.push(`- ignore: ${state.ignoredEvents.join(', ')}`);
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 {
@@ -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[] }; // "a,b" (match any in set)
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
@@ -21,7 +21,37 @@ function findColumnIndex(headers: string[], name: string): number {
21
21
 
22
22
  // --- Cell Value Parsing ---
23
23
 
24
- function parseCellValue(text: string | undefined): CellValue {
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(/(\d+)\s*\.\.\s*(\d+)/);
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: parseInt(rangeMatch[1], 10), max: parseInt(rangeMatch[2], 10) };
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 cellValue = parseCellValue(cell);
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
  }