@orcalang/orca-lang 0.1.21 → 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 +1 -0
- package/dist/compiler/dt-compiler.d.ts.map +1 -1
- package/dist/compiler/dt-compiler.js +149 -3
- 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/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/skills.d.ts +2 -2
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +200 -6
- 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.map +1 -1
- package/dist/verifier/dt-verifier.js +259 -31
- package/dist/verifier/dt-verifier.js.map +1 -1
- package/package.json +1 -1
- package/src/compiler/dt-compiler.ts +151 -3
- package/src/health-check.ts +79 -0
- package/src/parser/dt-ast.ts +4 -2
- package/src/parser/dt-parser.ts +46 -8
- package/src/skills.ts +201 -7
- package/src/tools.ts +4 -4
- package/src/verifier/dt-verifier.ts +272 -29
|
@@ -55,7 +55,7 @@ function generateTypeScriptType(type: string, values: string[]): string {
|
|
|
55
55
|
if (type === 'enum') {
|
|
56
56
|
return values.length > 0 ? values.map(v => `'${v}'`).join(' | ') : 'string';
|
|
57
57
|
}
|
|
58
|
-
if (type === 'int_range') {
|
|
58
|
+
if (type === 'int_range' || type === 'decimal_range') {
|
|
59
59
|
return 'number';
|
|
60
60
|
}
|
|
61
61
|
return 'string';
|
|
@@ -68,10 +68,12 @@ function generateConditionCheck(condName: string, condType: string, cell: CellVa
|
|
|
68
68
|
return ''; // No condition needed
|
|
69
69
|
|
|
70
70
|
case 'exact':
|
|
71
|
-
// For bool type, compare to boolean; for others, compare to string
|
|
72
71
|
if (condType === 'bool') {
|
|
73
72
|
return `input.${condName} === ${cell.value}`;
|
|
74
73
|
}
|
|
74
|
+
if (condType === 'int_range' || condType === 'decimal_range') {
|
|
75
|
+
return `input.${condName} === ${cell.value}`;
|
|
76
|
+
}
|
|
75
77
|
return `input.${condName} === '${cell.value}'`;
|
|
76
78
|
|
|
77
79
|
case 'negated':
|
|
@@ -80,7 +82,7 @@ function generateConditionCheck(condName: string, condType: string, cell: CellVa
|
|
|
80
82
|
}
|
|
81
83
|
return `input.${condName} !== '${cell.value}'`;
|
|
82
84
|
|
|
83
|
-
case 'set':
|
|
85
|
+
case 'set': {
|
|
84
86
|
const checks = cell.values.map(v => {
|
|
85
87
|
if (condType === 'bool') {
|
|
86
88
|
return `input.${condName} === ${v}`;
|
|
@@ -88,6 +90,16 @@ function generateConditionCheck(condName: string, condType: string, cell: CellVa
|
|
|
88
90
|
return `input.${condName} === '${v}'`;
|
|
89
91
|
}).join(' || ');
|
|
90
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
|
+
}
|
|
91
103
|
|
|
92
104
|
default:
|
|
93
105
|
return '';
|
|
@@ -246,6 +258,7 @@ export function compileDecisionTableToJSON(dt: DecisionTableDef): string {
|
|
|
246
258
|
function generatePythonType(type: string): string {
|
|
247
259
|
if (type === 'bool') return 'bool';
|
|
248
260
|
if (type === 'int_range') return 'int';
|
|
261
|
+
if (type === 'decimal_range') return 'float';
|
|
249
262
|
return 'str';
|
|
250
263
|
}
|
|
251
264
|
|
|
@@ -255,6 +268,7 @@ function generatePythonConditionCheck(condName: string, condType: string, cell:
|
|
|
255
268
|
return '';
|
|
256
269
|
case 'exact':
|
|
257
270
|
if (condType === 'bool') return `input.${condName} == ${cell.value}`;
|
|
271
|
+
if (condType === 'int_range' || condType === 'decimal_range') return `input.${condName} == ${cell.value}`;
|
|
258
272
|
return `input.${condName} == '${cell.value}'`;
|
|
259
273
|
case 'negated':
|
|
260
274
|
if (condType === 'bool') return `input.${condName} != ${cell.value}`;
|
|
@@ -265,6 +279,13 @@ function generatePythonConditionCheck(condName: string, condType: string, cell:
|
|
|
265
279
|
).join(' or ');
|
|
266
280
|
return `(${checks})`;
|
|
267
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
|
+
}
|
|
268
289
|
default:
|
|
269
290
|
return '';
|
|
270
291
|
}
|
|
@@ -352,6 +373,7 @@ export function compileDecisionTableToPython(dt: DecisionTableDef): string {
|
|
|
352
373
|
function generateGoType(type: string): string {
|
|
353
374
|
if (type === 'bool') return 'bool';
|
|
354
375
|
if (type === 'int_range') return 'int';
|
|
376
|
+
if (type === 'decimal_range') return 'float64';
|
|
355
377
|
return 'string';
|
|
356
378
|
}
|
|
357
379
|
|
|
@@ -362,6 +384,7 @@ function generateGoConditionCheck(condName: string, condType: string, cell: Cell
|
|
|
362
384
|
return '';
|
|
363
385
|
case 'exact':
|
|
364
386
|
if (condType === 'bool') return `input.${goField} == ${cell.value}`;
|
|
387
|
+
if (condType === 'int_range' || condType === 'decimal_range') return `input.${goField} == ${cell.value}`;
|
|
365
388
|
return `input.${goField} == "${cell.value}"`;
|
|
366
389
|
case 'negated':
|
|
367
390
|
if (condType === 'bool') return `input.${goField} != ${cell.value}`;
|
|
@@ -372,6 +395,13 @@ function generateGoConditionCheck(condName: string, condType: string, cell: Cell
|
|
|
372
395
|
).join(' || ');
|
|
373
396
|
return `(${checks})`;
|
|
374
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
|
+
}
|
|
375
405
|
default:
|
|
376
406
|
return '';
|
|
377
407
|
}
|
|
@@ -452,3 +482,121 @@ export function compileDecisionTableToGo(dt: DecisionTableDef): string {
|
|
|
452
482
|
|
|
453
483
|
return lines.join('\n');
|
|
454
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/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
|
}
|