@agentix-e/nl2spel 1.3.0 → 1.4.0
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/README.md +1 -1
- package/dist/index.cjs +935 -276
- package/dist/index.d.cts +228 -22
- package/dist/index.d.ts +228 -22
- package/dist/index.js +930 -275
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -394,10 +394,55 @@ var ChineseNumberParser = class {
|
|
|
394
394
|
};
|
|
395
395
|
|
|
396
396
|
// src/pattern/pattern-matcher.ts
|
|
397
|
+
var UnmappedFieldError = class extends Error {
|
|
398
|
+
/** The field word that could not be resolved. */
|
|
399
|
+
field;
|
|
400
|
+
constructor(field) {
|
|
401
|
+
super(
|
|
402
|
+
`No field mapping for '${field}'. Add a mapping, or use fieldPolicy 'passthrough' to emit it verbatim.`
|
|
403
|
+
);
|
|
404
|
+
this.name = "UnmappedFieldError";
|
|
405
|
+
this.field = field;
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
var CN_FIELD_MAP = {
|
|
409
|
+
\u5907\u6CE8: "remark",
|
|
410
|
+
\u8BF4\u660E: "description",
|
|
411
|
+
\u63CF\u8FF0: "description",
|
|
412
|
+
\u91D1\u989D: "amount",
|
|
413
|
+
\u6570\u91CF: "count",
|
|
414
|
+
\u4E2A\u6570: "count",
|
|
415
|
+
\u72B6\u6001: "status",
|
|
416
|
+
\u7C7B\u578B: "type",
|
|
417
|
+
\u540D\u79F0: "name",
|
|
418
|
+
\u6807\u9898: "title",
|
|
419
|
+
\u5730\u5740: "address",
|
|
420
|
+
\u90AE\u7BB1: "email",
|
|
421
|
+
\u624B\u673A: "phone",
|
|
422
|
+
\u7535\u8BDD: "phone",
|
|
423
|
+
\u65E5\u671F: "date",
|
|
424
|
+
\u65F6\u95F4: "time",
|
|
425
|
+
\u5E74\u9F84: "age",
|
|
426
|
+
\u4EF7\u683C: "price",
|
|
427
|
+
\u7528\u6237\u540D: "name",
|
|
428
|
+
\u6743\u9650: "role",
|
|
429
|
+
\u6807\u7B7E: "tags",
|
|
430
|
+
\u5217\u8868: "list",
|
|
431
|
+
\u6570\u7EC4: "items",
|
|
432
|
+
\u6587\u4EF6: "file",
|
|
433
|
+
\u6587\u4EF6\u540D: "name",
|
|
434
|
+
\u8FC7\u671F: "expiryDate",
|
|
435
|
+
\u521B\u5EFA: "createdAt",
|
|
436
|
+
\u6709\u6548: "valid",
|
|
437
|
+
\u6D3B\u8DC3: "active",
|
|
438
|
+
\u6FC0\u6D3B: "active"
|
|
439
|
+
};
|
|
397
440
|
var PatternMatcher = class {
|
|
398
441
|
_patterns;
|
|
399
|
-
|
|
442
|
+
fieldPolicy;
|
|
443
|
+
constructor(patterns = [], options = {}) {
|
|
400
444
|
this._patterns = [...patterns];
|
|
445
|
+
this.fieldPolicy = options.fieldPolicy ?? "passthrough";
|
|
401
446
|
this.sortByPriority();
|
|
402
447
|
}
|
|
403
448
|
get patternCount() {
|
|
@@ -431,9 +476,22 @@ var PatternMatcher = class {
|
|
|
431
476
|
}
|
|
432
477
|
}
|
|
433
478
|
}
|
|
434
|
-
const
|
|
479
|
+
const { expression, unmappedFields } = this.fillTemplate(
|
|
480
|
+
pattern,
|
|
481
|
+
slots,
|
|
482
|
+
normalized,
|
|
483
|
+
matchResult
|
|
484
|
+
);
|
|
435
485
|
const latencyMs2 = Date.now() - startTime;
|
|
436
|
-
return {
|
|
486
|
+
return {
|
|
487
|
+
matched: true,
|
|
488
|
+
pattern,
|
|
489
|
+
spel: expression,
|
|
490
|
+
confidence: pattern.confidence,
|
|
491
|
+
latencyMs: latencyMs2,
|
|
492
|
+
slots,
|
|
493
|
+
...unmappedFields.length > 0 ? { unmappedFields } : {}
|
|
494
|
+
};
|
|
437
495
|
}
|
|
438
496
|
const latencyMs = Date.now() - startTime;
|
|
439
497
|
return { matched: false, confidence: 0, latencyMs };
|
|
@@ -454,13 +512,20 @@ var PatternMatcher = class {
|
|
|
454
512
|
if (value !== void 0) slots[key] = value;
|
|
455
513
|
}
|
|
456
514
|
}
|
|
515
|
+
const { expression, unmappedFields } = this.fillTemplate(
|
|
516
|
+
pattern,
|
|
517
|
+
slots,
|
|
518
|
+
normalized,
|
|
519
|
+
matchResult
|
|
520
|
+
);
|
|
457
521
|
results.push({
|
|
458
522
|
matched: true,
|
|
459
523
|
pattern,
|
|
460
|
-
spel:
|
|
524
|
+
spel: expression,
|
|
461
525
|
confidence: pattern.confidence,
|
|
462
526
|
latencyMs: 0,
|
|
463
|
-
slots
|
|
527
|
+
slots,
|
|
528
|
+
...unmappedFields.length > 0 ? { unmappedFields } : {}
|
|
464
529
|
});
|
|
465
530
|
}
|
|
466
531
|
return results;
|
|
@@ -482,46 +547,37 @@ var PatternMatcher = class {
|
|
|
482
547
|
if (/^(?:商品|product|item)/i.test(input)) return "product";
|
|
483
548
|
return "order";
|
|
484
549
|
}
|
|
550
|
+
/**
|
|
551
|
+
* The first whitespace- or punctuation-delimited chunk of `input`.
|
|
552
|
+
*/
|
|
553
|
+
firstWord(input) {
|
|
554
|
+
const m = input.match(/^[^\s,,、]+/);
|
|
555
|
+
return m ? m[0] : "value";
|
|
556
|
+
}
|
|
485
557
|
/**
|
|
486
558
|
* Extract Chinese field names from input and map to SpEL fields
|
|
487
559
|
*/
|
|
488
560
|
extractChineseField(input) {
|
|
489
|
-
const
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
\u5E74\u9F84: "age",
|
|
510
|
-
\u4EF7\u683C: "price",
|
|
511
|
-
\u7528\u6237\u540D: "name",
|
|
512
|
-
\u6743\u9650: "role",
|
|
513
|
-
\u6807\u7B7E: "tags",
|
|
514
|
-
\u5217\u8868: "list",
|
|
515
|
-
\u6570\u7EC4: "items",
|
|
516
|
-
\u6587\u4EF6: "file",
|
|
517
|
-
\u6587\u4EF6\u540D: "name",
|
|
518
|
-
\u8FC7\u671F: "expiryDate",
|
|
519
|
-
\u521B\u5EFA: "createdAt",
|
|
520
|
-
\u6709\u6548: "valid",
|
|
521
|
-
\u6D3B\u8DC3: "active",
|
|
522
|
-
\u6FC0\u6D3B: "active"
|
|
523
|
-
};
|
|
524
|
-
return cnMap[first] ?? first;
|
|
561
|
+
const first = this.firstWord(input);
|
|
562
|
+
return CN_FIELD_MAP[first] ?? first;
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Resolve a captured field word into the identifier to emit, and report whether
|
|
566
|
+
* the dictionary recognised it.
|
|
567
|
+
*
|
|
568
|
+
* An ASCII word is already an identifier and is emitted unchanged. Anything else
|
|
569
|
+
* is looked up: a word the dictionary knows becomes its English identifier, and a
|
|
570
|
+
* word it does not know is emitted verbatim and reported as unmapped. Emitting it
|
|
571
|
+
* verbatim is legal Spring, but it is a guess about the caller's schema, so the
|
|
572
|
+
* guess is never silent.
|
|
573
|
+
*/
|
|
574
|
+
resolveField(captured) {
|
|
575
|
+
if (/^[a-zA-Z_]\w*$/.test(captured)) {
|
|
576
|
+
return { field: captured, mapped: true };
|
|
577
|
+
}
|
|
578
|
+
const first = this.firstWord(captured);
|
|
579
|
+
const mapped = CN_FIELD_MAP[first];
|
|
580
|
+
return { field: mapped ?? first, mapped: mapped !== void 0 };
|
|
525
581
|
}
|
|
526
582
|
/**
|
|
527
583
|
* Template filling and value transformation
|
|
@@ -529,7 +585,20 @@ var PatternMatcher = class {
|
|
|
529
585
|
fillTemplate(pattern, slots, originalInput, _matchResult) {
|
|
530
586
|
let result = pattern.spelTemplate;
|
|
531
587
|
const hasFieldSlot = "field" in slots;
|
|
532
|
-
const
|
|
588
|
+
const unmappedFields = [];
|
|
589
|
+
let field;
|
|
590
|
+
if (hasFieldSlot) {
|
|
591
|
+
const resolved = this.resolveField(slots["field"]);
|
|
592
|
+
if (!resolved.mapped) {
|
|
593
|
+
if (this.fieldPolicy === "strict") {
|
|
594
|
+
throw new UnmappedFieldError(resolved.field);
|
|
595
|
+
}
|
|
596
|
+
unmappedFields.push(resolved.field);
|
|
597
|
+
}
|
|
598
|
+
field = resolved.field;
|
|
599
|
+
} else {
|
|
600
|
+
field = this.extractChineseField(originalInput);
|
|
601
|
+
}
|
|
533
602
|
const root = this.inferRoot(originalInput);
|
|
534
603
|
result = result.replace(/\{field\}/g, field);
|
|
535
604
|
result = result.replace(/\{root\}/g, root);
|
|
@@ -557,14 +626,7 @@ var PatternMatcher = class {
|
|
|
557
626
|
result = result.replace(`{${key}}`, transformedValue ?? "");
|
|
558
627
|
}
|
|
559
628
|
result = result.replace(/\{[a-zA-Z_]+\}/g, "");
|
|
560
|
-
return result.trim();
|
|
561
|
-
}
|
|
562
|
-
/**
|
|
563
|
-
* Infer SpEL field name from capture group
|
|
564
|
-
*/
|
|
565
|
-
inferFieldFromCapture(captured) {
|
|
566
|
-
if (/^[a-zA-Z_]\w*$/.test(captured)) return captured;
|
|
567
|
-
return this.extractChineseField(captured);
|
|
629
|
+
return { expression: result.trim(), unmappedFields };
|
|
568
630
|
}
|
|
569
631
|
};
|
|
570
632
|
|
|
@@ -630,7 +692,7 @@ var BUILTIN_PATTERNS = [
|
|
|
630
692
|
slots: {},
|
|
631
693
|
priority: 98,
|
|
632
694
|
tags: ["null", "isNotNull"],
|
|
633
|
-
examples: [{ nl: "\u5907\u6CE8\u4E0D\u4E3A\u7A7A", spel: "
|
|
695
|
+
examples: [{ nl: "\u5907\u6CE8\u4E0D\u4E3A\u7A7A", spel: "#remark != null" }],
|
|
634
696
|
difficulty: "easy",
|
|
635
697
|
confidence: 0.98
|
|
636
698
|
},
|
|
@@ -641,7 +703,7 @@ var BUILTIN_PATTERNS = [
|
|
|
641
703
|
slots: {},
|
|
642
704
|
priority: 97,
|
|
643
705
|
tags: ["null", "isNull"],
|
|
644
|
-
examples: [{ nl: "\u5907\u6CE8\u4E3A\u7A7A", spel: "
|
|
706
|
+
examples: [{ nl: "\u5907\u6CE8\u4E3A\u7A7A", spel: "#remark == null" }],
|
|
645
707
|
difficulty: "easy",
|
|
646
708
|
confidence: 0.98
|
|
647
709
|
},
|
|
@@ -672,29 +734,29 @@ var BUILTIN_PATTERNS = [
|
|
|
672
734
|
// ================================================================
|
|
673
735
|
{
|
|
674
736
|
id: "CN-CMP-GE",
|
|
675
|
-
match: /^(?<field>[^\s,,、]+?)\s*(
|
|
737
|
+
match: /^(?<field>[^\s,,、]+?)\s*(?:不小于|不低于|大于等于|>=)\s*(?<value>\d+(?:\.\d+)?)/,
|
|
676
738
|
spelTemplate: "#{field} >= {value}",
|
|
677
739
|
slots: { value: { key: "value", type: "number", transform: "toNumber" } },
|
|
678
740
|
priority: 93,
|
|
679
741
|
tags: ["comparison", "ge", "chinese"],
|
|
680
|
-
examples: [{ nl: "\u91D1\u989D\u4E0D\u5C0F\u4E8E100", spel: "
|
|
742
|
+
examples: [{ nl: "\u91D1\u989D\u4E0D\u5C0F\u4E8E100", spel: "#amount >= 100" }],
|
|
681
743
|
difficulty: "easy",
|
|
682
744
|
confidence: 0.95
|
|
683
745
|
},
|
|
684
746
|
{
|
|
685
747
|
id: "CN-CMP-LE",
|
|
686
|
-
match: /^(?<field>[^\s,,、]+?)\s*(
|
|
748
|
+
match: /^(?<field>[^\s,,、]+?)\s*(?:不大于|不超过|小于等于|<=)\s*(?<value>\d+(?:\.\d+)?)/,
|
|
687
749
|
spelTemplate: "#{field} <= {value}",
|
|
688
750
|
slots: { value: { key: "value", type: "number", transform: "toNumber" } },
|
|
689
751
|
priority: 93,
|
|
690
752
|
tags: ["comparison", "le", "chinese"],
|
|
691
|
-
examples: [{ nl: "\u91D1\u989D\u4E0D\u8D85\u8FC7500", spel: "
|
|
753
|
+
examples: [{ nl: "\u91D1\u989D\u4E0D\u8D85\u8FC7500", spel: "#amount <= 500" }],
|
|
692
754
|
difficulty: "easy",
|
|
693
755
|
confidence: 0.95
|
|
694
756
|
},
|
|
695
757
|
{
|
|
696
758
|
id: "CN-CMP-GT",
|
|
697
|
-
match: /^(?<field>[^\s,,、]+?)\s*(
|
|
759
|
+
match: /^(?<field>[^\s,,、]+?)\s*(?:大于|超过|高于)\s*(?<value>\d+(?:\.\d+)?)/,
|
|
698
760
|
spelTemplate: "#{field} > {value}",
|
|
699
761
|
slots: { value: { key: "value", type: "number", transform: "toNumber" } },
|
|
700
762
|
priority: 92,
|
|
@@ -705,7 +767,7 @@ var BUILTIN_PATTERNS = [
|
|
|
705
767
|
},
|
|
706
768
|
{
|
|
707
769
|
id: "CN-CMP-LT",
|
|
708
|
-
match: /^(?<field>[^\s,,、]+?)\s*(
|
|
770
|
+
match: /^(?<field>[^\s,,、]+?)\s*(?:小于|低于|不到)\s*(?<value>\d+(?:\.\d+)?)/,
|
|
709
771
|
spelTemplate: "#{field} < {value}",
|
|
710
772
|
slots: { value: { key: "value", type: "number", transform: "toNumber" } },
|
|
711
773
|
priority: 92,
|
|
@@ -719,7 +781,7 @@ var BUILTIN_PATTERNS = [
|
|
|
719
781
|
// ================================================================
|
|
720
782
|
{
|
|
721
783
|
id: "CN-EQ-STATUS",
|
|
722
|
-
match: /^(?<field>[^\s,,、]+?)\s*(
|
|
784
|
+
match: /^(?<field>[^\s,,、]+?)\s*(?:等于|是|为)\s*(?<value>[^\s,,、]+)$/,
|
|
723
785
|
spelTemplate: "#{field} == '{value}'",
|
|
724
786
|
slots: { value: { key: "value", type: "string" } },
|
|
725
787
|
priority: 85,
|
|
@@ -742,7 +804,7 @@ var BUILTIN_PATTERNS = [
|
|
|
742
804
|
// CN: "order status is not cancelled" — must be before NOT pattern
|
|
743
805
|
{
|
|
744
806
|
id: "CN-NE-STATUS",
|
|
745
|
-
match: /^(?<field>[^\s,,、]+?)\s*(
|
|
807
|
+
match: /^(?<field>[^\s,,、]+?)\s*(?:不等于|!=|不是)\s*(?<value>[^\s,,、]+)$/,
|
|
746
808
|
spelTemplate: "#{field} != '{value}'",
|
|
747
809
|
slots: { value: { key: "value", type: "string" } },
|
|
748
810
|
priority: 91,
|
|
@@ -765,12 +827,12 @@ var BUILTIN_PATTERNS = [
|
|
|
765
827
|
// CN/EN: count equality
|
|
766
828
|
{
|
|
767
829
|
id: "CN-EQ-COUNT",
|
|
768
|
-
match: /^(?<field>[^\s,,、]+?)\s*(
|
|
830
|
+
match: /^(?<field>[^\s,,、]+?)\s*(?:等于|==)\s*(?<value>\d+)/,
|
|
769
831
|
spelTemplate: "#{field} == {value}",
|
|
770
832
|
slots: { value: { key: "value", type: "number", transform: "toNumber" } },
|
|
771
833
|
priority: 91,
|
|
772
834
|
tags: ["comparison", "eq", "number"],
|
|
773
|
-
examples: [{ nl: "\u6570\u91CF\u7B49\u4E8E5", spel: "
|
|
835
|
+
examples: [{ nl: "\u6570\u91CF\u7B49\u4E8E5", spel: "#count == 5" }],
|
|
774
836
|
difficulty: "easy",
|
|
775
837
|
confidence: 0.95
|
|
776
838
|
},
|
|
@@ -812,7 +874,9 @@ var BUILTIN_PATTERNS = [
|
|
|
812
874
|
},
|
|
813
875
|
{
|
|
814
876
|
id: "CN-PERM-PERM",
|
|
815
|
-
|
|
877
|
+
// Chinese-only operator: keeping the English "can"/"may" here would let the
|
|
878
|
+
// pattern fire on the "can" inside "not cancelled".
|
|
879
|
+
match: /^(?<field>[^\s,,、]+)\s*(?:可以|能够|允许)\s*(?<permission>.+)$/,
|
|
816
880
|
spelTemplate: "hasPermission('{permission}')",
|
|
817
881
|
slots: { permission: { key: "permission", type: "string" } },
|
|
818
882
|
priority: 88,
|
|
@@ -837,7 +901,7 @@ var BUILTIN_PATTERNS = [
|
|
|
837
901
|
// ================================================================
|
|
838
902
|
{
|
|
839
903
|
id: "CN-COLL-EMPTY",
|
|
840
|
-
match: /^(?<field>[^\s,,、]+?)\s*(
|
|
904
|
+
match: /^(?<field>[^\s,,、]+?)\s*(?:为空|是空的|没有|无)\s*(?:元素|数据|项)?$/,
|
|
841
905
|
spelTemplate: "#{field}.isEmpty()",
|
|
842
906
|
slots: {},
|
|
843
907
|
priority: 89,
|
|
@@ -859,7 +923,7 @@ var BUILTIN_PATTERNS = [
|
|
|
859
923
|
},
|
|
860
924
|
{
|
|
861
925
|
id: "CN-COLL-NOTEMPTY",
|
|
862
|
-
match: /^(?<field>[^\s,,、]+?)\s*(
|
|
926
|
+
match: /^(?<field>[^\s,,、]+?)\s*(?:不为空|有)\s*(?:元素|数据|项)?$/,
|
|
863
927
|
spelTemplate: "!#{field}.isEmpty()",
|
|
864
928
|
slots: {},
|
|
865
929
|
priority: 89,
|
|
@@ -881,7 +945,7 @@ var BUILTIN_PATTERNS = [
|
|
|
881
945
|
},
|
|
882
946
|
{
|
|
883
947
|
id: "CN-COLL-CONTAINS",
|
|
884
|
-
match: /^(?<field>[^\s,,、]+?)\s*(
|
|
948
|
+
match: /^(?<field>[^\s,,、]+?)\s*(?:中\s*)?(?:包含|含有|有)\s*(?<element>[^\s,,、]+)/,
|
|
885
949
|
spelTemplate: "#{field}.contains('{element}')",
|
|
886
950
|
slots: { element: { key: "element", type: "string" } },
|
|
887
951
|
priority: 87,
|
|
@@ -903,7 +967,7 @@ var BUILTIN_PATTERNS = [
|
|
|
903
967
|
},
|
|
904
968
|
{
|
|
905
969
|
id: "CN-COLL-SIZE",
|
|
906
|
-
match: /^(?<field>[^\s,,、]+?)\s*(
|
|
970
|
+
match: /^(?<field>[^\s,,、]+?)\s*(?:数量|个数|大小|长度)\s*(?<op>大于|>|超过|小于|<|等于|==)\s*(?<value>\d+)/,
|
|
907
971
|
spelTemplate: "#{field}.size() {op} {value}",
|
|
908
972
|
slots: {
|
|
909
973
|
value: { key: "value", type: "number", transform: "toNumber" },
|
|
@@ -934,7 +998,7 @@ var BUILTIN_PATTERNS = [
|
|
|
934
998
|
// ================================================================
|
|
935
999
|
{
|
|
936
1000
|
id: "CN-STR-CONTAINS",
|
|
937
|
-
match: /^(?<field>[^\s,,、]+?)
|
|
1001
|
+
match: /^(?<field>[^\s,,、]+?)\s*(?:包含|含有|包括)\s*(?<substr>[^\s,,、]+)/,
|
|
938
1002
|
spelTemplate: "#{field}.contains('{substr}')",
|
|
939
1003
|
slots: { substr: { key: "substr", type: "string" } },
|
|
940
1004
|
priority: 85,
|
|
@@ -983,7 +1047,7 @@ var BUILTIN_PATTERNS = [
|
|
|
983
1047
|
slots: { suffix: { key: "suffix", type: "string" } },
|
|
984
1048
|
priority: 85,
|
|
985
1049
|
tags: ["string", "endsWith"],
|
|
986
|
-
examples: [{ nl: "\u6587\u4EF6\u540D\u4EE5.pdf\u7ED3\u5C3E", spel: "
|
|
1050
|
+
examples: [{ nl: "\u6587\u4EF6\u540D\u4EE5.pdf\u7ED3\u5C3E", spel: "#name.endsWith('.pdf')" }],
|
|
987
1051
|
difficulty: "easy",
|
|
988
1052
|
confidence: 0.95
|
|
989
1053
|
},
|
|
@@ -1026,21 +1090,21 @@ var BUILTIN_PATTERNS = [
|
|
|
1026
1090
|
{
|
|
1027
1091
|
id: "CN-RANGE-BETWEEN",
|
|
1028
1092
|
match: /^(?<field>[^\s,,、]+?)\s*(?:在|介于)\s*(?<min>\d+)\s*(?:和|到|~)\s*(?<max>\d+)\s*(?:之间|范围)?/,
|
|
1029
|
-
spelTemplate: "#{field} between {{
|
|
1093
|
+
spelTemplate: "#{field} between {{min}, {max}}",
|
|
1030
1094
|
slots: {
|
|
1031
1095
|
min: { key: "min", type: "number", transform: "toNumber" },
|
|
1032
1096
|
max: { key: "max", type: "number", transform: "toNumber" }
|
|
1033
1097
|
},
|
|
1034
1098
|
priority: 82,
|
|
1035
1099
|
tags: ["range", "between"],
|
|
1036
|
-
examples: [{ nl: "\u5E74\u9F84\u572818\u523060\u4E4B\u95F4", spel: "
|
|
1100
|
+
examples: [{ nl: "\u5E74\u9F84\u572818\u523060\u4E4B\u95F4", spel: "#age between {18, 60}" }],
|
|
1037
1101
|
difficulty: "easy",
|
|
1038
1102
|
confidence: 0.95
|
|
1039
1103
|
},
|
|
1040
1104
|
{
|
|
1041
1105
|
id: "EN-RANGE-BETWEEN",
|
|
1042
1106
|
match: /\b(?<field>\w+)\s+between\s+(?<min>\d+)\s+and\s+(?<max>\d+)/i,
|
|
1043
|
-
spelTemplate: "#{field} between {{
|
|
1107
|
+
spelTemplate: "#{field} between {{min}, {max}}",
|
|
1044
1108
|
slots: {
|
|
1045
1109
|
min: { key: "min", type: "number", transform: "toNumber" },
|
|
1046
1110
|
max: { key: "max", type: "number", transform: "toNumber" }
|
|
@@ -1061,7 +1125,7 @@ var BUILTIN_PATTERNS = [
|
|
|
1061
1125
|
slots: { default: { key: "default", type: "string" } },
|
|
1062
1126
|
priority: 78,
|
|
1063
1127
|
tags: ["elvis"],
|
|
1064
|
-
examples: [{ nl: "\u7528\u6237\u540D\u6216\u8005\u533F\u540D\u7528\u6237", spel: "
|
|
1128
|
+
examples: [{ nl: "\u7528\u6237\u540D\u6216\u8005\u533F\u540D\u7528\u6237", spel: "#name ?: '\u533F\u540D\u7528\u6237'" }],
|
|
1065
1129
|
difficulty: "medium",
|
|
1066
1130
|
confidence: 0.85
|
|
1067
1131
|
},
|
|
@@ -1167,7 +1231,11 @@ var BUILTIN_PATTERNS = [
|
|
|
1167
1231
|
// ================================================================
|
|
1168
1232
|
{
|
|
1169
1233
|
id: "CN-BOOL-TRUE",
|
|
1170
|
-
|
|
1234
|
+
// "用户是VIP" asserts the subject through a Latin marker, so accept either a
|
|
1235
|
+
// truth word or a Latin word. A fully open marker would fire on function
|
|
1236
|
+
// words ("一个非常…"), and the lookbehind keeps a negator out of the field
|
|
1237
|
+
// ("用户不" is not the subject).
|
|
1238
|
+
match: /^(?<field>[^\s,,、]+?)(?<![不非])\s*(?:是|为|等于|==)\s*(?:true|真|是|yes|[A-Za-z]\w*)$/,
|
|
1171
1239
|
spelTemplate: "#{field} == true",
|
|
1172
1240
|
slots: {},
|
|
1173
1241
|
priority: 74,
|
|
@@ -1178,7 +1246,10 @@ var BUILTIN_PATTERNS = [
|
|
|
1178
1246
|
},
|
|
1179
1247
|
{
|
|
1180
1248
|
id: "CN-BOOL-FALSE",
|
|
1181
|
-
|
|
1249
|
+
// Mirror of CN-BOOL-TRUE. The marker stays narrow so that function words
|
|
1250
|
+
// ("非常…") do not match, and the lookbehind keeps "不是有效" with
|
|
1251
|
+
// CN-LOGIC-NOT instead of capturing "不" as the field.
|
|
1252
|
+
match: /^(?<field>[^\s,,、]+?)(?<![不非])\s*(?:不是|非|为|是|等于|==)\s*(?:false|假|否|no|[A-Za-z]\w*)$/,
|
|
1182
1253
|
spelTemplate: "#{field} == false",
|
|
1183
1254
|
slots: {},
|
|
1184
1255
|
priority: 73,
|
|
@@ -1286,7 +1357,7 @@ var BUILTIN_PATTERNS = [
|
|
|
1286
1357
|
// ================================================================
|
|
1287
1358
|
{
|
|
1288
1359
|
id: "CN-LOGIC-NOT",
|
|
1289
|
-
match: /^不是\s
|
|
1360
|
+
match: /^不是\s*(?<expr>.+)/,
|
|
1290
1361
|
spelTemplate: "!({expr})",
|
|
1291
1362
|
slots: { expr: { key: "expr", type: "variable" } },
|
|
1292
1363
|
priority: 62,
|
|
@@ -1311,8 +1382,8 @@ var BUILTIN_PATTERNS = [
|
|
|
1311
1382
|
// ================================================================
|
|
1312
1383
|
{
|
|
1313
1384
|
id: "CN-SELECT-FIRST",
|
|
1314
|
-
match: /^(?<root>[^\s,,、]+?)\s*中\s*第一[个位]\s*(?<field>[^\s,,、]+?)\s*(?:大于|>|超过|<|小于|等于|==)\s*(?<value>[^\s
|
|
1315
|
-
spelTemplate: "#{root}.items.^[#
|
|
1385
|
+
match: /^(?<root>[^\s,,、]+?)\s*中\s*第一[个位]\s*(?<field>[^\s,,、]+?)\s*(?:大于|>|超过|<|小于|等于|==)\s*(?<value>[^\s,,、的]+)/,
|
|
1386
|
+
spelTemplate: "#{root}.items.^[#this.{field} > {value}]",
|
|
1316
1387
|
slots: {
|
|
1317
1388
|
root: { key: "root", type: "variable" },
|
|
1318
1389
|
field: { key: "field", type: "variable" },
|
|
@@ -1320,14 +1391,14 @@ var BUILTIN_PATTERNS = [
|
|
|
1320
1391
|
},
|
|
1321
1392
|
priority: 60,
|
|
1322
1393
|
tags: ["selection", "first"],
|
|
1323
|
-
examples: [{ nl: "\u8BA2\u5355\u4E2D\u7B2C\u4E00\u4E2A\u91D1\u989D\u5927\u4E8E1000\u7684", spel: "
|
|
1394
|
+
examples: [{ nl: "\u8BA2\u5355\u4E2D\u7B2C\u4E00\u4E2A\u91D1\u989D\u5927\u4E8E1000\u7684", spel: "#order.items.^[#this.amount > 1000]" }],
|
|
1324
1395
|
difficulty: "medium",
|
|
1325
1396
|
confidence: 0.8
|
|
1326
1397
|
},
|
|
1327
1398
|
{
|
|
1328
1399
|
id: "CN-SELECT-ALL",
|
|
1329
|
-
match: /^(?<root>[^\s,,、]+?)\s*中\s*所有\s*(?<field>[^\s,,、]+?)\s*(?:大于|>|超过|<|小于|等于|==|包含|满足)\s*(?<value>[^\s
|
|
1330
|
-
spelTemplate: "#{root}.items.?[#
|
|
1400
|
+
match: /^(?<root>[^\s,,、]+?)\s*中\s*所有\s*(?<field>[^\s,,、]+?)\s*(?:大于|>|超过|<|小于|等于|==|包含|满足)\s*(?<value>[^\s,,、的]+)/,
|
|
1401
|
+
spelTemplate: "#{root}.items.?[#this.{field} > {value}]",
|
|
1331
1402
|
slots: {
|
|
1332
1403
|
root: { key: "root", type: "variable" },
|
|
1333
1404
|
field: { key: "field", type: "variable" },
|
|
@@ -1335,28 +1406,28 @@ var BUILTIN_PATTERNS = [
|
|
|
1335
1406
|
},
|
|
1336
1407
|
priority: 60,
|
|
1337
1408
|
tags: ["selection", "all"],
|
|
1338
|
-
examples: [{ nl: "\u8BA2\u5355\u4E2D\u6240\u6709\u91D1\u989D\u5927\u4E8E1000\u7684", spel: "
|
|
1409
|
+
examples: [{ nl: "\u8BA2\u5355\u4E2D\u6240\u6709\u91D1\u989D\u5927\u4E8E1000\u7684", spel: "#order.items.?[#this.amount > 1000]" }],
|
|
1339
1410
|
difficulty: "medium",
|
|
1340
1411
|
confidence: 0.8
|
|
1341
1412
|
},
|
|
1342
1413
|
{
|
|
1343
1414
|
id: "CN-PROJ",
|
|
1344
|
-
match: /^(?<root>[^\s,,、]+?)\s*中\s*每[个一]\s*(?:的)?(?<field>[^\s,,、]+?)\s*(?:值|金额|名称|价格|name|amount|price)
|
|
1345
|
-
spelTemplate: "#{root}.items.![#
|
|
1415
|
+
match: /^(?<root>[^\s,,、]+?)\s*中\s*每[个一]\s*(?:的)?(?<field>[^\s,,、]+?)\s*(?:的)?(?:值|金额|名称|价格|name|amount|price)?$/,
|
|
1416
|
+
spelTemplate: "#{root}.items.![#this.{field}]",
|
|
1346
1417
|
slots: {
|
|
1347
1418
|
root: { key: "root", type: "variable" },
|
|
1348
1419
|
field: { key: "field", type: "variable" }
|
|
1349
1420
|
},
|
|
1350
1421
|
priority: 55,
|
|
1351
1422
|
tags: ["projection"],
|
|
1352
|
-
examples: [{ nl: "\u8BA2\u5355\u4E2D\u6BCF\u4E2A\u5546\u54C1\u7684\u4EF7\u683C", spel: "
|
|
1423
|
+
examples: [{ nl: "\u8BA2\u5355\u4E2D\u6BCF\u4E2A\u5546\u54C1\u7684\u4EF7\u683C", spel: "#order.items.![#this.\u5546\u54C1]" }],
|
|
1353
1424
|
difficulty: "medium",
|
|
1354
1425
|
confidence: 0.75
|
|
1355
1426
|
},
|
|
1356
1427
|
{
|
|
1357
1428
|
id: "EN-SELECT-ALL",
|
|
1358
1429
|
match: /all\s+(?<root>\w+)\s+with\s+(?<field>\w+)\s*>\s*(?<value>\d+)/i,
|
|
1359
|
-
spelTemplate: "#{root}.items.?[#
|
|
1430
|
+
spelTemplate: "#{root}.items.?[#this.{field} > {value}]",
|
|
1360
1431
|
slots: {
|
|
1361
1432
|
root: { key: "root", type: "variable" },
|
|
1362
1433
|
field: { key: "field", type: "variable" },
|
|
@@ -1364,13 +1435,149 @@ var BUILTIN_PATTERNS = [
|
|
|
1364
1435
|
},
|
|
1365
1436
|
priority: 50,
|
|
1366
1437
|
tags: ["selection", "all", "english"],
|
|
1367
|
-
examples: [{ nl: "all items with price > 100", spel: "#
|
|
1438
|
+
examples: [{ nl: "all items with price > 100", spel: "#order.items.?[#this.price > 100]" }],
|
|
1368
1439
|
difficulty: "medium",
|
|
1369
1440
|
confidence: 0.8
|
|
1370
1441
|
}
|
|
1371
1442
|
];
|
|
1372
1443
|
BUILTIN_PATTERNS.sort((a, b) => b.priority - a.priority);
|
|
1373
1444
|
|
|
1445
|
+
// src/pattern/clause-splitter.ts
|
|
1446
|
+
var CONNECTORS = [
|
|
1447
|
+
// Chinese conjunctions. `和` is deliberately absent: it is a range separator in
|
|
1448
|
+
// `价格在10和20之间`, not a conjunction.
|
|
1449
|
+
{ pattern: /(?:并且|且|同时|而且|、)/y, operator: "and" },
|
|
1450
|
+
{ pattern: /(?:或者|或|要么)/y, operator: "or" },
|
|
1451
|
+
// English conjunctions, matched as whole words.
|
|
1452
|
+
{ pattern: /\band\b/iy, operator: "and" },
|
|
1453
|
+
{ pattern: /\bor\b/iy, operator: "or" }
|
|
1454
|
+
];
|
|
1455
|
+
var UnconvertibleClauseError = class extends Error {
|
|
1456
|
+
/** The clause texts that could not be converted. */
|
|
1457
|
+
unconvertible;
|
|
1458
|
+
/** The full input that was being decomposed. */
|
|
1459
|
+
input;
|
|
1460
|
+
constructor(input, unconvertible) {
|
|
1461
|
+
const detail = unconvertible.map((clause) => `'${clause}'`).join(", ");
|
|
1462
|
+
super(
|
|
1463
|
+
`Cannot decompose '${input}': no conversion for ${detail}. Refusing to emit a partial rule.`
|
|
1464
|
+
);
|
|
1465
|
+
this.name = "UnconvertibleClauseError";
|
|
1466
|
+
this.input = input;
|
|
1467
|
+
this.unconvertible = unconvertible;
|
|
1468
|
+
}
|
|
1469
|
+
};
|
|
1470
|
+
var QUOTES = /* @__PURE__ */ new Set(["'", '"']);
|
|
1471
|
+
var OPENERS = { "(": ")", "[": "]", "{": "}" };
|
|
1472
|
+
var CLOSERS = /* @__PURE__ */ new Set([")", "]", "}"]);
|
|
1473
|
+
var isDigit = (ch) => ch !== void 0 && ch >= "0" && ch <= "9";
|
|
1474
|
+
function nextConnector(input, from) {
|
|
1475
|
+
let depth = 0;
|
|
1476
|
+
let quote = null;
|
|
1477
|
+
for (let i = from; i < input.length; i += 1) {
|
|
1478
|
+
const ch = input[i];
|
|
1479
|
+
if (quote !== null) {
|
|
1480
|
+
if (ch === quote) {
|
|
1481
|
+
if (input[i + 1] === quote) {
|
|
1482
|
+
i += 1;
|
|
1483
|
+
continue;
|
|
1484
|
+
}
|
|
1485
|
+
quote = null;
|
|
1486
|
+
}
|
|
1487
|
+
continue;
|
|
1488
|
+
}
|
|
1489
|
+
if (QUOTES.has(ch)) {
|
|
1490
|
+
quote = ch;
|
|
1491
|
+
continue;
|
|
1492
|
+
}
|
|
1493
|
+
if (ch in OPENERS) {
|
|
1494
|
+
depth += 1;
|
|
1495
|
+
continue;
|
|
1496
|
+
}
|
|
1497
|
+
if (CLOSERS.has(ch)) {
|
|
1498
|
+
depth = Math.max(0, depth - 1);
|
|
1499
|
+
continue;
|
|
1500
|
+
}
|
|
1501
|
+
if (depth > 0) continue;
|
|
1502
|
+
for (const { pattern, operator } of CONNECTORS) {
|
|
1503
|
+
pattern.lastIndex = i;
|
|
1504
|
+
const match = pattern.exec(input);
|
|
1505
|
+
if (!match) continue;
|
|
1506
|
+
if (operator === "and") {
|
|
1507
|
+
let before = i - 1;
|
|
1508
|
+
while (before >= 0 && input[before] === " ") before -= 1;
|
|
1509
|
+
let after = i + match[0].length;
|
|
1510
|
+
while (after < input.length && input[after] === " ") after += 1;
|
|
1511
|
+
if (isDigit(input[before]) && isDigit(input[after])) continue;
|
|
1512
|
+
}
|
|
1513
|
+
return { index: i, length: match[0].length, operator };
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
return null;
|
|
1517
|
+
}
|
|
1518
|
+
function splitClauses(input) {
|
|
1519
|
+
const clauses = [];
|
|
1520
|
+
let start = 0;
|
|
1521
|
+
let connector = "and";
|
|
1522
|
+
let cursor = 0;
|
|
1523
|
+
for (; ; ) {
|
|
1524
|
+
const found = nextConnector(input, cursor);
|
|
1525
|
+
if (found === null) break;
|
|
1526
|
+
clauses.push({ text: input.slice(start, found.index).trim(), connector });
|
|
1527
|
+
connector = found.operator;
|
|
1528
|
+
start = found.index + found.length;
|
|
1529
|
+
cursor = start;
|
|
1530
|
+
}
|
|
1531
|
+
const tail = input.slice(start).trim();
|
|
1532
|
+
if (clauses.length > 0 || tail.length > 0) {
|
|
1533
|
+
clauses.push({ text: tail, connector });
|
|
1534
|
+
}
|
|
1535
|
+
if (clauses.length === 0) {
|
|
1536
|
+
clauses.push({ text: "", connector: "and" });
|
|
1537
|
+
}
|
|
1538
|
+
return clauses;
|
|
1539
|
+
}
|
|
1540
|
+
function groupByPrecedence(clauses) {
|
|
1541
|
+
const groups = [];
|
|
1542
|
+
let current = [];
|
|
1543
|
+
for (const clause of clauses) {
|
|
1544
|
+
if (clause.connector === "or" && current.length > 0) {
|
|
1545
|
+
groups.push(current);
|
|
1546
|
+
current = [];
|
|
1547
|
+
}
|
|
1548
|
+
current.push(clause);
|
|
1549
|
+
}
|
|
1550
|
+
if (current.length > 0) groups.push(current);
|
|
1551
|
+
return groups;
|
|
1552
|
+
}
|
|
1553
|
+
function decompose(input, convert) {
|
|
1554
|
+
const clauses = splitClauses(input);
|
|
1555
|
+
if (clauses.length <= 1) return null;
|
|
1556
|
+
const unconvertible = [];
|
|
1557
|
+
const resolved = clauses.map((clause) => {
|
|
1558
|
+
const expression = clause.text.length === 0 ? null : convert(clause.text);
|
|
1559
|
+
if (expression === null) {
|
|
1560
|
+
unconvertible.push(clause.text);
|
|
1561
|
+
return { ...clause, expression: "" };
|
|
1562
|
+
}
|
|
1563
|
+
return { ...clause, expression };
|
|
1564
|
+
});
|
|
1565
|
+
if (unconvertible.length > 0) {
|
|
1566
|
+
throw new UnconvertibleClauseError(input, unconvertible);
|
|
1567
|
+
}
|
|
1568
|
+
const groups = groupByPrecedence(resolved);
|
|
1569
|
+
const mixed = groups.length > 1;
|
|
1570
|
+
const rendered = groups.map((group) => {
|
|
1571
|
+
const parts = group.map((clause) => `(${clause.expression})`);
|
|
1572
|
+
const joined = parts.join(" and ");
|
|
1573
|
+
return mixed && group.length > 1 ? `(${joined})` : joined;
|
|
1574
|
+
});
|
|
1575
|
+
return {
|
|
1576
|
+
expression: rendered.join(" or "),
|
|
1577
|
+
clauses: clauses.map((clause) => clause.text)
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1374
1581
|
// src/template/nl-intent.ts
|
|
1375
1582
|
var NLIntent = /* @__PURE__ */ ((NLIntent2) => {
|
|
1376
1583
|
NLIntent2["COMPARISON"] = "COMPARISON";
|
|
@@ -1409,7 +1616,7 @@ var INTENT_KEYWORDS = {
|
|
|
1409
1616
|
en: ["greater", "less", "equal", "above", "below", "exceed", ">", "<", "==", "!=", ">=", "<="]
|
|
1410
1617
|
},
|
|
1411
1618
|
["NULL_CHECK" /* NULL_CHECK */]: {
|
|
1412
|
-
zh: ["\u4E3A\u7A7A", "\u4E0D\u4E3A\u7A7A", "\u662F\u7A7A", "\u5B58\u5728", "\u4E0D\u5B58\u5728", "null", "\u6CA1\u6709\u503C"],
|
|
1619
|
+
zh: ["\u4E3A\u7A7A", "\u4E0D\u4E3A\u7A7A", "\u662F\u7A7A", "\u975E\u7A7A", "\u5B58\u5728", "\u4E0D\u5B58\u5728", "\u6709\u503C", "\u65E0\u503C", "null", "\u6CA1\u6709\u503C"],
|
|
1413
1620
|
en: ["null", "empty", "is null", "is not null", "is empty", "is not empty"]
|
|
1414
1621
|
},
|
|
1415
1622
|
["PERMISSION_CHECK" /* PERMISSION_CHECK */]: {
|
|
@@ -1446,7 +1653,10 @@ var INTENT_KEYWORDS = {
|
|
|
1446
1653
|
},
|
|
1447
1654
|
["BOOLEAN" /* BOOLEAN */]: {
|
|
1448
1655
|
zh: ["\u662F\u5426", "\u771F\u5047", "true", "false", "\u662F", "\u5426"],
|
|
1449
|
-
|
|
1656
|
+
// "no" is deliberately absent: it is a substring of "not", so it fired on
|
|
1657
|
+
// every negated null/emptiness phrase and made "remark is not empty" a
|
|
1658
|
+
// boolean check. "false"/"否" already cover the negative boolean spelling.
|
|
1659
|
+
en: ["true", "false", "yes", "is", "is not"]
|
|
1450
1660
|
},
|
|
1451
1661
|
["DATE" /* DATE */]: {
|
|
1452
1662
|
zh: ["\u65E5\u671F", "\u65F6\u95F4", "\u4E4B\u540E", "\u4E4B\u524D", "\u65E9\u4E8E", "\u665A\u4E8E"],
|
|
@@ -1465,6 +1675,38 @@ var INTENT_KEYWORDS = {
|
|
|
1465
1675
|
en: ["plus", "minus", "multiply", "divide", "mod", "sum", "average"]
|
|
1466
1676
|
}
|
|
1467
1677
|
};
|
|
1678
|
+
var NULL_NEGATED_SPECIFIC = [
|
|
1679
|
+
"\u4E0D\u4E3A\u7A7A",
|
|
1680
|
+
"\u4E0D\u4E3Anull",
|
|
1681
|
+
"\u4E0D\u662F\u7A7A",
|
|
1682
|
+
"\u975E\u7A7A",
|
|
1683
|
+
"is not null",
|
|
1684
|
+
"is not empty"
|
|
1685
|
+
];
|
|
1686
|
+
var NULL_AFFIRMATIVE = [
|
|
1687
|
+
"\u4E0D\u5B58\u5728",
|
|
1688
|
+
"\u65E0\u503C",
|
|
1689
|
+
"\u6CA1\u6709\u503C",
|
|
1690
|
+
"\u4E3A\u7A7A",
|
|
1691
|
+
"\u4E3Anull",
|
|
1692
|
+
"\u662F\u7A7A",
|
|
1693
|
+
"is null",
|
|
1694
|
+
"is empty"
|
|
1695
|
+
];
|
|
1696
|
+
var NULL_NEGATED_GENERIC = ["\u6709\u503C", "\u5B58\u5728"];
|
|
1697
|
+
function detectNullPredicate(input) {
|
|
1698
|
+
const text = input.trim().toLowerCase().replace(/[\uFF01-\uFF5E]/g, (ch) => String.fromCharCode(ch.charCodeAt(0) - 65248)).replace(/\s+/g, " ");
|
|
1699
|
+
for (const token of NULL_NEGATED_SPECIFIC) {
|
|
1700
|
+
if (text.includes(token)) return "negated";
|
|
1701
|
+
}
|
|
1702
|
+
for (const token of NULL_AFFIRMATIVE) {
|
|
1703
|
+
if (text.includes(token)) return "affirmative";
|
|
1704
|
+
}
|
|
1705
|
+
for (const token of NULL_NEGATED_GENERIC) {
|
|
1706
|
+
if (text.includes(token)) return "negated";
|
|
1707
|
+
}
|
|
1708
|
+
return null;
|
|
1709
|
+
}
|
|
1468
1710
|
var IntentClassifier = class {
|
|
1469
1711
|
/**
|
|
1470
1712
|
* Main method for classifying natural language input
|
|
@@ -1498,6 +1740,9 @@ var IntentClassifier = class {
|
|
|
1498
1740
|
if (/isempty|\.isEmpty|列表|数组|集合/.test(normalized)) {
|
|
1499
1741
|
intentScores.set("COLLECTION" /* COLLECTION */, (intentScores.get("COLLECTION" /* COLLECTION */) ?? 0) + 1);
|
|
1500
1742
|
}
|
|
1743
|
+
if (detectNullPredicate(normalized)) {
|
|
1744
|
+
intentScores.set("NULL_CHECK" /* NULL_CHECK */, (intentScores.get("NULL_CHECK" /* NULL_CHECK */) ?? 0) + 2);
|
|
1745
|
+
}
|
|
1501
1746
|
const entities = this.extractEntities(normalized);
|
|
1502
1747
|
const operators = this.extractOperators(normalized);
|
|
1503
1748
|
const logicalConnectors = this.extractLogicalConnectors(normalized);
|
|
@@ -1610,6 +1855,68 @@ var IntentClassifier = class {
|
|
|
1610
1855
|
};
|
|
1611
1856
|
|
|
1612
1857
|
// src/template/template-engine.ts
|
|
1858
|
+
var CHINESE_FIELD_MAP = {
|
|
1859
|
+
\u5907\u6CE8: "remark",
|
|
1860
|
+
\u8BF4\u660E: "description",
|
|
1861
|
+
\u63CF\u8FF0: "description",
|
|
1862
|
+
\u91D1\u989D: "amount",
|
|
1863
|
+
\u6570\u91CF: "count",
|
|
1864
|
+
\u4E2A\u6570: "count",
|
|
1865
|
+
\u72B6\u6001: "status",
|
|
1866
|
+
\u7C7B\u578B: "type",
|
|
1867
|
+
\u540D\u79F0: "name",
|
|
1868
|
+
\u6807\u9898: "title",
|
|
1869
|
+
\u5730\u5740: "address",
|
|
1870
|
+
\u90AE\u7BB1: "email",
|
|
1871
|
+
\u624B\u673A: "phone",
|
|
1872
|
+
\u7535\u8BDD: "phone",
|
|
1873
|
+
\u65E5\u671F: "date",
|
|
1874
|
+
\u65F6\u95F4: "time",
|
|
1875
|
+
\u5E74\u9F84: "age",
|
|
1876
|
+
\u4EF7\u683C: "price",
|
|
1877
|
+
\u7528\u6237\u540D: "name",
|
|
1878
|
+
\u6743\u9650: "role",
|
|
1879
|
+
\u6807\u7B7E: "tags",
|
|
1880
|
+
\u5217\u8868: "list",
|
|
1881
|
+
\u6570\u7EC4: "items",
|
|
1882
|
+
\u6587\u4EF6: "file",
|
|
1883
|
+
\u6587\u4EF6\u540D: "name",
|
|
1884
|
+
\u8FC7\u671F: "expiryDate",
|
|
1885
|
+
\u521B\u5EFA: "createdAt",
|
|
1886
|
+
\u6709\u6548: "valid",
|
|
1887
|
+
\u6D3B\u8DC3: "active",
|
|
1888
|
+
\u6FC0\u6D3B: "active"
|
|
1889
|
+
};
|
|
1890
|
+
var CHINESE_FIELDS_BY_LENGTH = Object.entries(CHINESE_FIELD_MAP).sort(
|
|
1891
|
+
(a, b) => b[0].length - a[0].length
|
|
1892
|
+
);
|
|
1893
|
+
var FIELD_STOPWORDS = /* @__PURE__ */ new Set([
|
|
1894
|
+
"a",
|
|
1895
|
+
"an",
|
|
1896
|
+
"account",
|
|
1897
|
+
"and",
|
|
1898
|
+
"are",
|
|
1899
|
+
"be",
|
|
1900
|
+
"between",
|
|
1901
|
+
"empty",
|
|
1902
|
+
"false",
|
|
1903
|
+
"file",
|
|
1904
|
+
"has",
|
|
1905
|
+
"have",
|
|
1906
|
+
"is",
|
|
1907
|
+
"no",
|
|
1908
|
+
"not",
|
|
1909
|
+
"null",
|
|
1910
|
+
"or",
|
|
1911
|
+
"order",
|
|
1912
|
+
"product",
|
|
1913
|
+
"than",
|
|
1914
|
+
"the",
|
|
1915
|
+
"true",
|
|
1916
|
+
"user",
|
|
1917
|
+
"value",
|
|
1918
|
+
"yes"
|
|
1919
|
+
]);
|
|
1613
1920
|
var TEMPLATE_LIBRARY = {
|
|
1614
1921
|
["COMPARISON" /* COMPARISON */]: [
|
|
1615
1922
|
{
|
|
@@ -1854,8 +2161,9 @@ var TemplateEngine = class {
|
|
|
1854
2161
|
selectBestTemplate(templates, intentResult, input) {
|
|
1855
2162
|
let bestScore = -1;
|
|
1856
2163
|
let bestTemplate = null;
|
|
1857
|
-
const
|
|
1858
|
-
const
|
|
2164
|
+
const polarity = detectNullPredicate(input);
|
|
2165
|
+
const isAffirmative = polarity === "affirmative";
|
|
2166
|
+
const isNegated = polarity === "negated";
|
|
1859
2167
|
for (const template of templates) {
|
|
1860
2168
|
const conditions = template.conditions;
|
|
1861
2169
|
let score = 0;
|
|
@@ -1864,8 +2172,10 @@ var TemplateEngine = class {
|
|
|
1864
2172
|
if (conditions.hasCollection) score += 0.5;
|
|
1865
2173
|
if (conditions.hasNull) score += 0.5;
|
|
1866
2174
|
if (conditions.hasString) score += 1;
|
|
1867
|
-
if (template.name.includes("IS_EMPTY") &&
|
|
1868
|
-
if (template.name.includes("IS_NOT_EMPTY") &&
|
|
2175
|
+
if (template.name.includes("IS_EMPTY") && isAffirmative) score += 2;
|
|
2176
|
+
if (template.name.includes("IS_NOT_EMPTY") && isNegated) score += 2;
|
|
2177
|
+
if (template.name === "NULL-IS_NULL" && isAffirmative) score += 2;
|
|
2178
|
+
if (template.name === "NULL-IS_NOT_NULL" && isNegated) score += 2;
|
|
1869
2179
|
if (conditions.entityCount) {
|
|
1870
2180
|
if (conditions.entityCount.min && intentResult.entities.length < conditions.entityCount.min) {
|
|
1871
2181
|
continue;
|
|
@@ -1881,37 +2191,8 @@ var TemplateEngine = class {
|
|
|
1881
2191
|
fillTemplate(template, input, intentResult) {
|
|
1882
2192
|
let expression = template;
|
|
1883
2193
|
const unfilledSlots = [];
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
if (this.contextSchema?.root) {
|
|
1887
|
-
rootName = this.contextSchema.root.name;
|
|
1888
|
-
const fields = Object.keys(this.contextSchema.root.fields ?? {});
|
|
1889
|
-
for (const f of fields) {
|
|
1890
|
-
if (input.includes(f)) {
|
|
1891
|
-
fieldName = f;
|
|
1892
|
-
break;
|
|
1893
|
-
}
|
|
1894
|
-
}
|
|
1895
|
-
} else {
|
|
1896
|
-
const rootMap = {
|
|
1897
|
-
\u8BA2\u5355: "order",
|
|
1898
|
-
order: "order",
|
|
1899
|
-
\u7528\u6237: "user",
|
|
1900
|
-
user: "user",
|
|
1901
|
-
\u6587\u4EF6: "file",
|
|
1902
|
-
file: "file",
|
|
1903
|
-
\u8D26\u53F7: "account",
|
|
1904
|
-
account: "account",
|
|
1905
|
-
\u5546\u54C1: "item",
|
|
1906
|
-
product: "item"
|
|
1907
|
-
};
|
|
1908
|
-
for (const [key, val] of Object.entries(rootMap)) {
|
|
1909
|
-
if (input.includes(key)) {
|
|
1910
|
-
rootName = val;
|
|
1911
|
-
break;
|
|
1912
|
-
}
|
|
1913
|
-
}
|
|
1914
|
-
}
|
|
2194
|
+
const rootName = this.resolveRootName(input);
|
|
2195
|
+
const fieldName = this.resolveFieldName(input);
|
|
1915
2196
|
expression = expression.replace(/\{root\}/g, rootName);
|
|
1916
2197
|
expression = expression.replace(/\{field\}/g, fieldName);
|
|
1917
2198
|
const fieldEntities = intentResult.entities.filter((e) => e.type === "field");
|
|
@@ -1980,6 +2261,58 @@ var TemplateEngine = class {
|
|
|
1980
2261
|
}
|
|
1981
2262
|
return { expression, unfilledSlots };
|
|
1982
2263
|
}
|
|
2264
|
+
/**
|
|
2265
|
+
* Resolve the SpEL root name for `input`: the configured schema root when one
|
|
2266
|
+
* exists, otherwise a keyword heuristic over the well-known roots.
|
|
2267
|
+
*/
|
|
2268
|
+
resolveRootName(input) {
|
|
2269
|
+
if (this.contextSchema?.root) {
|
|
2270
|
+
return this.contextSchema.root.name;
|
|
2271
|
+
}
|
|
2272
|
+
const rootMap = {
|
|
2273
|
+
\u8BA2\u5355: "order",
|
|
2274
|
+
order: "order",
|
|
2275
|
+
\u7528\u6237: "user",
|
|
2276
|
+
user: "user",
|
|
2277
|
+
\u6587\u4EF6: "file",
|
|
2278
|
+
file: "file",
|
|
2279
|
+
\u8D26\u53F7: "account",
|
|
2280
|
+
account: "account",
|
|
2281
|
+
\u5546\u54C1: "item",
|
|
2282
|
+
product: "item"
|
|
2283
|
+
};
|
|
2284
|
+
for (const [key, val] of Object.entries(rootMap)) {
|
|
2285
|
+
if (input.includes(key)) return val;
|
|
2286
|
+
}
|
|
2287
|
+
return "order";
|
|
2288
|
+
}
|
|
2289
|
+
/**
|
|
2290
|
+
* Resolve the field name for `input`.
|
|
2291
|
+
*
|
|
2292
|
+
* The previous implementation only compared schema *keys* and otherwise left
|
|
2293
|
+
* the literal placeholder default in place, which is how "#order.field == null"
|
|
2294
|
+
* reached callers. The lookup now falls back in order: schema key, schema
|
|
2295
|
+
* field description (Chinese inputs name the field by its description),
|
|
2296
|
+
* Chinese field word, first English identifier, and finally "value" — the
|
|
2297
|
+
* neutral default the pattern layer already uses for an unknown field.
|
|
2298
|
+
*/
|
|
2299
|
+
resolveFieldName(input) {
|
|
2300
|
+
const fields = this.contextSchema?.root?.fields;
|
|
2301
|
+
if (fields) {
|
|
2302
|
+
for (const [key, schema] of Object.entries(fields)) {
|
|
2303
|
+
if (input.includes(key)) return key;
|
|
2304
|
+
if (schema.description && input.includes(schema.description)) return key;
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
for (const [word, field] of CHINESE_FIELDS_BY_LENGTH) {
|
|
2308
|
+
if (input.includes(word)) return field;
|
|
2309
|
+
}
|
|
2310
|
+
const tokens = input.match(/[A-Za-z_][A-Za-z0-9_]*/g) ?? [];
|
|
2311
|
+
for (const token of tokens) {
|
|
2312
|
+
if (!FIELD_STOPWORDS.has(token.toLowerCase())) return token;
|
|
2313
|
+
}
|
|
2314
|
+
return "value";
|
|
2315
|
+
}
|
|
1983
2316
|
};
|
|
1984
2317
|
|
|
1985
2318
|
// src/template/prompts/prompt-builder.ts
|
|
@@ -2217,6 +2550,172 @@ ${userInput}
|
|
|
2217
2550
|
}
|
|
2218
2551
|
};
|
|
2219
2552
|
|
|
2553
|
+
// src/validation/validation-pipeline.ts
|
|
2554
|
+
import { TokenKind, Tokenizer } from "@agentix-e/spel-ts";
|
|
2555
|
+
|
|
2556
|
+
// src/validation/auto-fixer.ts
|
|
2557
|
+
function scanStringLiterals(expression) {
|
|
2558
|
+
const spans = [];
|
|
2559
|
+
let unterminatedQuote = null;
|
|
2560
|
+
let i = 0;
|
|
2561
|
+
while (i < expression.length) {
|
|
2562
|
+
const quote = expression[i];
|
|
2563
|
+
if (quote !== "'" && quote !== '"') {
|
|
2564
|
+
i += 1;
|
|
2565
|
+
continue;
|
|
2566
|
+
}
|
|
2567
|
+
const start = i;
|
|
2568
|
+
i += 1;
|
|
2569
|
+
let closed = false;
|
|
2570
|
+
while (i < expression.length) {
|
|
2571
|
+
if (expression[i] === quote) {
|
|
2572
|
+
if (expression[i + 1] === quote) {
|
|
2573
|
+
i += 2;
|
|
2574
|
+
continue;
|
|
2575
|
+
}
|
|
2576
|
+
i += 1;
|
|
2577
|
+
closed = true;
|
|
2578
|
+
break;
|
|
2579
|
+
}
|
|
2580
|
+
i += 1;
|
|
2581
|
+
}
|
|
2582
|
+
spans.push({ start, end: i });
|
|
2583
|
+
if (!closed) {
|
|
2584
|
+
unterminatedQuote = quote;
|
|
2585
|
+
break;
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
return { spans, unterminatedQuote };
|
|
2589
|
+
}
|
|
2590
|
+
function hasUnterminatedStringLiteral(expression) {
|
|
2591
|
+
return scanStringLiterals(expression).unterminatedQuote !== null;
|
|
2592
|
+
}
|
|
2593
|
+
function maskStringLiterals(expression) {
|
|
2594
|
+
const { spans } = scanStringLiterals(expression);
|
|
2595
|
+
if (spans.length === 0) return expression;
|
|
2596
|
+
const chars = expression.split("");
|
|
2597
|
+
for (const span of spans) {
|
|
2598
|
+
for (let i = span.start; i < span.end && i < chars.length; i++) {
|
|
2599
|
+
chars[i] = " ";
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
return chars.join("");
|
|
2603
|
+
}
|
|
2604
|
+
var AutoFixer = class {
|
|
2605
|
+
fix(expression) {
|
|
2606
|
+
const { chunks, unterminatedQuote } = this.split(expression);
|
|
2607
|
+
const changes = [];
|
|
2608
|
+
this.applyRule(
|
|
2609
|
+
chunks,
|
|
2610
|
+
/=== undefined/g,
|
|
2611
|
+
"== null",
|
|
2612
|
+
() => "Replaced === undefined with == null",
|
|
2613
|
+
changes
|
|
2614
|
+
);
|
|
2615
|
+
this.applyRule(
|
|
2616
|
+
chunks,
|
|
2617
|
+
/!== undefined/g,
|
|
2618
|
+
"!= null",
|
|
2619
|
+
() => "Replaced !== undefined with != null",
|
|
2620
|
+
changes
|
|
2621
|
+
);
|
|
2622
|
+
this.applyRule(chunks, /!==/g, "!=", (n) => `Replaced ${n}x !== with !=`, changes);
|
|
2623
|
+
this.applyRule(chunks, /===/g, "==", (n) => `Replaced ${n}x === with ==`, changes);
|
|
2624
|
+
this.applyRule(chunks, /&&/g, "and", (n) => `Replaced ${n}x && with and`, changes);
|
|
2625
|
+
this.applyRule(chunks, /\|\|/g, "or", (n) => `Replaced ${n}x || with or`, changes);
|
|
2626
|
+
this.applyRule(chunks, /> ==/g, ">=", () => "Replaced > == with >=", changes);
|
|
2627
|
+
this.applyRule(chunks, /< ==/g, "<=", () => "Replaced < == with <=", changes);
|
|
2628
|
+
this.applyElvisRule(chunks, changes);
|
|
2629
|
+
let fixed = chunks.map((chunk) => chunk.text).join("");
|
|
2630
|
+
if (unterminatedQuote !== null && !fixed.endsWith(unterminatedQuote)) {
|
|
2631
|
+
fixed += unterminatedQuote;
|
|
2632
|
+
changes.push(
|
|
2633
|
+
unterminatedQuote === "'" ? "Added missing closing single quote" : "Added missing closing double quote"
|
|
2634
|
+
);
|
|
2635
|
+
}
|
|
2636
|
+
const wasFixed = changes.length > 0;
|
|
2637
|
+
return {
|
|
2638
|
+
wasFixed,
|
|
2639
|
+
expression: wasFixed ? fixed : expression,
|
|
2640
|
+
changes
|
|
2641
|
+
};
|
|
2642
|
+
}
|
|
2643
|
+
/**
|
|
2644
|
+
* Apply one global replacement rule to every unprotected chunk.
|
|
2645
|
+
*
|
|
2646
|
+
* The replacement is only reported when it actually matched, and the count
|
|
2647
|
+
* is the number of matches across all chunks, so the human-readable change
|
|
2648
|
+
* log is unchanged from the previous implementation.
|
|
2649
|
+
*/
|
|
2650
|
+
applyRule(chunks, pattern, replacement, describe, changes) {
|
|
2651
|
+
let count = 0;
|
|
2652
|
+
for (const chunk of chunks) {
|
|
2653
|
+
if (chunk.protected) continue;
|
|
2654
|
+
const matches = chunk.text.match(pattern);
|
|
2655
|
+
if (matches === null) continue;
|
|
2656
|
+
count += matches.length;
|
|
2657
|
+
chunk.text = chunk.text.replace(pattern, replacement);
|
|
2658
|
+
}
|
|
2659
|
+
if (count > 0) {
|
|
2660
|
+
changes.push(describe(count));
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
/**
|
|
2664
|
+
* Normalise the Elvis operator only when it was written with whitespace
|
|
2665
|
+
* between `?` and `:`. A correctly written `?:` must be left untouched so
|
|
2666
|
+
* that `fix()` is a no-op on already-valid input.
|
|
2667
|
+
*/
|
|
2668
|
+
applyElvisRule(chunks, changes) {
|
|
2669
|
+
let touched = false;
|
|
2670
|
+
for (const chunk of chunks) {
|
|
2671
|
+
if (chunk.protected || !chunk.text.includes("? :")) continue;
|
|
2672
|
+
const next = chunk.text.replace(/\s*\?\s*:\s*/g, " ?: ");
|
|
2673
|
+
if (next !== chunk.text) {
|
|
2674
|
+
chunk.text = next;
|
|
2675
|
+
touched = true;
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
if (touched) {
|
|
2679
|
+
changes.push("Fixed Elvis operator spacing");
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
/**
|
|
2683
|
+
* Split the expression into alternating protected (string literal) and
|
|
2684
|
+
* unprotected chunks. Ranges come from spel-ts's tokenizer so they agree
|
|
2685
|
+
* exactly with what the parser considers opaque.
|
|
2686
|
+
*/
|
|
2687
|
+
split(expression) {
|
|
2688
|
+
const { spans, unterminatedQuote } = scanStringLiterals(expression);
|
|
2689
|
+
return { chunks: this.toChunks(expression, spans), unterminatedQuote };
|
|
2690
|
+
}
|
|
2691
|
+
/**
|
|
2692
|
+
* Turn protected spans into a chunk list covering the whole expression.
|
|
2693
|
+
*/
|
|
2694
|
+
toChunks(expression, spans) {
|
|
2695
|
+
const ordered = [...spans].sort((a, b) => a.start - b.start);
|
|
2696
|
+
const chunks = [];
|
|
2697
|
+
let cursor = 0;
|
|
2698
|
+
for (const span of ordered) {
|
|
2699
|
+
const start = Math.max(span.start, cursor);
|
|
2700
|
+
const end = Math.max(span.end, start);
|
|
2701
|
+
if (start > cursor) {
|
|
2702
|
+
chunks.push({ protected: false, text: expression.slice(cursor, start) });
|
|
2703
|
+
}
|
|
2704
|
+
if (end > start) {
|
|
2705
|
+
chunks.push({ protected: true, text: expression.slice(start, end) });
|
|
2706
|
+
}
|
|
2707
|
+
cursor = end;
|
|
2708
|
+
}
|
|
2709
|
+
if (cursor < expression.length) {
|
|
2710
|
+
chunks.push({ protected: false, text: expression.slice(cursor) });
|
|
2711
|
+
}
|
|
2712
|
+
if (chunks.length === 0) {
|
|
2713
|
+
chunks.push({ protected: false, text: "" });
|
|
2714
|
+
}
|
|
2715
|
+
return chunks;
|
|
2716
|
+
}
|
|
2717
|
+
};
|
|
2718
|
+
|
|
2220
2719
|
// src/validation/validation-pipeline.ts
|
|
2221
2720
|
var ValidationPipeline = class {
|
|
2222
2721
|
evaluator;
|
|
@@ -2236,25 +2735,29 @@ var ValidationPipeline = class {
|
|
|
2236
2735
|
const typeStage = this.validateTypes(expression, contextSchema);
|
|
2237
2736
|
const semanticStage = this.validateSemantic(expression);
|
|
2238
2737
|
const contextStage = this.validateContext(expression, contextSchema);
|
|
2738
|
+
const finalStage = this.validateFinal(expression);
|
|
2239
2739
|
errors.push(
|
|
2240
2740
|
...parseStage.errors,
|
|
2241
2741
|
...typeStage.errors,
|
|
2242
2742
|
...semanticStage.errors,
|
|
2243
|
-
...contextStage.errors
|
|
2743
|
+
...contextStage.errors,
|
|
2744
|
+
...finalStage.errors
|
|
2244
2745
|
);
|
|
2245
2746
|
warnings.push(
|
|
2246
2747
|
...parseStage.warnings,
|
|
2247
2748
|
...typeStage.warnings,
|
|
2248
2749
|
...semanticStage.warnings,
|
|
2249
|
-
...contextStage.warnings
|
|
2750
|
+
...contextStage.warnings,
|
|
2751
|
+
...finalStage.warnings
|
|
2250
2752
|
);
|
|
2251
2753
|
return {
|
|
2252
|
-
valid:
|
|
2754
|
+
valid: errors.length === 0,
|
|
2253
2755
|
stages: {
|
|
2254
2756
|
parse: parseStage,
|
|
2255
2757
|
type: typeStage,
|
|
2256
2758
|
semantic: semanticStage,
|
|
2257
|
-
context: contextStage
|
|
2759
|
+
context: contextStage,
|
|
2760
|
+
final: finalStage
|
|
2258
2761
|
},
|
|
2259
2762
|
errors,
|
|
2260
2763
|
warnings
|
|
@@ -2262,47 +2765,57 @@ var ValidationPipeline = class {
|
|
|
2262
2765
|
}
|
|
2263
2766
|
/**
|
|
2264
2767
|
* Stage 1: Parse Check — syntax validation
|
|
2768
|
+
*
|
|
2769
|
+
* Delimiter balance and JavaScript operators are checked against a copy of
|
|
2770
|
+
* the expression with string literals blanked out, so literal text can never
|
|
2771
|
+
* be mistaken for syntax.
|
|
2265
2772
|
*/
|
|
2266
2773
|
async validateParse(expression) {
|
|
2267
2774
|
const errors = [];
|
|
2268
2775
|
const warnings = [];
|
|
2776
|
+
const structural = maskStringLiterals(expression);
|
|
2269
2777
|
if (!expression || expression.trim().length === 0) {
|
|
2270
2778
|
errors.push({
|
|
2271
2779
|
code: "PARSE-EMPTY",
|
|
2272
2780
|
message: "Expression is empty",
|
|
2781
|
+
severity: "error",
|
|
2273
2782
|
stage: "parse",
|
|
2274
2783
|
requiresLLM: true
|
|
2275
2784
|
});
|
|
2276
2785
|
return { passed: false, errors, warnings };
|
|
2277
2786
|
}
|
|
2278
|
-
if (!this.hasBalancedParentheses(
|
|
2787
|
+
if (!this.hasBalancedParentheses(structural)) {
|
|
2279
2788
|
errors.push({
|
|
2280
2789
|
code: "PARSE-UNBALANCED_PARENS",
|
|
2281
2790
|
message: "Unbalanced parentheses in expression",
|
|
2791
|
+
severity: "error",
|
|
2282
2792
|
stage: "parse",
|
|
2283
2793
|
requiresLLM: true
|
|
2284
2794
|
});
|
|
2285
2795
|
}
|
|
2286
|
-
if (
|
|
2796
|
+
if (structural.includes("===") || structural.includes("!==")) {
|
|
2287
2797
|
errors.push({
|
|
2288
2798
|
code: "PARSE-JS_OPERATOR",
|
|
2289
2799
|
message: "JavaScript operators detected (=== or !==), use == or != in SpEL",
|
|
2800
|
+
severity: "error",
|
|
2290
2801
|
stage: "parse",
|
|
2291
2802
|
requiresLLM: true
|
|
2292
2803
|
});
|
|
2293
2804
|
}
|
|
2294
|
-
if (
|
|
2805
|
+
if (structural.includes("&&")) {
|
|
2295
2806
|
errors.push({
|
|
2296
2807
|
code: "PARSE-JS_LOGIC",
|
|
2297
2808
|
message: 'JavaScript && detected, use "and" in SpEL',
|
|
2809
|
+
severity: "error",
|
|
2298
2810
|
stage: "parse",
|
|
2299
2811
|
requiresLLM: true
|
|
2300
2812
|
});
|
|
2301
2813
|
}
|
|
2302
|
-
if (
|
|
2814
|
+
if (structural.includes("||")) {
|
|
2303
2815
|
errors.push({
|
|
2304
2816
|
code: "PARSE-JS_LOGIC",
|
|
2305
2817
|
message: 'JavaScript || detected, use "or" in SpEL',
|
|
2818
|
+
severity: "error",
|
|
2306
2819
|
stage: "parse",
|
|
2307
2820
|
requiresLLM: true
|
|
2308
2821
|
});
|
|
@@ -2315,6 +2828,7 @@ var ValidationPipeline = class {
|
|
|
2315
2828
|
errors.push({
|
|
2316
2829
|
code: `PARSE-${pe.code ?? "SYNTAX"}`,
|
|
2317
2830
|
message: pe.message,
|
|
2831
|
+
severity: "error",
|
|
2318
2832
|
position: pe.position,
|
|
2319
2833
|
stage: "parse",
|
|
2320
2834
|
requiresLLM: true
|
|
@@ -2325,6 +2839,7 @@ var ValidationPipeline = class {
|
|
|
2325
2839
|
errors.push({
|
|
2326
2840
|
code: "PARSE-EXCEPTION",
|
|
2327
2841
|
message: `Parse threw exception: ${err.message}`,
|
|
2842
|
+
severity: "error",
|
|
2328
2843
|
stage: "parse",
|
|
2329
2844
|
requiresLLM: true
|
|
2330
2845
|
});
|
|
@@ -2337,27 +2852,43 @@ var ValidationPipeline = class {
|
|
|
2337
2852
|
};
|
|
2338
2853
|
}
|
|
2339
2854
|
/**
|
|
2340
|
-
* Stage 2: Type Check — type validation
|
|
2855
|
+
* Stage 2: Type Check — advisory type validation
|
|
2341
2856
|
*/
|
|
2342
2857
|
validateTypes(expression, contextSchema) {
|
|
2343
2858
|
const errors = [];
|
|
2344
2859
|
const warnings = [];
|
|
2345
|
-
const
|
|
2860
|
+
const structural = maskStringLiterals(expression);
|
|
2861
|
+
const strNumMismatch = /'(?:\\.|[^'\\])*'\s*(?:>|<|>=|<=)\s*\d+|\d+\s*(?:>|<|>=|<=)\s*'(?:\\.|[^'\\])*'/;
|
|
2346
2862
|
if (strNumMismatch.test(expression)) {
|
|
2347
2863
|
warnings.push({
|
|
2348
2864
|
code: "TYPE-STR_NUM_CMP",
|
|
2349
2865
|
message: "String literal compared with number using arithmetic operator",
|
|
2866
|
+
severity: "warning",
|
|
2350
2867
|
stage: "type"
|
|
2351
2868
|
});
|
|
2352
2869
|
}
|
|
2353
2870
|
if (contextSchema?.root) {
|
|
2871
|
+
const rootRef = escapeRegExp(contextSchema.root.name);
|
|
2354
2872
|
for (const [fieldName, field] of Object.entries(contextSchema.root.fields)) {
|
|
2873
|
+
const fieldRef = `#(?:${rootRef}\\.)?${escapeRegExp(fieldName)}`;
|
|
2355
2874
|
if (field.type === "boolean") {
|
|
2356
|
-
const boolNumPattern = new RegExp(
|
|
2357
|
-
if (boolNumPattern.test(
|
|
2875
|
+
const boolNumPattern = new RegExp(`${fieldRef}\\s*(?:>|<|>=|<=)\\s*\\d+`);
|
|
2876
|
+
if (boolNumPattern.test(structural)) {
|
|
2358
2877
|
warnings.push({
|
|
2359
2878
|
code: "TYPE-BOOL_NUM_CMP",
|
|
2360
2879
|
message: `Boolean field '${fieldName}' compared with number`,
|
|
2880
|
+
severity: "warning",
|
|
2881
|
+
stage: "type"
|
|
2882
|
+
});
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
if (field.type === "number") {
|
|
2886
|
+
const numStrPattern = new RegExp(`${fieldRef}\\s*(?:>|<|>=|<=)\\s*'(?:\\\\.|[^'\\\\])*'`);
|
|
2887
|
+
if (numStrPattern.test(expression)) {
|
|
2888
|
+
warnings.push({
|
|
2889
|
+
code: "TYPE-STR_NUM_CMP",
|
|
2890
|
+
message: `Numeric field '${fieldName}' compared with a string literal`,
|
|
2891
|
+
severity: "warning",
|
|
2361
2892
|
stage: "type"
|
|
2362
2893
|
});
|
|
2363
2894
|
}
|
|
@@ -2371,23 +2902,26 @@ var ValidationPipeline = class {
|
|
|
2371
2902
|
};
|
|
2372
2903
|
}
|
|
2373
2904
|
/**
|
|
2374
|
-
* Stage 3: Semantic Check — semantic
|
|
2905
|
+
* Stage 3: Semantic Check — advisory semantic validation
|
|
2375
2906
|
*/
|
|
2376
2907
|
validateSemantic(expression) {
|
|
2377
2908
|
const errors = [];
|
|
2378
2909
|
const warnings = [];
|
|
2910
|
+
const structural = maskStringLiterals(expression);
|
|
2379
2911
|
const selfCompare = /(#\w+(?:\.\w+)*)\s*==\s*\1/;
|
|
2380
|
-
if (selfCompare.test(
|
|
2912
|
+
if (selfCompare.test(structural)) {
|
|
2381
2913
|
warnings.push({
|
|
2382
2914
|
code: "SEM-SELF_COMPARE",
|
|
2383
2915
|
message: "Self-comparison detected: expression is always true",
|
|
2916
|
+
severity: "warning",
|
|
2384
2917
|
stage: "semantic"
|
|
2385
2918
|
});
|
|
2386
2919
|
}
|
|
2387
|
-
if (
|
|
2920
|
+
if (structural.includes("!!")) {
|
|
2388
2921
|
warnings.push({
|
|
2389
2922
|
code: "SEM-DOUBLE_NEGATION",
|
|
2390
2923
|
message: "Double negation detected, consider simplifying",
|
|
2924
|
+
severity: "warning",
|
|
2391
2925
|
stage: "semantic"
|
|
2392
2926
|
});
|
|
2393
2927
|
}
|
|
@@ -2399,59 +2933,72 @@ var ValidationPipeline = class {
|
|
|
2399
2933
|
}
|
|
2400
2934
|
/**
|
|
2401
2935
|
* Stage 4: Context Check — context reference validation
|
|
2936
|
+
*
|
|
2937
|
+
* A supplied schema turns this stage into a real gate: an undeclared bean,
|
|
2938
|
+
* a missing root field or an undeclared variable is an error. Without a
|
|
2939
|
+
* schema nothing can be judged, so the stage stays advisory.
|
|
2402
2940
|
*/
|
|
2403
2941
|
validateContext(expression, contextSchema) {
|
|
2404
2942
|
const errors = [];
|
|
2405
2943
|
const warnings = [];
|
|
2944
|
+
const structural = maskStringLiterals(expression);
|
|
2406
2945
|
if (!contextSchema) {
|
|
2407
2946
|
warnings.push({
|
|
2408
2947
|
code: "CTX-NO_SCHEMA",
|
|
2409
2948
|
message: "No ContextSchema provided, skipping context validation",
|
|
2949
|
+
severity: "warning",
|
|
2410
2950
|
stage: "context"
|
|
2411
2951
|
});
|
|
2412
2952
|
return { passed: true, errors, warnings };
|
|
2413
2953
|
}
|
|
2414
|
-
const
|
|
2415
|
-
|
|
2416
|
-
|
|
2954
|
+
const variables = contextSchema.variables ?? {};
|
|
2955
|
+
const functions = contextSchema.functions ?? {};
|
|
2956
|
+
const beans = contextSchema.beans ?? {};
|
|
2957
|
+
const root = contextSchema.root;
|
|
2958
|
+
const rootFields = root?.fields ?? {};
|
|
2959
|
+
const refs = this.extractReferences(structural);
|
|
2960
|
+
if (root) {
|
|
2417
2961
|
for (const ref of refs) {
|
|
2418
|
-
if (ref.startsWith(`#${
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
}
|
|
2962
|
+
if (!ref.startsWith(`#${root.name}.`)) continue;
|
|
2963
|
+
const field = ref.split(".")[1];
|
|
2964
|
+
if (field && !(field in rootFields)) {
|
|
2965
|
+
errors.push({
|
|
2966
|
+
code: "CTX-UNKNOWN_FIELD",
|
|
2967
|
+
message: `Field '${field}' not found in root '${root.name}'`,
|
|
2968
|
+
severity: "error",
|
|
2969
|
+
stage: "context",
|
|
2970
|
+
requiresLLM: true
|
|
2971
|
+
});
|
|
2428
2972
|
}
|
|
2429
2973
|
}
|
|
2430
2974
|
}
|
|
2431
2975
|
for (const ref of refs) {
|
|
2432
|
-
if (ref.startsWith("#")
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2976
|
+
if (!ref.startsWith("#") || ref.includes(".")) continue;
|
|
2977
|
+
const varName = ref.slice(1);
|
|
2978
|
+
const known = varName === "this" || varName === "root" || root?.name === varName || varName in variables || varName in functions || // A bare `#name` may also name a root field, which the generator emits
|
|
2979
|
+
// as shorthand for `#root.name`.
|
|
2980
|
+
varName in rootFields;
|
|
2981
|
+
if (!known) {
|
|
2982
|
+
errors.push({
|
|
2983
|
+
code: "CTX-UNKNOWN_REF",
|
|
2984
|
+
message: `Unknown reference '${ref}'`,
|
|
2985
|
+
severity: "error",
|
|
2986
|
+
stage: "context",
|
|
2987
|
+
requiresLLM: true
|
|
2988
|
+
});
|
|
2444
2989
|
}
|
|
2445
2990
|
}
|
|
2446
|
-
const beanMatch =
|
|
2447
|
-
if (beanMatch
|
|
2991
|
+
const beanMatch = structural.match(/@(\w+)/g);
|
|
2992
|
+
if (beanMatch) {
|
|
2448
2993
|
for (const b of beanMatch) {
|
|
2449
2994
|
const beanName = b.slice(1);
|
|
2450
|
-
if (!(beanName in
|
|
2451
|
-
|
|
2995
|
+
if (!(beanName in beans)) {
|
|
2996
|
+
errors.push({
|
|
2452
2997
|
code: "CTX-UNKNOWN_BEAN",
|
|
2453
2998
|
message: `Bean '${beanName}' not found in ContextSchema`,
|
|
2454
|
-
|
|
2999
|
+
severity: "error",
|
|
3000
|
+
stage: "context",
|
|
3001
|
+
requiresLLM: true
|
|
2455
3002
|
});
|
|
2456
3003
|
}
|
|
2457
3004
|
}
|
|
@@ -2463,7 +3010,62 @@ var ValidationPipeline = class {
|
|
|
2463
3010
|
};
|
|
2464
3011
|
}
|
|
2465
3012
|
/**
|
|
2466
|
-
* Check
|
|
3013
|
+
* Stage 5: Final Check — the generated expression must parse.
|
|
3014
|
+
*
|
|
3015
|
+
* When an evaluator is configured the parse stage already performed a real
|
|
3016
|
+
* parse; this mandatory final gate adds the structural check that a truncated
|
|
3017
|
+
* tail (an expression ending on an operator) is never accepted, even when no
|
|
3018
|
+
* engine is wired in. It is deliberately conservative: only a trailing
|
|
3019
|
+
* operator or an unterminated literal fails, so no well-formed expression is
|
|
3020
|
+
* rejected.
|
|
3021
|
+
*/
|
|
3022
|
+
validateFinal(expression) {
|
|
3023
|
+
const errors = [];
|
|
3024
|
+
const warnings = [];
|
|
3025
|
+
if (this.isManifestlyIncomplete(expression)) {
|
|
3026
|
+
errors.push({
|
|
3027
|
+
code: "FINAL-TRUNCATED",
|
|
3028
|
+
message: "Expression is incomplete: it ends with an operator or is not terminated",
|
|
3029
|
+
severity: "error",
|
|
3030
|
+
stage: "final",
|
|
3031
|
+
requiresLLM: true
|
|
3032
|
+
});
|
|
3033
|
+
}
|
|
3034
|
+
return { passed: errors.length === 0, errors, warnings };
|
|
3035
|
+
}
|
|
3036
|
+
/**
|
|
3037
|
+
* Whether the expression is obviously incomplete.
|
|
3038
|
+
*
|
|
3039
|
+
* The last token is obtained from spel-ts's tokenizer, so a trailing
|
|
3040
|
+
* operator keyword or an unterminated string literal is detected exactly,
|
|
3041
|
+
* and text inside a literal is never read as a trailing operator.
|
|
3042
|
+
*/
|
|
3043
|
+
isManifestlyIncomplete(expression) {
|
|
3044
|
+
if (expression.trim().length === 0) return true;
|
|
3045
|
+
const tokenizer = new Tokenizer(expression);
|
|
3046
|
+
let lastKind = null;
|
|
3047
|
+
let lastLiteral;
|
|
3048
|
+
let previousKind = null;
|
|
3049
|
+
try {
|
|
3050
|
+
for (; ; ) {
|
|
3051
|
+
const token = tokenizer.nextToken();
|
|
3052
|
+
if (token.kind === TokenKind.EOF) break;
|
|
3053
|
+
previousKind = lastKind;
|
|
3054
|
+
lastKind = token.kind;
|
|
3055
|
+
lastLiteral = token.literal;
|
|
3056
|
+
}
|
|
3057
|
+
} catch {
|
|
3058
|
+
return hasUnterminatedStringLiteral(expression);
|
|
3059
|
+
}
|
|
3060
|
+
if (lastKind === null) return true;
|
|
3061
|
+
if (INCOMPLETE_TRAILING_TOKENS.has(lastKind)) return true;
|
|
3062
|
+
if (lastLiteral !== void 0 && /^[A-Za-z]+$/.test(lastLiteral) && lastKind !== TokenKind.LITERAL_STRING && previousKind !== TokenKind.DOT && previousKind !== TokenKind.SAFE_NAV && WORD_OPERATORS.has(lastLiteral.toLowerCase())) {
|
|
3063
|
+
return true;
|
|
3064
|
+
}
|
|
3065
|
+
return false;
|
|
3066
|
+
}
|
|
3067
|
+
/**
|
|
3068
|
+
* Check if parentheses are balanced. Expects a literal-masked expression.
|
|
2467
3069
|
*/
|
|
2468
3070
|
hasBalancedParentheses(expression) {
|
|
2469
3071
|
const stack = [];
|
|
@@ -2479,17 +3081,29 @@ var ValidationPipeline = class {
|
|
|
2479
3081
|
return stack.length === 0;
|
|
2480
3082
|
}
|
|
2481
3083
|
/**
|
|
2482
|
-
* Extract all identifier references from expression
|
|
3084
|
+
* Extract all identifier references from expression.
|
|
3085
|
+
*
|
|
3086
|
+
* A bare `#x` that is only the head of a dotted reference (`#x.y`) is not
|
|
3087
|
+
* emitted on its own: a dotted reference names a property of some object,
|
|
3088
|
+
* not a variable of that name.
|
|
2483
3089
|
*/
|
|
2484
3090
|
extractReferences(expression) {
|
|
2485
3091
|
const refs = [];
|
|
3092
|
+
const dottedHeads = /* @__PURE__ */ new Set();
|
|
2486
3093
|
const varMatch = expression.matchAll(/#(\w+(?:\.\w+(?:\.\w+)?)?)/g);
|
|
2487
3094
|
for (const m of varMatch) {
|
|
2488
|
-
|
|
3095
|
+
const ref = `#${m[1]}`;
|
|
3096
|
+
if (!refs.includes(ref)) {
|
|
3097
|
+
refs.push(ref);
|
|
3098
|
+
}
|
|
3099
|
+
if (ref.includes(".")) {
|
|
3100
|
+
dottedHeads.add(ref.split(".")[0]);
|
|
3101
|
+
}
|
|
2489
3102
|
}
|
|
2490
3103
|
const simpleMatch = expression.matchAll(/#(\w+)(?!\w*\()/g);
|
|
2491
3104
|
for (const m of simpleMatch) {
|
|
2492
3105
|
const ref = `#${m[1]}`;
|
|
3106
|
+
if (dottedHeads.has(ref)) continue;
|
|
2493
3107
|
if (!refs.includes(ref)) {
|
|
2494
3108
|
refs.push(ref);
|
|
2495
3109
|
}
|
|
@@ -2497,98 +3111,68 @@ var ValidationPipeline = class {
|
|
|
2497
3111
|
return refs;
|
|
2498
3112
|
}
|
|
2499
3113
|
};
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
fixed = fixed.replace(/\?\s*:\s*/g, " ?: ");
|
|
2563
|
-
changes.push("Fixed Elvis operator spacing");
|
|
2564
|
-
}
|
|
2565
|
-
const wasFixed = changes.length > 0;
|
|
2566
|
-
return {
|
|
2567
|
-
wasFixed,
|
|
2568
|
-
expression: wasFixed ? fixed : expression,
|
|
2569
|
-
changes
|
|
2570
|
-
};
|
|
2571
|
-
}
|
|
2572
|
-
fixAllBrackets(expr) {
|
|
2573
|
-
let result = expr;
|
|
2574
|
-
const openParen = (result.match(/\(/g) ?? []).length;
|
|
2575
|
-
const closeParen = (result.match(/\)/g) ?? []).length;
|
|
2576
|
-
if (openParen > closeParen) {
|
|
2577
|
-
result += ")".repeat(openParen - closeParen);
|
|
2578
|
-
}
|
|
2579
|
-
const openBracket = (result.match(/\[/g) ?? []).length;
|
|
2580
|
-
const closeBracket = (result.match(/\]/g) ?? []).length;
|
|
2581
|
-
if (openBracket > closeBracket) {
|
|
2582
|
-
result += "]".repeat(openBracket - closeBracket);
|
|
2583
|
-
}
|
|
2584
|
-
const openBrace = (result.match(/\{/g) ?? []).length;
|
|
2585
|
-
const closeBrace = (result.match(/\}/g) ?? []).length;
|
|
2586
|
-
if (openBrace > closeBrace) {
|
|
2587
|
-
result += "}".repeat(openBrace - closeBrace);
|
|
2588
|
-
}
|
|
2589
|
-
return result;
|
|
2590
|
-
}
|
|
2591
|
-
};
|
|
3114
|
+
var INCOMPLETE_TRAILING_TOKENS = /* @__PURE__ */ new Set([
|
|
3115
|
+
TokenKind.PLUS,
|
|
3116
|
+
TokenKind.MINUS,
|
|
3117
|
+
TokenKind.STAR,
|
|
3118
|
+
TokenKind.SLASH,
|
|
3119
|
+
TokenKind.PERCENT,
|
|
3120
|
+
TokenKind.MOD,
|
|
3121
|
+
TokenKind.POWER,
|
|
3122
|
+
TokenKind.INC,
|
|
3123
|
+
TokenKind.DEC,
|
|
3124
|
+
TokenKind.EQ,
|
|
3125
|
+
TokenKind.NE,
|
|
3126
|
+
TokenKind.LT,
|
|
3127
|
+
TokenKind.LE,
|
|
3128
|
+
TokenKind.GT,
|
|
3129
|
+
TokenKind.GE,
|
|
3130
|
+
TokenKind.AND,
|
|
3131
|
+
TokenKind.OR,
|
|
3132
|
+
TokenKind.NOT,
|
|
3133
|
+
TokenKind.ASSIGN,
|
|
3134
|
+
TokenKind.MATCHES,
|
|
3135
|
+
TokenKind.BETWEEN,
|
|
3136
|
+
TokenKind.INSTANCEOF,
|
|
3137
|
+
TokenKind.LPAREN,
|
|
3138
|
+
TokenKind.LBRACKET,
|
|
3139
|
+
TokenKind.LBRACE,
|
|
3140
|
+
TokenKind.COMMA,
|
|
3141
|
+
TokenKind.COLON,
|
|
3142
|
+
TokenKind.DOT,
|
|
3143
|
+
TokenKind.SAFE_NAV,
|
|
3144
|
+
TokenKind.QMARK,
|
|
3145
|
+
TokenKind.ELVIS,
|
|
3146
|
+
TokenKind.HASH,
|
|
3147
|
+
TokenKind.AT,
|
|
3148
|
+
TokenKind.AMP_AT,
|
|
3149
|
+
TokenKind.PROJECTION,
|
|
3150
|
+
TokenKind.SELECTION,
|
|
3151
|
+
TokenKind.SELECT_FIRST,
|
|
3152
|
+
TokenKind.SELECT_LAST,
|
|
3153
|
+
TokenKind.TYPE_START,
|
|
3154
|
+
TokenKind.NEW,
|
|
3155
|
+
TokenKind.DOTDOT
|
|
3156
|
+
]);
|
|
3157
|
+
var WORD_OPERATORS = /* @__PURE__ */ new Set([
|
|
3158
|
+
"and",
|
|
3159
|
+
"or",
|
|
3160
|
+
"not",
|
|
3161
|
+
"matches",
|
|
3162
|
+
"between",
|
|
3163
|
+
"instanceof",
|
|
3164
|
+
// The textual operators in Spring's ALTERNATIVE_OPERATOR_NAMES (`eq`, `ne`,
|
|
3165
|
+
// `div`, …) tokenize as their own kinds where the engine defines one, and as
|
|
3166
|
+
// plain identifiers where it does not. `div` is the one that has no kind on
|
|
3167
|
+
// the engine build this package compiles against — `1 div 2` tokenizes as
|
|
3168
|
+
// IDENTIFIER — so it is listed here to keep `#a div` from passing as complete.
|
|
3169
|
+
// A property named `div` is still safe: the preceding-dot guard below exempts
|
|
3170
|
+
// `#order.div`.
|
|
3171
|
+
"div"
|
|
3172
|
+
]);
|
|
3173
|
+
function escapeRegExp(value) {
|
|
3174
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3175
|
+
}
|
|
2592
3176
|
|
|
2593
3177
|
// src/validation/self-correction-loop.ts
|
|
2594
3178
|
var SelfCorrectionLoop = class {
|
|
@@ -2746,20 +3330,23 @@ var StrategyRouter = class {
|
|
|
2746
3330
|
if (contextSchema) {
|
|
2747
3331
|
this.templateEngine.setContext(contextSchema);
|
|
2748
3332
|
}
|
|
2749
|
-
const
|
|
2750
|
-
|
|
2751
|
-
|
|
3333
|
+
const isCompound = splitClauses(nl).length > 1;
|
|
3334
|
+
let clauseFailure = null;
|
|
3335
|
+
const wholeMatch = this.patternMatcher.match(nl);
|
|
3336
|
+
const wholeIsFaithful = wholeMatch.matched && wholeMatch.confidence >= this.config.patternMinConfidence && (!isCompound || (wholeMatch.pattern?.tags.includes("logic") ?? false));
|
|
3337
|
+
if (wholeIsFaithful) {
|
|
3338
|
+
const validation = await this.validationPipeline.validate(wholeMatch.spel, contextSchema);
|
|
2752
3339
|
if (validation.valid) {
|
|
2753
3340
|
return {
|
|
2754
|
-
expression:
|
|
3341
|
+
expression: wholeMatch.spel,
|
|
2755
3342
|
strategy: "pattern",
|
|
2756
|
-
confidence:
|
|
2757
|
-
metadata: { patternId:
|
|
3343
|
+
confidence: wholeMatch.confidence,
|
|
3344
|
+
metadata: { patternId: wholeMatch.pattern?.id },
|
|
2758
3345
|
latencyMs: Date.now() - startTime
|
|
2759
3346
|
};
|
|
2760
3347
|
}
|
|
2761
3348
|
try {
|
|
2762
|
-
const afResult = this.autoFixer.fix(
|
|
3349
|
+
const afResult = this.autoFixer.fix(wholeMatch.spel);
|
|
2763
3350
|
if (afResult.wasFixed) {
|
|
2764
3351
|
const afValidation = await this.validationPipeline.validate(
|
|
2765
3352
|
afResult.expression,
|
|
@@ -2769,8 +3356,8 @@ var StrategyRouter = class {
|
|
|
2769
3356
|
return {
|
|
2770
3357
|
expression: afResult.expression,
|
|
2771
3358
|
strategy: "pattern",
|
|
2772
|
-
confidence:
|
|
2773
|
-
metadata: { patternId:
|
|
3359
|
+
confidence: wholeMatch.confidence * 0.95,
|
|
3360
|
+
metadata: { patternId: wholeMatch.pattern?.id },
|
|
2774
3361
|
latencyMs: Date.now() - startTime
|
|
2775
3362
|
};
|
|
2776
3363
|
}
|
|
@@ -2778,6 +3365,30 @@ var StrategyRouter = class {
|
|
|
2778
3365
|
} catch {
|
|
2779
3366
|
}
|
|
2780
3367
|
}
|
|
3368
|
+
if (isCompound && !wholeIsFaithful) {
|
|
3369
|
+
let decomposition = null;
|
|
3370
|
+
try {
|
|
3371
|
+
decomposition = this.decomposeClauses(nl);
|
|
3372
|
+
} catch (error) {
|
|
3373
|
+
if (!(error instanceof UnconvertibleClauseError)) throw error;
|
|
3374
|
+
clauseFailure = error;
|
|
3375
|
+
}
|
|
3376
|
+
if (decomposition) {
|
|
3377
|
+
const validation = await this.validationPipeline.validate(
|
|
3378
|
+
decomposition.expression,
|
|
3379
|
+
contextSchema
|
|
3380
|
+
);
|
|
3381
|
+
if (validation.valid) {
|
|
3382
|
+
return {
|
|
3383
|
+
expression: decomposition.expression,
|
|
3384
|
+
strategy: "pattern",
|
|
3385
|
+
confidence: decomposition.confidence,
|
|
3386
|
+
metadata: { clauses: decomposition.clauses },
|
|
3387
|
+
latencyMs: Date.now() - startTime
|
|
3388
|
+
};
|
|
3389
|
+
}
|
|
3390
|
+
}
|
|
3391
|
+
}
|
|
2781
3392
|
const intentResult = this.intentClassifier.classify(nl);
|
|
2782
3393
|
let templateResult = null;
|
|
2783
3394
|
try {
|
|
@@ -2811,7 +3422,7 @@ var StrategyRouter = class {
|
|
|
2811
3422
|
}
|
|
2812
3423
|
}
|
|
2813
3424
|
if (providers.length === 0) {
|
|
2814
|
-
throw new Error("No LLM providers available");
|
|
3425
|
+
throw clauseFailure ?? new Error("No LLM providers available");
|
|
2815
3426
|
}
|
|
2816
3427
|
let lastError = null;
|
|
2817
3428
|
for (const provider of providers) {
|
|
@@ -2857,6 +3468,33 @@ var StrategyRouter = class {
|
|
|
2857
3468
|
/**
|
|
2858
3469
|
* Get PatternMatcher (for external testing/debugging)
|
|
2859
3470
|
*/
|
|
3471
|
+
/**
|
|
3472
|
+
* Convert a sentence that joins its clauses with a logical connector.
|
|
3473
|
+
*
|
|
3474
|
+
* Returns `null` when `nl` has no top-level connector, in which case the caller
|
|
3475
|
+
* should use its ordinary single-pass conversion. Throws
|
|
3476
|
+
* {@link UnconvertibleClauseError} when a clause cannot be converted, so the
|
|
3477
|
+
* caller can refuse instead of emitting a partial rule.
|
|
3478
|
+
*/
|
|
3479
|
+
decomposeClauses(nl) {
|
|
3480
|
+
if (splitClauses(nl).length <= 1) return null;
|
|
3481
|
+
const confidences = [];
|
|
3482
|
+
const convert = (clause) => {
|
|
3483
|
+
const result = this.patternMatcher.match(clause);
|
|
3484
|
+
if (!result.matched || result.confidence < this.config.patternMinConfidence) {
|
|
3485
|
+
return null;
|
|
3486
|
+
}
|
|
3487
|
+
confidences.push(result.confidence);
|
|
3488
|
+
return result.spel;
|
|
3489
|
+
};
|
|
3490
|
+
const decomposition = decompose(nl, convert);
|
|
3491
|
+
if (decomposition === null) return null;
|
|
3492
|
+
return {
|
|
3493
|
+
expression: decomposition.expression,
|
|
3494
|
+
clauses: decomposition.clauses,
|
|
3495
|
+
confidence: confidences.length > 0 ? Math.min(...confidences) : 0
|
|
3496
|
+
};
|
|
3497
|
+
}
|
|
2860
3498
|
getPatternMatcher() {
|
|
2861
3499
|
return this.patternMatcher;
|
|
2862
3500
|
}
|
|
@@ -2940,6 +3578,19 @@ var NL2SpelEngine = class {
|
|
|
2940
3578
|
if (options.offlineOnly) {
|
|
2941
3579
|
const patternMatcher = this.router.getPatternMatcher();
|
|
2942
3580
|
const patternResult = patternMatcher.match(nl);
|
|
3581
|
+
const isCompound = splitClauses(nl).length > 1;
|
|
3582
|
+
const wholeIsFaithful = patternResult.matched && (patternResult.pattern?.tags.includes("logic") ?? false);
|
|
3583
|
+
if (isCompound && !wholeIsFaithful) {
|
|
3584
|
+
const decomposition = this.router.decomposeClauses(nl);
|
|
3585
|
+
if (decomposition) {
|
|
3586
|
+
return {
|
|
3587
|
+
expression: decomposition.expression,
|
|
3588
|
+
strategy: "pattern",
|
|
3589
|
+
confidence: decomposition.confidence,
|
|
3590
|
+
latencyMs: Date.now() - startTime
|
|
3591
|
+
};
|
|
3592
|
+
}
|
|
3593
|
+
}
|
|
2943
3594
|
if (patternResult.matched) {
|
|
2944
3595
|
return {
|
|
2945
3596
|
expression: patternResult.spel,
|
|
@@ -3017,5 +3668,9 @@ export {
|
|
|
3017
3668
|
SelfCorrectionLoop,
|
|
3018
3669
|
StrategyRouter,
|
|
3019
3670
|
TemplateEngine,
|
|
3020
|
-
|
|
3671
|
+
UnconvertibleClauseError,
|
|
3672
|
+
UnmappedFieldError,
|
|
3673
|
+
ValidationPipeline,
|
|
3674
|
+
decompose,
|
|
3675
|
+
splitClauses
|
|
3021
3676
|
};
|