@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
|
@@ -14,7 +14,7 @@ function getConditionValues(condition: ConditionDef): string[] {
|
|
|
14
14
|
return condition.values;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
// Check if a cell matches a given value
|
|
17
|
+
// Check if a cell matches a given value (string or numeric)
|
|
18
18
|
function cellMatches(cell: CellValue, value: string): boolean {
|
|
19
19
|
switch (cell.kind) {
|
|
20
20
|
case 'any':
|
|
@@ -25,6 +25,24 @@ function cellMatches(cell: CellValue, value: string): boolean {
|
|
|
25
25
|
return cell.value !== value;
|
|
26
26
|
case 'set':
|
|
27
27
|
return cell.values.includes(value);
|
|
28
|
+
case 'compare': {
|
|
29
|
+
const num = parseFloat(value);
|
|
30
|
+
if (isNaN(num)) return false;
|
|
31
|
+
switch (cell.op) {
|
|
32
|
+
case '>': return num > cell.value;
|
|
33
|
+
case '>=': return num >= cell.value;
|
|
34
|
+
case '<': return num < cell.value;
|
|
35
|
+
case '<=': return num <= cell.value;
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
case 'range': {
|
|
40
|
+
const num = parseFloat(value);
|
|
41
|
+
if (isNaN(num)) return false;
|
|
42
|
+
const aboveLow = cell.lowInc ? num >= cell.low : num > cell.low;
|
|
43
|
+
const belowHigh = cell.highInc ? num <= cell.high : num < cell.high;
|
|
44
|
+
return aboveLow && belowHigh;
|
|
45
|
+
}
|
|
28
46
|
}
|
|
29
47
|
}
|
|
30
48
|
|
|
@@ -103,6 +121,44 @@ function cellsIntersect(cell1: CellValue, cell2: CellValue): boolean {
|
|
|
103
121
|
return cell1.values.some(v => cell2.values.includes(v));
|
|
104
122
|
}
|
|
105
123
|
|
|
124
|
+
// --- Numeric cell intersection helpers ---
|
|
125
|
+
|
|
126
|
+
// Convert a cell to a numeric interval [low, high] (inclusive flags)
|
|
127
|
+
// Returns null for non-numeric kinds.
|
|
128
|
+
function toInterval(cell: CellValue): { low: number; high: number; lowInc: boolean; highInc: boolean } | null {
|
|
129
|
+
if (cell.kind === 'range') {
|
|
130
|
+
return { low: cell.low, high: cell.high, lowInc: cell.lowInc, highInc: cell.highInc };
|
|
131
|
+
}
|
|
132
|
+
if (cell.kind === 'compare') {
|
|
133
|
+
switch (cell.op) {
|
|
134
|
+
case '>=': return { low: cell.value, high: Infinity, lowInc: true, highInc: false };
|
|
135
|
+
case '>': return { low: cell.value, high: Infinity, lowInc: false, highInc: false };
|
|
136
|
+
case '<=': return { low: -Infinity, high: cell.value, lowInc: false, highInc: true };
|
|
137
|
+
case '<': return { low: -Infinity, high: cell.value, lowInc: false, highInc: false };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (cell.kind === 'exact') {
|
|
141
|
+
const n = parseFloat(cell.value);
|
|
142
|
+
if (!isNaN(n)) return { low: n, high: n, lowInc: true, highInc: true };
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function intervalsOverlap(
|
|
148
|
+
a: { low: number; high: number; lowInc: boolean; highInc: boolean },
|
|
149
|
+
b: { low: number; high: number; lowInc: boolean; highInc: boolean }
|
|
150
|
+
): boolean {
|
|
151
|
+
// a.high < b.low or a.high == b.low and at least one exclusive → no overlap
|
|
152
|
+
if (a.high < b.low || b.high < a.low) return false;
|
|
153
|
+
if (a.high === b.low && !(a.highInc && b.lowInc)) return false;
|
|
154
|
+
if (b.high === a.low && !(b.highInc && a.lowInc)) return false;
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const iv1 = toInterval(cell1);
|
|
159
|
+
const iv2 = toInterval(cell2);
|
|
160
|
+
if (iv1 && iv2) return intervalsOverlap(iv1, iv2);
|
|
161
|
+
|
|
106
162
|
return true;
|
|
107
163
|
}
|
|
108
164
|
|
|
@@ -345,7 +401,6 @@ function checkStructural(dt: DecisionTableDef): VerificationError[] {
|
|
|
345
401
|
|
|
346
402
|
function cellsEqual(cell1: CellValue, cell2: CellValue): boolean {
|
|
347
403
|
if (cell1.kind !== cell2.kind) return false;
|
|
348
|
-
// Now cell1 and cell2 have the same kind, narrow both
|
|
349
404
|
const kind = cell1.kind;
|
|
350
405
|
if (kind === 'any') return true;
|
|
351
406
|
if (kind === 'exact') {
|
|
@@ -359,6 +414,16 @@ function cellsEqual(cell1: CellValue, cell2: CellValue): boolean {
|
|
|
359
414
|
const s2 = cell2 as { kind: 'set'; values: string[] };
|
|
360
415
|
return s1.values.length === s2.values.length && s1.values.every(v => s2.values.includes(v));
|
|
361
416
|
}
|
|
417
|
+
if (kind === 'compare') {
|
|
418
|
+
const c1 = cell1 as { kind: 'compare'; op: string; value: number };
|
|
419
|
+
const c2 = cell2 as { kind: 'compare'; op: string; value: number };
|
|
420
|
+
return c1.op === c2.op && c1.value === c2.value;
|
|
421
|
+
}
|
|
422
|
+
if (kind === 'range') {
|
|
423
|
+
const r1 = cell1 as { kind: 'range'; low: number; high: number; lowInc: boolean; highInc: boolean };
|
|
424
|
+
const r2 = cell2 as { kind: 'range'; low: number; high: number; lowInc: boolean; highInc: boolean };
|
|
425
|
+
return r1.low === r2.low && r1.high === r2.high && r1.lowInc === r2.lowInc && r1.highInc === r2.highInc;
|
|
426
|
+
}
|
|
362
427
|
return true;
|
|
363
428
|
}
|
|
364
429
|
|
|
@@ -366,26 +431,174 @@ function cellsEqual(cell1: CellValue, cell2: CellValue): boolean {
|
|
|
366
431
|
// Completeness Check
|
|
367
432
|
// ============================================================
|
|
368
433
|
|
|
434
|
+
// --- Interval-based completeness for numeric conditions ---
|
|
435
|
+
|
|
436
|
+
interface Interval {
|
|
437
|
+
low: number;
|
|
438
|
+
high: number;
|
|
439
|
+
lowInc: boolean;
|
|
440
|
+
highInc: boolean;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** Convert a CellValue to a list of intervals it covers within the given domain. */
|
|
444
|
+
function cellToIntervals(cell: CellValue, domain: Interval): Interval[] {
|
|
445
|
+
switch (cell.kind) {
|
|
446
|
+
case 'any':
|
|
447
|
+
return [domain];
|
|
448
|
+
case 'compare':
|
|
449
|
+
switch (cell.op) {
|
|
450
|
+
case '>=': return [{ low: Math.max(cell.value, domain.low), high: domain.high, lowInc: cell.value >= domain.low, highInc: domain.highInc }];
|
|
451
|
+
case '>': return [{ low: Math.max(cell.value, domain.low), high: domain.high, lowInc: false, highInc: domain.highInc }];
|
|
452
|
+
case '<=': return [{ low: domain.low, high: Math.min(cell.value, domain.high), lowInc: domain.lowInc, highInc: cell.value <= domain.high }];
|
|
453
|
+
case '<': return [{ low: domain.low, high: Math.min(cell.value, domain.high), lowInc: domain.lowInc, highInc: false }];
|
|
454
|
+
}
|
|
455
|
+
return [];
|
|
456
|
+
case 'range':
|
|
457
|
+
return [{
|
|
458
|
+
low: Math.max(cell.low, domain.low),
|
|
459
|
+
high: Math.min(cell.high, domain.high),
|
|
460
|
+
lowInc: cell.low > domain.low ? cell.lowInc : domain.lowInc,
|
|
461
|
+
highInc: cell.high < domain.high ? cell.highInc : domain.highInc,
|
|
462
|
+
}];
|
|
463
|
+
case 'exact': {
|
|
464
|
+
const v = parseFloat(cell.value);
|
|
465
|
+
if (isNaN(v)) return [];
|
|
466
|
+
if (v >= domain.low && v <= domain.high) return [{ low: v, high: v, lowInc: true, highInc: true }];
|
|
467
|
+
return [];
|
|
468
|
+
}
|
|
469
|
+
default:
|
|
470
|
+
return [];
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/** Check if a sorted list of non-overlapping intervals covers the entire domain without gaps.
|
|
475
|
+
* For integer types, adjacent integers (e.g., [a,N] and [N+1,b]) are treated as contiguous. */
|
|
476
|
+
function checkIntervalCoverage(intervals: Interval[], domain: Interval, isInteger: boolean): Interval[] {
|
|
477
|
+
if (intervals.length === 0) return [domain];
|
|
478
|
+
|
|
479
|
+
// Sort by low bound, then by inclusive (inclusive first)
|
|
480
|
+
const sorted = [...intervals].sort((a, b) => {
|
|
481
|
+
if (a.low !== b.low) return a.low - b.low;
|
|
482
|
+
if (a.lowInc && !b.lowInc) return -1;
|
|
483
|
+
if (!a.lowInc && b.lowInc) return 1;
|
|
484
|
+
return 0;
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
// Merge overlapping/adjacent intervals
|
|
488
|
+
const merged: Interval[] = [sorted[0]];
|
|
489
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
490
|
+
const prev = merged[merged.length - 1];
|
|
491
|
+
const curr = sorted[i];
|
|
492
|
+
// Adjacent or overlapping: prev.high >= curr.low (or == with at least one inclusive)
|
|
493
|
+
// For integers: [a, N] and [N+1, b] are adjacent
|
|
494
|
+
const adjacent = prev.high > curr.low
|
|
495
|
+
|| (prev.high === curr.low && (prev.highInc || curr.lowInc))
|
|
496
|
+
|| (isInteger && prev.highInc && curr.lowInc && curr.low === prev.high + 1);
|
|
497
|
+
if (adjacent) {
|
|
498
|
+
if (curr.high > prev.high) {
|
|
499
|
+
prev.high = curr.high;
|
|
500
|
+
prev.highInc = curr.highInc;
|
|
501
|
+
} else if (curr.high === prev.high) {
|
|
502
|
+
prev.highInc = prev.highInc || curr.highInc;
|
|
503
|
+
}
|
|
504
|
+
} else {
|
|
505
|
+
merged.push({ ...curr });
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Check for gaps against domain
|
|
510
|
+
const gaps: Interval[] = [];
|
|
511
|
+
let cursor = domain.low;
|
|
512
|
+
let cursorInc = domain.lowInc;
|
|
513
|
+
|
|
514
|
+
for (const iv of merged) {
|
|
515
|
+
// Is there a gap between cursor and this interval's start?
|
|
516
|
+
if (iv.low > cursor || (iv.low === cursor && !iv.lowInc && cursorInc)) {
|
|
517
|
+
// Gap starts just after the cursor (cursor itself is covered if cursorInc)
|
|
518
|
+
gaps.push({ low: cursor, high: iv.low, lowInc: !cursorInc, highInc: !iv.lowInc });
|
|
519
|
+
}
|
|
520
|
+
if (iv.high > cursor || (iv.high === cursor && iv.highInc && !cursorInc)) {
|
|
521
|
+
cursor = iv.high;
|
|
522
|
+
cursorInc = iv.highInc;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (cursor < domain.high || (cursor === domain.high && !cursorInc && domain.highInc)) {
|
|
527
|
+
gaps.push({ low: cursor, high: domain.high, lowInc: !cursorInc, highInc: domain.highInc });
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
return gaps;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/** Check completeness of numeric conditions on a single numeric axis.
|
|
534
|
+
* For each numeric condition, project the rules onto that axis (treating all
|
|
535
|
+
* other conditions as wildcards) and verify there are no gaps in the domain. */
|
|
536
|
+
function checkNumericCompleteness(dt: DecisionTableDef): VerificationError[] {
|
|
537
|
+
const errors: VerificationError[] = [];
|
|
538
|
+
|
|
539
|
+
for (const cond of dt.conditions) {
|
|
540
|
+
if (cond.type !== 'int_range' && cond.type !== 'decimal_range') continue;
|
|
541
|
+
if (!cond.range) {
|
|
542
|
+
errors.push({
|
|
543
|
+
code: 'DT_COMPLETENESS_SKIPPED',
|
|
544
|
+
message: `Completeness check skipped for '${cond.name}': no domain range declared (use Values column e.g. "0..1000")`,
|
|
545
|
+
severity: 'warning',
|
|
546
|
+
location: { decisionTable: dt.name, condition: cond.name },
|
|
547
|
+
suggestion: `Add a domain range to the conditions table, e.g. "0..1000" in the Values column`,
|
|
548
|
+
});
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const domain: Interval = { low: cond.range.min, high: cond.range.max, lowInc: true, highInc: true };
|
|
553
|
+
|
|
554
|
+
// Collect all intervals from rules for this condition
|
|
555
|
+
const allIntervals: Interval[] = [];
|
|
556
|
+
for (const rule of dt.rules) {
|
|
557
|
+
const cell = rule.conditions.get(cond.name);
|
|
558
|
+
if (!cell) {
|
|
559
|
+
// No condition means wildcard — covers entire domain
|
|
560
|
+
allIntervals.push(domain);
|
|
561
|
+
} else {
|
|
562
|
+
allIntervals.push(...cellToIntervals(cell, domain));
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const gaps = checkIntervalCoverage(allIntervals, domain, cond.type === 'int_range');
|
|
567
|
+
for (const gap of gaps) {
|
|
568
|
+
const lowBound = gap.lowInc ? '[' : '(';
|
|
569
|
+
const highBound = gap.highInc ? ']' : ')';
|
|
570
|
+
errors.push({
|
|
571
|
+
code: 'DT_INCOMPLETE',
|
|
572
|
+
message: `Numeric condition '${cond.name}' has uncovered range ${lowBound}${gap.low}, ${gap.high}${highBound}`,
|
|
573
|
+
severity: 'error',
|
|
574
|
+
location: { decisionTable: dt.name, condition: cond.name },
|
|
575
|
+
suggestion: `Add a rule covering the range ${lowBound}${gap.low}, ${gap.high}${highBound}`,
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return errors;
|
|
581
|
+
}
|
|
582
|
+
|
|
369
583
|
function checkCompleteness(dt: DecisionTableDef): VerificationError[] {
|
|
370
584
|
const errors: VerificationError[] = [];
|
|
371
585
|
|
|
372
586
|
if (dt.conditions.length === 0) return errors; // Already reported in structural
|
|
373
587
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
location: { decisionTable: dt.name },
|
|
381
|
-
suggestion: 'Manually verify that all numeric ranges are covered without gaps',
|
|
382
|
-
});
|
|
383
|
-
return errors;
|
|
588
|
+
const hasNumericConditions = dt.conditions.some(c => c.type === 'int_range' || c.type === 'decimal_range');
|
|
589
|
+
const enumBoolConditions = dt.conditions.filter(c => c.type !== 'int_range' && c.type !== 'decimal_range');
|
|
590
|
+
|
|
591
|
+
// Interval-based completeness for numeric conditions (per-axis projection)
|
|
592
|
+
if (hasNumericConditions) {
|
|
593
|
+
errors.push(...checkNumericCompleteness(dt));
|
|
384
594
|
}
|
|
385
595
|
|
|
386
|
-
//
|
|
596
|
+
// Skip cartesian enumeration if there are no enum/bool conditions
|
|
597
|
+
if (enumBoolConditions.length === 0) return errors;
|
|
598
|
+
|
|
599
|
+
// Calculate total combinations for enum/bool conditions only
|
|
387
600
|
let totalCombinations = 1;
|
|
388
|
-
for (const cond of
|
|
601
|
+
for (const cond of enumBoolConditions) {
|
|
389
602
|
const values = getConditionValues(cond);
|
|
390
603
|
if (values.length === 0) {
|
|
391
604
|
totalCombinations = Infinity;
|
|
@@ -398,7 +611,7 @@ function checkCompleteness(dt: DecisionTableDef): VerificationError[] {
|
|
|
398
611
|
if (totalCombinations > 4096) {
|
|
399
612
|
errors.push({
|
|
400
613
|
code: 'DT_COMPLETENESS_SKIPPED',
|
|
401
|
-
message: `Completeness check skipped: ${totalCombinations} combinations exceed limit of 4096`,
|
|
614
|
+
message: `Completeness check skipped: ${totalCombinations} enum/bool combinations exceed limit of 4096`,
|
|
402
615
|
severity: 'warning',
|
|
403
616
|
location: { decisionTable: dt.name },
|
|
404
617
|
suggestion: 'Consider simplifying conditions or using wildcards to reduce combination count',
|
|
@@ -406,22 +619,52 @@ function checkCompleteness(dt: DecisionTableDef): VerificationError[] {
|
|
|
406
619
|
return errors;
|
|
407
620
|
}
|
|
408
621
|
|
|
409
|
-
//
|
|
410
|
-
|
|
411
|
-
|
|
622
|
+
// For mixed tables (numeric + enum/bool), enumerate only the enum/bool axes
|
|
623
|
+
// and check that for each enum/bool combination, at least one rule could fire
|
|
624
|
+
// (numeric conditions are checked separately above via interval analysis)
|
|
625
|
+
if (hasNumericConditions) {
|
|
626
|
+
// Enumerate enum/bool combinations, check that at least one rule has matching enum/bool
|
|
627
|
+
// cells (ignoring numeric conditions which are covered by interval checks)
|
|
628
|
+
const enumCombinations = generateCombinations(enumBoolConditions);
|
|
629
|
+
for (const combo of enumCombinations) {
|
|
630
|
+
const anyRuleMatchesEnums = dt.rules.some(rule => {
|
|
631
|
+
for (const cond of enumBoolConditions) {
|
|
632
|
+
const cell = rule.conditions.get(cond.name);
|
|
633
|
+
if (!cell) continue;
|
|
634
|
+
const expectedValue = combo.get(cond.name);
|
|
635
|
+
if (!cellMatches(cell, expectedValue || '')) return false;
|
|
636
|
+
}
|
|
637
|
+
return true;
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
if (!anyRuleMatchesEnums) {
|
|
641
|
+
const comboDesc = [...combo.entries()].map(([k, v]) => `${k}=${v}`).join(', ');
|
|
642
|
+
errors.push({
|
|
643
|
+
code: 'DT_INCOMPLETE',
|
|
644
|
+
message: `Missing coverage for enum/bool combination: ${comboDesc}`,
|
|
645
|
+
severity: 'error',
|
|
646
|
+
location: { decisionTable: dt.name },
|
|
647
|
+
suggestion: `Add a rule to cover this condition combination`,
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
} else {
|
|
652
|
+
// Pure enum/bool table — original cartesian completeness check
|
|
653
|
+
const combinations = generateCombinations(dt.conditions);
|
|
412
654
|
|
|
413
|
-
|
|
414
|
-
|
|
655
|
+
for (const combo of combinations) {
|
|
656
|
+
const matchingRules = findMatchingRules(dt, combo);
|
|
415
657
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
658
|
+
if (matchingRules.length === 0) {
|
|
659
|
+
const comboDesc = [...combo.entries()].map(([k, v]) => `${k}=${v}`).join(', ');
|
|
660
|
+
errors.push({
|
|
661
|
+
code: 'DT_INCOMPLETE',
|
|
662
|
+
message: `Missing coverage for: ${comboDesc}`,
|
|
663
|
+
severity: 'error',
|
|
664
|
+
location: { decisionTable: dt.name },
|
|
665
|
+
suggestion: `Add a rule to cover this condition combination`,
|
|
666
|
+
});
|
|
667
|
+
}
|
|
425
668
|
}
|
|
426
669
|
}
|
|
427
670
|
|