@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.cjs
CHANGED
|
@@ -34,7 +34,11 @@ __export(index_exports, {
|
|
|
34
34
|
SelfCorrectionLoop: () => SelfCorrectionLoop,
|
|
35
35
|
StrategyRouter: () => StrategyRouter,
|
|
36
36
|
TemplateEngine: () => TemplateEngine,
|
|
37
|
-
|
|
37
|
+
UnconvertibleClauseError: () => UnconvertibleClauseError,
|
|
38
|
+
UnmappedFieldError: () => UnmappedFieldError,
|
|
39
|
+
ValidationPipeline: () => ValidationPipeline,
|
|
40
|
+
decompose: () => decompose,
|
|
41
|
+
splitClauses: () => splitClauses
|
|
38
42
|
});
|
|
39
43
|
module.exports = __toCommonJS(index_exports);
|
|
40
44
|
|
|
@@ -434,10 +438,55 @@ var ChineseNumberParser = class {
|
|
|
434
438
|
};
|
|
435
439
|
|
|
436
440
|
// src/pattern/pattern-matcher.ts
|
|
441
|
+
var UnmappedFieldError = class extends Error {
|
|
442
|
+
/** The field word that could not be resolved. */
|
|
443
|
+
field;
|
|
444
|
+
constructor(field) {
|
|
445
|
+
super(
|
|
446
|
+
`No field mapping for '${field}'. Add a mapping, or use fieldPolicy 'passthrough' to emit it verbatim.`
|
|
447
|
+
);
|
|
448
|
+
this.name = "UnmappedFieldError";
|
|
449
|
+
this.field = field;
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
var CN_FIELD_MAP = {
|
|
453
|
+
\u5907\u6CE8: "remark",
|
|
454
|
+
\u8BF4\u660E: "description",
|
|
455
|
+
\u63CF\u8FF0: "description",
|
|
456
|
+
\u91D1\u989D: "amount",
|
|
457
|
+
\u6570\u91CF: "count",
|
|
458
|
+
\u4E2A\u6570: "count",
|
|
459
|
+
\u72B6\u6001: "status",
|
|
460
|
+
\u7C7B\u578B: "type",
|
|
461
|
+
\u540D\u79F0: "name",
|
|
462
|
+
\u6807\u9898: "title",
|
|
463
|
+
\u5730\u5740: "address",
|
|
464
|
+
\u90AE\u7BB1: "email",
|
|
465
|
+
\u624B\u673A: "phone",
|
|
466
|
+
\u7535\u8BDD: "phone",
|
|
467
|
+
\u65E5\u671F: "date",
|
|
468
|
+
\u65F6\u95F4: "time",
|
|
469
|
+
\u5E74\u9F84: "age",
|
|
470
|
+
\u4EF7\u683C: "price",
|
|
471
|
+
\u7528\u6237\u540D: "name",
|
|
472
|
+
\u6743\u9650: "role",
|
|
473
|
+
\u6807\u7B7E: "tags",
|
|
474
|
+
\u5217\u8868: "list",
|
|
475
|
+
\u6570\u7EC4: "items",
|
|
476
|
+
\u6587\u4EF6: "file",
|
|
477
|
+
\u6587\u4EF6\u540D: "name",
|
|
478
|
+
\u8FC7\u671F: "expiryDate",
|
|
479
|
+
\u521B\u5EFA: "createdAt",
|
|
480
|
+
\u6709\u6548: "valid",
|
|
481
|
+
\u6D3B\u8DC3: "active",
|
|
482
|
+
\u6FC0\u6D3B: "active"
|
|
483
|
+
};
|
|
437
484
|
var PatternMatcher = class {
|
|
438
485
|
_patterns;
|
|
439
|
-
|
|
486
|
+
fieldPolicy;
|
|
487
|
+
constructor(patterns = [], options = {}) {
|
|
440
488
|
this._patterns = [...patterns];
|
|
489
|
+
this.fieldPolicy = options.fieldPolicy ?? "passthrough";
|
|
441
490
|
this.sortByPriority();
|
|
442
491
|
}
|
|
443
492
|
get patternCount() {
|
|
@@ -471,9 +520,22 @@ var PatternMatcher = class {
|
|
|
471
520
|
}
|
|
472
521
|
}
|
|
473
522
|
}
|
|
474
|
-
const
|
|
523
|
+
const { expression, unmappedFields } = this.fillTemplate(
|
|
524
|
+
pattern,
|
|
525
|
+
slots,
|
|
526
|
+
normalized,
|
|
527
|
+
matchResult
|
|
528
|
+
);
|
|
475
529
|
const latencyMs2 = Date.now() - startTime;
|
|
476
|
-
return {
|
|
530
|
+
return {
|
|
531
|
+
matched: true,
|
|
532
|
+
pattern,
|
|
533
|
+
spel: expression,
|
|
534
|
+
confidence: pattern.confidence,
|
|
535
|
+
latencyMs: latencyMs2,
|
|
536
|
+
slots,
|
|
537
|
+
...unmappedFields.length > 0 ? { unmappedFields } : {}
|
|
538
|
+
};
|
|
477
539
|
}
|
|
478
540
|
const latencyMs = Date.now() - startTime;
|
|
479
541
|
return { matched: false, confidence: 0, latencyMs };
|
|
@@ -494,13 +556,20 @@ var PatternMatcher = class {
|
|
|
494
556
|
if (value !== void 0) slots[key] = value;
|
|
495
557
|
}
|
|
496
558
|
}
|
|
559
|
+
const { expression, unmappedFields } = this.fillTemplate(
|
|
560
|
+
pattern,
|
|
561
|
+
slots,
|
|
562
|
+
normalized,
|
|
563
|
+
matchResult
|
|
564
|
+
);
|
|
497
565
|
results.push({
|
|
498
566
|
matched: true,
|
|
499
567
|
pattern,
|
|
500
|
-
spel:
|
|
568
|
+
spel: expression,
|
|
501
569
|
confidence: pattern.confidence,
|
|
502
570
|
latencyMs: 0,
|
|
503
|
-
slots
|
|
571
|
+
slots,
|
|
572
|
+
...unmappedFields.length > 0 ? { unmappedFields } : {}
|
|
504
573
|
});
|
|
505
574
|
}
|
|
506
575
|
return results;
|
|
@@ -522,46 +591,37 @@ var PatternMatcher = class {
|
|
|
522
591
|
if (/^(?:商品|product|item)/i.test(input)) return "product";
|
|
523
592
|
return "order";
|
|
524
593
|
}
|
|
594
|
+
/**
|
|
595
|
+
* The first whitespace- or punctuation-delimited chunk of `input`.
|
|
596
|
+
*/
|
|
597
|
+
firstWord(input) {
|
|
598
|
+
const m = input.match(/^[^\s,,、]+/);
|
|
599
|
+
return m ? m[0] : "value";
|
|
600
|
+
}
|
|
525
601
|
/**
|
|
526
602
|
* Extract Chinese field names from input and map to SpEL fields
|
|
527
603
|
*/
|
|
528
604
|
extractChineseField(input) {
|
|
529
|
-
const
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
\u5E74\u9F84: "age",
|
|
550
|
-
\u4EF7\u683C: "price",
|
|
551
|
-
\u7528\u6237\u540D: "name",
|
|
552
|
-
\u6743\u9650: "role",
|
|
553
|
-
\u6807\u7B7E: "tags",
|
|
554
|
-
\u5217\u8868: "list",
|
|
555
|
-
\u6570\u7EC4: "items",
|
|
556
|
-
\u6587\u4EF6: "file",
|
|
557
|
-
\u6587\u4EF6\u540D: "name",
|
|
558
|
-
\u8FC7\u671F: "expiryDate",
|
|
559
|
-
\u521B\u5EFA: "createdAt",
|
|
560
|
-
\u6709\u6548: "valid",
|
|
561
|
-
\u6D3B\u8DC3: "active",
|
|
562
|
-
\u6FC0\u6D3B: "active"
|
|
563
|
-
};
|
|
564
|
-
return cnMap[first] ?? first;
|
|
605
|
+
const first = this.firstWord(input);
|
|
606
|
+
return CN_FIELD_MAP[first] ?? first;
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Resolve a captured field word into the identifier to emit, and report whether
|
|
610
|
+
* the dictionary recognised it.
|
|
611
|
+
*
|
|
612
|
+
* An ASCII word is already an identifier and is emitted unchanged. Anything else
|
|
613
|
+
* is looked up: a word the dictionary knows becomes its English identifier, and a
|
|
614
|
+
* word it does not know is emitted verbatim and reported as unmapped. Emitting it
|
|
615
|
+
* verbatim is legal Spring, but it is a guess about the caller's schema, so the
|
|
616
|
+
* guess is never silent.
|
|
617
|
+
*/
|
|
618
|
+
resolveField(captured) {
|
|
619
|
+
if (/^[a-zA-Z_]\w*$/.test(captured)) {
|
|
620
|
+
return { field: captured, mapped: true };
|
|
621
|
+
}
|
|
622
|
+
const first = this.firstWord(captured);
|
|
623
|
+
const mapped = CN_FIELD_MAP[first];
|
|
624
|
+
return { field: mapped ?? first, mapped: mapped !== void 0 };
|
|
565
625
|
}
|
|
566
626
|
/**
|
|
567
627
|
* Template filling and value transformation
|
|
@@ -569,7 +629,20 @@ var PatternMatcher = class {
|
|
|
569
629
|
fillTemplate(pattern, slots, originalInput, _matchResult) {
|
|
570
630
|
let result = pattern.spelTemplate;
|
|
571
631
|
const hasFieldSlot = "field" in slots;
|
|
572
|
-
const
|
|
632
|
+
const unmappedFields = [];
|
|
633
|
+
let field;
|
|
634
|
+
if (hasFieldSlot) {
|
|
635
|
+
const resolved = this.resolveField(slots["field"]);
|
|
636
|
+
if (!resolved.mapped) {
|
|
637
|
+
if (this.fieldPolicy === "strict") {
|
|
638
|
+
throw new UnmappedFieldError(resolved.field);
|
|
639
|
+
}
|
|
640
|
+
unmappedFields.push(resolved.field);
|
|
641
|
+
}
|
|
642
|
+
field = resolved.field;
|
|
643
|
+
} else {
|
|
644
|
+
field = this.extractChineseField(originalInput);
|
|
645
|
+
}
|
|
573
646
|
const root = this.inferRoot(originalInput);
|
|
574
647
|
result = result.replace(/\{field\}/g, field);
|
|
575
648
|
result = result.replace(/\{root\}/g, root);
|
|
@@ -597,14 +670,7 @@ var PatternMatcher = class {
|
|
|
597
670
|
result = result.replace(`{${key}}`, transformedValue ?? "");
|
|
598
671
|
}
|
|
599
672
|
result = result.replace(/\{[a-zA-Z_]+\}/g, "");
|
|
600
|
-
return result.trim();
|
|
601
|
-
}
|
|
602
|
-
/**
|
|
603
|
-
* Infer SpEL field name from capture group
|
|
604
|
-
*/
|
|
605
|
-
inferFieldFromCapture(captured) {
|
|
606
|
-
if (/^[a-zA-Z_]\w*$/.test(captured)) return captured;
|
|
607
|
-
return this.extractChineseField(captured);
|
|
673
|
+
return { expression: result.trim(), unmappedFields };
|
|
608
674
|
}
|
|
609
675
|
};
|
|
610
676
|
|
|
@@ -670,7 +736,7 @@ var BUILTIN_PATTERNS = [
|
|
|
670
736
|
slots: {},
|
|
671
737
|
priority: 98,
|
|
672
738
|
tags: ["null", "isNotNull"],
|
|
673
|
-
examples: [{ nl: "\u5907\u6CE8\u4E0D\u4E3A\u7A7A", spel: "
|
|
739
|
+
examples: [{ nl: "\u5907\u6CE8\u4E0D\u4E3A\u7A7A", spel: "#remark != null" }],
|
|
674
740
|
difficulty: "easy",
|
|
675
741
|
confidence: 0.98
|
|
676
742
|
},
|
|
@@ -681,7 +747,7 @@ var BUILTIN_PATTERNS = [
|
|
|
681
747
|
slots: {},
|
|
682
748
|
priority: 97,
|
|
683
749
|
tags: ["null", "isNull"],
|
|
684
|
-
examples: [{ nl: "\u5907\u6CE8\u4E3A\u7A7A", spel: "
|
|
750
|
+
examples: [{ nl: "\u5907\u6CE8\u4E3A\u7A7A", spel: "#remark == null" }],
|
|
685
751
|
difficulty: "easy",
|
|
686
752
|
confidence: 0.98
|
|
687
753
|
},
|
|
@@ -712,29 +778,29 @@ var BUILTIN_PATTERNS = [
|
|
|
712
778
|
// ================================================================
|
|
713
779
|
{
|
|
714
780
|
id: "CN-CMP-GE",
|
|
715
|
-
match: /^(?<field>[^\s,,、]+?)\s*(
|
|
781
|
+
match: /^(?<field>[^\s,,、]+?)\s*(?:不小于|不低于|大于等于|>=)\s*(?<value>\d+(?:\.\d+)?)/,
|
|
716
782
|
spelTemplate: "#{field} >= {value}",
|
|
717
783
|
slots: { value: { key: "value", type: "number", transform: "toNumber" } },
|
|
718
784
|
priority: 93,
|
|
719
785
|
tags: ["comparison", "ge", "chinese"],
|
|
720
|
-
examples: [{ nl: "\u91D1\u989D\u4E0D\u5C0F\u4E8E100", spel: "
|
|
786
|
+
examples: [{ nl: "\u91D1\u989D\u4E0D\u5C0F\u4E8E100", spel: "#amount >= 100" }],
|
|
721
787
|
difficulty: "easy",
|
|
722
788
|
confidence: 0.95
|
|
723
789
|
},
|
|
724
790
|
{
|
|
725
791
|
id: "CN-CMP-LE",
|
|
726
|
-
match: /^(?<field>[^\s,,、]+?)\s*(
|
|
792
|
+
match: /^(?<field>[^\s,,、]+?)\s*(?:不大于|不超过|小于等于|<=)\s*(?<value>\d+(?:\.\d+)?)/,
|
|
727
793
|
spelTemplate: "#{field} <= {value}",
|
|
728
794
|
slots: { value: { key: "value", type: "number", transform: "toNumber" } },
|
|
729
795
|
priority: 93,
|
|
730
796
|
tags: ["comparison", "le", "chinese"],
|
|
731
|
-
examples: [{ nl: "\u91D1\u989D\u4E0D\u8D85\u8FC7500", spel: "
|
|
797
|
+
examples: [{ nl: "\u91D1\u989D\u4E0D\u8D85\u8FC7500", spel: "#amount <= 500" }],
|
|
732
798
|
difficulty: "easy",
|
|
733
799
|
confidence: 0.95
|
|
734
800
|
},
|
|
735
801
|
{
|
|
736
802
|
id: "CN-CMP-GT",
|
|
737
|
-
match: /^(?<field>[^\s,,、]+?)\s*(
|
|
803
|
+
match: /^(?<field>[^\s,,、]+?)\s*(?:大于|超过|高于)\s*(?<value>\d+(?:\.\d+)?)/,
|
|
738
804
|
spelTemplate: "#{field} > {value}",
|
|
739
805
|
slots: { value: { key: "value", type: "number", transform: "toNumber" } },
|
|
740
806
|
priority: 92,
|
|
@@ -745,7 +811,7 @@ var BUILTIN_PATTERNS = [
|
|
|
745
811
|
},
|
|
746
812
|
{
|
|
747
813
|
id: "CN-CMP-LT",
|
|
748
|
-
match: /^(?<field>[^\s,,、]+?)\s*(
|
|
814
|
+
match: /^(?<field>[^\s,,、]+?)\s*(?:小于|低于|不到)\s*(?<value>\d+(?:\.\d+)?)/,
|
|
749
815
|
spelTemplate: "#{field} < {value}",
|
|
750
816
|
slots: { value: { key: "value", type: "number", transform: "toNumber" } },
|
|
751
817
|
priority: 92,
|
|
@@ -759,7 +825,7 @@ var BUILTIN_PATTERNS = [
|
|
|
759
825
|
// ================================================================
|
|
760
826
|
{
|
|
761
827
|
id: "CN-EQ-STATUS",
|
|
762
|
-
match: /^(?<field>[^\s,,、]+?)\s*(
|
|
828
|
+
match: /^(?<field>[^\s,,、]+?)\s*(?:等于|是|为)\s*(?<value>[^\s,,、]+)$/,
|
|
763
829
|
spelTemplate: "#{field} == '{value}'",
|
|
764
830
|
slots: { value: { key: "value", type: "string" } },
|
|
765
831
|
priority: 85,
|
|
@@ -782,7 +848,7 @@ var BUILTIN_PATTERNS = [
|
|
|
782
848
|
// CN: "order status is not cancelled" — must be before NOT pattern
|
|
783
849
|
{
|
|
784
850
|
id: "CN-NE-STATUS",
|
|
785
|
-
match: /^(?<field>[^\s,,、]+?)\s*(
|
|
851
|
+
match: /^(?<field>[^\s,,、]+?)\s*(?:不等于|!=|不是)\s*(?<value>[^\s,,、]+)$/,
|
|
786
852
|
spelTemplate: "#{field} != '{value}'",
|
|
787
853
|
slots: { value: { key: "value", type: "string" } },
|
|
788
854
|
priority: 91,
|
|
@@ -805,12 +871,12 @@ var BUILTIN_PATTERNS = [
|
|
|
805
871
|
// CN/EN: count equality
|
|
806
872
|
{
|
|
807
873
|
id: "CN-EQ-COUNT",
|
|
808
|
-
match: /^(?<field>[^\s,,、]+?)\s*(
|
|
874
|
+
match: /^(?<field>[^\s,,、]+?)\s*(?:等于|==)\s*(?<value>\d+)/,
|
|
809
875
|
spelTemplate: "#{field} == {value}",
|
|
810
876
|
slots: { value: { key: "value", type: "number", transform: "toNumber" } },
|
|
811
877
|
priority: 91,
|
|
812
878
|
tags: ["comparison", "eq", "number"],
|
|
813
|
-
examples: [{ nl: "\u6570\u91CF\u7B49\u4E8E5", spel: "
|
|
879
|
+
examples: [{ nl: "\u6570\u91CF\u7B49\u4E8E5", spel: "#count == 5" }],
|
|
814
880
|
difficulty: "easy",
|
|
815
881
|
confidence: 0.95
|
|
816
882
|
},
|
|
@@ -852,7 +918,9 @@ var BUILTIN_PATTERNS = [
|
|
|
852
918
|
},
|
|
853
919
|
{
|
|
854
920
|
id: "CN-PERM-PERM",
|
|
855
|
-
|
|
921
|
+
// Chinese-only operator: keeping the English "can"/"may" here would let the
|
|
922
|
+
// pattern fire on the "can" inside "not cancelled".
|
|
923
|
+
match: /^(?<field>[^\s,,、]+)\s*(?:可以|能够|允许)\s*(?<permission>.+)$/,
|
|
856
924
|
spelTemplate: "hasPermission('{permission}')",
|
|
857
925
|
slots: { permission: { key: "permission", type: "string" } },
|
|
858
926
|
priority: 88,
|
|
@@ -877,7 +945,7 @@ var BUILTIN_PATTERNS = [
|
|
|
877
945
|
// ================================================================
|
|
878
946
|
{
|
|
879
947
|
id: "CN-COLL-EMPTY",
|
|
880
|
-
match: /^(?<field>[^\s,,、]+?)\s*(
|
|
948
|
+
match: /^(?<field>[^\s,,、]+?)\s*(?:为空|是空的|没有|无)\s*(?:元素|数据|项)?$/,
|
|
881
949
|
spelTemplate: "#{field}.isEmpty()",
|
|
882
950
|
slots: {},
|
|
883
951
|
priority: 89,
|
|
@@ -899,7 +967,7 @@ var BUILTIN_PATTERNS = [
|
|
|
899
967
|
},
|
|
900
968
|
{
|
|
901
969
|
id: "CN-COLL-NOTEMPTY",
|
|
902
|
-
match: /^(?<field>[^\s,,、]+?)\s*(
|
|
970
|
+
match: /^(?<field>[^\s,,、]+?)\s*(?:不为空|有)\s*(?:元素|数据|项)?$/,
|
|
903
971
|
spelTemplate: "!#{field}.isEmpty()",
|
|
904
972
|
slots: {},
|
|
905
973
|
priority: 89,
|
|
@@ -921,7 +989,7 @@ var BUILTIN_PATTERNS = [
|
|
|
921
989
|
},
|
|
922
990
|
{
|
|
923
991
|
id: "CN-COLL-CONTAINS",
|
|
924
|
-
match: /^(?<field>[^\s,,、]+?)\s*(
|
|
992
|
+
match: /^(?<field>[^\s,,、]+?)\s*(?:中\s*)?(?:包含|含有|有)\s*(?<element>[^\s,,、]+)/,
|
|
925
993
|
spelTemplate: "#{field}.contains('{element}')",
|
|
926
994
|
slots: { element: { key: "element", type: "string" } },
|
|
927
995
|
priority: 87,
|
|
@@ -943,7 +1011,7 @@ var BUILTIN_PATTERNS = [
|
|
|
943
1011
|
},
|
|
944
1012
|
{
|
|
945
1013
|
id: "CN-COLL-SIZE",
|
|
946
|
-
match: /^(?<field>[^\s,,、]+?)\s*(
|
|
1014
|
+
match: /^(?<field>[^\s,,、]+?)\s*(?:数量|个数|大小|长度)\s*(?<op>大于|>|超过|小于|<|等于|==)\s*(?<value>\d+)/,
|
|
947
1015
|
spelTemplate: "#{field}.size() {op} {value}",
|
|
948
1016
|
slots: {
|
|
949
1017
|
value: { key: "value", type: "number", transform: "toNumber" },
|
|
@@ -974,7 +1042,7 @@ var BUILTIN_PATTERNS = [
|
|
|
974
1042
|
// ================================================================
|
|
975
1043
|
{
|
|
976
1044
|
id: "CN-STR-CONTAINS",
|
|
977
|
-
match: /^(?<field>[^\s,,、]+?)
|
|
1045
|
+
match: /^(?<field>[^\s,,、]+?)\s*(?:包含|含有|包括)\s*(?<substr>[^\s,,、]+)/,
|
|
978
1046
|
spelTemplate: "#{field}.contains('{substr}')",
|
|
979
1047
|
slots: { substr: { key: "substr", type: "string" } },
|
|
980
1048
|
priority: 85,
|
|
@@ -1023,7 +1091,7 @@ var BUILTIN_PATTERNS = [
|
|
|
1023
1091
|
slots: { suffix: { key: "suffix", type: "string" } },
|
|
1024
1092
|
priority: 85,
|
|
1025
1093
|
tags: ["string", "endsWith"],
|
|
1026
|
-
examples: [{ nl: "\u6587\u4EF6\u540D\u4EE5.pdf\u7ED3\u5C3E", spel: "
|
|
1094
|
+
examples: [{ nl: "\u6587\u4EF6\u540D\u4EE5.pdf\u7ED3\u5C3E", spel: "#name.endsWith('.pdf')" }],
|
|
1027
1095
|
difficulty: "easy",
|
|
1028
1096
|
confidence: 0.95
|
|
1029
1097
|
},
|
|
@@ -1066,21 +1134,21 @@ var BUILTIN_PATTERNS = [
|
|
|
1066
1134
|
{
|
|
1067
1135
|
id: "CN-RANGE-BETWEEN",
|
|
1068
1136
|
match: /^(?<field>[^\s,,、]+?)\s*(?:在|介于)\s*(?<min>\d+)\s*(?:和|到|~)\s*(?<max>\d+)\s*(?:之间|范围)?/,
|
|
1069
|
-
spelTemplate: "#{field} between {{
|
|
1137
|
+
spelTemplate: "#{field} between {{min}, {max}}",
|
|
1070
1138
|
slots: {
|
|
1071
1139
|
min: { key: "min", type: "number", transform: "toNumber" },
|
|
1072
1140
|
max: { key: "max", type: "number", transform: "toNumber" }
|
|
1073
1141
|
},
|
|
1074
1142
|
priority: 82,
|
|
1075
1143
|
tags: ["range", "between"],
|
|
1076
|
-
examples: [{ nl: "\u5E74\u9F84\u572818\u523060\u4E4B\u95F4", spel: "
|
|
1144
|
+
examples: [{ nl: "\u5E74\u9F84\u572818\u523060\u4E4B\u95F4", spel: "#age between {18, 60}" }],
|
|
1077
1145
|
difficulty: "easy",
|
|
1078
1146
|
confidence: 0.95
|
|
1079
1147
|
},
|
|
1080
1148
|
{
|
|
1081
1149
|
id: "EN-RANGE-BETWEEN",
|
|
1082
1150
|
match: /\b(?<field>\w+)\s+between\s+(?<min>\d+)\s+and\s+(?<max>\d+)/i,
|
|
1083
|
-
spelTemplate: "#{field} between {{
|
|
1151
|
+
spelTemplate: "#{field} between {{min}, {max}}",
|
|
1084
1152
|
slots: {
|
|
1085
1153
|
min: { key: "min", type: "number", transform: "toNumber" },
|
|
1086
1154
|
max: { key: "max", type: "number", transform: "toNumber" }
|
|
@@ -1101,7 +1169,7 @@ var BUILTIN_PATTERNS = [
|
|
|
1101
1169
|
slots: { default: { key: "default", type: "string" } },
|
|
1102
1170
|
priority: 78,
|
|
1103
1171
|
tags: ["elvis"],
|
|
1104
|
-
examples: [{ nl: "\u7528\u6237\u540D\u6216\u8005\u533F\u540D\u7528\u6237", spel: "
|
|
1172
|
+
examples: [{ nl: "\u7528\u6237\u540D\u6216\u8005\u533F\u540D\u7528\u6237", spel: "#name ?: '\u533F\u540D\u7528\u6237'" }],
|
|
1105
1173
|
difficulty: "medium",
|
|
1106
1174
|
confidence: 0.85
|
|
1107
1175
|
},
|
|
@@ -1207,7 +1275,11 @@ var BUILTIN_PATTERNS = [
|
|
|
1207
1275
|
// ================================================================
|
|
1208
1276
|
{
|
|
1209
1277
|
id: "CN-BOOL-TRUE",
|
|
1210
|
-
|
|
1278
|
+
// "用户是VIP" asserts the subject through a Latin marker, so accept either a
|
|
1279
|
+
// truth word or a Latin word. A fully open marker would fire on function
|
|
1280
|
+
// words ("一个非常…"), and the lookbehind keeps a negator out of the field
|
|
1281
|
+
// ("用户不" is not the subject).
|
|
1282
|
+
match: /^(?<field>[^\s,,、]+?)(?<![不非])\s*(?:是|为|等于|==)\s*(?:true|真|是|yes|[A-Za-z]\w*)$/,
|
|
1211
1283
|
spelTemplate: "#{field} == true",
|
|
1212
1284
|
slots: {},
|
|
1213
1285
|
priority: 74,
|
|
@@ -1218,7 +1290,10 @@ var BUILTIN_PATTERNS = [
|
|
|
1218
1290
|
},
|
|
1219
1291
|
{
|
|
1220
1292
|
id: "CN-BOOL-FALSE",
|
|
1221
|
-
|
|
1293
|
+
// Mirror of CN-BOOL-TRUE. The marker stays narrow so that function words
|
|
1294
|
+
// ("非常…") do not match, and the lookbehind keeps "不是有效" with
|
|
1295
|
+
// CN-LOGIC-NOT instead of capturing "不" as the field.
|
|
1296
|
+
match: /^(?<field>[^\s,,、]+?)(?<![不非])\s*(?:不是|非|为|是|等于|==)\s*(?:false|假|否|no|[A-Za-z]\w*)$/,
|
|
1222
1297
|
spelTemplate: "#{field} == false",
|
|
1223
1298
|
slots: {},
|
|
1224
1299
|
priority: 73,
|
|
@@ -1326,7 +1401,7 @@ var BUILTIN_PATTERNS = [
|
|
|
1326
1401
|
// ================================================================
|
|
1327
1402
|
{
|
|
1328
1403
|
id: "CN-LOGIC-NOT",
|
|
1329
|
-
match: /^不是\s
|
|
1404
|
+
match: /^不是\s*(?<expr>.+)/,
|
|
1330
1405
|
spelTemplate: "!({expr})",
|
|
1331
1406
|
slots: { expr: { key: "expr", type: "variable" } },
|
|
1332
1407
|
priority: 62,
|
|
@@ -1351,8 +1426,8 @@ var BUILTIN_PATTERNS = [
|
|
|
1351
1426
|
// ================================================================
|
|
1352
1427
|
{
|
|
1353
1428
|
id: "CN-SELECT-FIRST",
|
|
1354
|
-
match: /^(?<root>[^\s,,、]+?)\s*中\s*第一[个位]\s*(?<field>[^\s,,、]+?)\s*(?:大于|>|超过|<|小于|等于|==)\s*(?<value>[^\s
|
|
1355
|
-
spelTemplate: "#{root}.items.^[#
|
|
1429
|
+
match: /^(?<root>[^\s,,、]+?)\s*中\s*第一[个位]\s*(?<field>[^\s,,、]+?)\s*(?:大于|>|超过|<|小于|等于|==)\s*(?<value>[^\s,,、的]+)/,
|
|
1430
|
+
spelTemplate: "#{root}.items.^[#this.{field} > {value}]",
|
|
1356
1431
|
slots: {
|
|
1357
1432
|
root: { key: "root", type: "variable" },
|
|
1358
1433
|
field: { key: "field", type: "variable" },
|
|
@@ -1360,14 +1435,14 @@ var BUILTIN_PATTERNS = [
|
|
|
1360
1435
|
},
|
|
1361
1436
|
priority: 60,
|
|
1362
1437
|
tags: ["selection", "first"],
|
|
1363
|
-
examples: [{ nl: "\u8BA2\u5355\u4E2D\u7B2C\u4E00\u4E2A\u91D1\u989D\u5927\u4E8E1000\u7684", spel: "
|
|
1438
|
+
examples: [{ nl: "\u8BA2\u5355\u4E2D\u7B2C\u4E00\u4E2A\u91D1\u989D\u5927\u4E8E1000\u7684", spel: "#order.items.^[#this.amount > 1000]" }],
|
|
1364
1439
|
difficulty: "medium",
|
|
1365
1440
|
confidence: 0.8
|
|
1366
1441
|
},
|
|
1367
1442
|
{
|
|
1368
1443
|
id: "CN-SELECT-ALL",
|
|
1369
|
-
match: /^(?<root>[^\s,,、]+?)\s*中\s*所有\s*(?<field>[^\s,,、]+?)\s*(?:大于|>|超过|<|小于|等于|==|包含|满足)\s*(?<value>[^\s
|
|
1370
|
-
spelTemplate: "#{root}.items.?[#
|
|
1444
|
+
match: /^(?<root>[^\s,,、]+?)\s*中\s*所有\s*(?<field>[^\s,,、]+?)\s*(?:大于|>|超过|<|小于|等于|==|包含|满足)\s*(?<value>[^\s,,、的]+)/,
|
|
1445
|
+
spelTemplate: "#{root}.items.?[#this.{field} > {value}]",
|
|
1371
1446
|
slots: {
|
|
1372
1447
|
root: { key: "root", type: "variable" },
|
|
1373
1448
|
field: { key: "field", type: "variable" },
|
|
@@ -1375,28 +1450,28 @@ var BUILTIN_PATTERNS = [
|
|
|
1375
1450
|
},
|
|
1376
1451
|
priority: 60,
|
|
1377
1452
|
tags: ["selection", "all"],
|
|
1378
|
-
examples: [{ nl: "\u8BA2\u5355\u4E2D\u6240\u6709\u91D1\u989D\u5927\u4E8E1000\u7684", spel: "
|
|
1453
|
+
examples: [{ nl: "\u8BA2\u5355\u4E2D\u6240\u6709\u91D1\u989D\u5927\u4E8E1000\u7684", spel: "#order.items.?[#this.amount > 1000]" }],
|
|
1379
1454
|
difficulty: "medium",
|
|
1380
1455
|
confidence: 0.8
|
|
1381
1456
|
},
|
|
1382
1457
|
{
|
|
1383
1458
|
id: "CN-PROJ",
|
|
1384
|
-
match: /^(?<root>[^\s,,、]+?)\s*中\s*每[个一]\s*(?:的)?(?<field>[^\s,,、]+?)\s*(?:值|金额|名称|价格|name|amount|price)
|
|
1385
|
-
spelTemplate: "#{root}.items.![#
|
|
1459
|
+
match: /^(?<root>[^\s,,、]+?)\s*中\s*每[个一]\s*(?:的)?(?<field>[^\s,,、]+?)\s*(?:的)?(?:值|金额|名称|价格|name|amount|price)?$/,
|
|
1460
|
+
spelTemplate: "#{root}.items.![#this.{field}]",
|
|
1386
1461
|
slots: {
|
|
1387
1462
|
root: { key: "root", type: "variable" },
|
|
1388
1463
|
field: { key: "field", type: "variable" }
|
|
1389
1464
|
},
|
|
1390
1465
|
priority: 55,
|
|
1391
1466
|
tags: ["projection"],
|
|
1392
|
-
examples: [{ nl: "\u8BA2\u5355\u4E2D\u6BCF\u4E2A\u5546\u54C1\u7684\u4EF7\u683C", spel: "
|
|
1467
|
+
examples: [{ nl: "\u8BA2\u5355\u4E2D\u6BCF\u4E2A\u5546\u54C1\u7684\u4EF7\u683C", spel: "#order.items.![#this.\u5546\u54C1]" }],
|
|
1393
1468
|
difficulty: "medium",
|
|
1394
1469
|
confidence: 0.75
|
|
1395
1470
|
},
|
|
1396
1471
|
{
|
|
1397
1472
|
id: "EN-SELECT-ALL",
|
|
1398
1473
|
match: /all\s+(?<root>\w+)\s+with\s+(?<field>\w+)\s*>\s*(?<value>\d+)/i,
|
|
1399
|
-
spelTemplate: "#{root}.items.?[#
|
|
1474
|
+
spelTemplate: "#{root}.items.?[#this.{field} > {value}]",
|
|
1400
1475
|
slots: {
|
|
1401
1476
|
root: { key: "root", type: "variable" },
|
|
1402
1477
|
field: { key: "field", type: "variable" },
|
|
@@ -1404,13 +1479,149 @@ var BUILTIN_PATTERNS = [
|
|
|
1404
1479
|
},
|
|
1405
1480
|
priority: 50,
|
|
1406
1481
|
tags: ["selection", "all", "english"],
|
|
1407
|
-
examples: [{ nl: "all items with price > 100", spel: "#
|
|
1482
|
+
examples: [{ nl: "all items with price > 100", spel: "#order.items.?[#this.price > 100]" }],
|
|
1408
1483
|
difficulty: "medium",
|
|
1409
1484
|
confidence: 0.8
|
|
1410
1485
|
}
|
|
1411
1486
|
];
|
|
1412
1487
|
BUILTIN_PATTERNS.sort((a, b) => b.priority - a.priority);
|
|
1413
1488
|
|
|
1489
|
+
// src/pattern/clause-splitter.ts
|
|
1490
|
+
var CONNECTORS = [
|
|
1491
|
+
// Chinese conjunctions. `和` is deliberately absent: it is a range separator in
|
|
1492
|
+
// `价格在10和20之间`, not a conjunction.
|
|
1493
|
+
{ pattern: /(?:并且|且|同时|而且|、)/y, operator: "and" },
|
|
1494
|
+
{ pattern: /(?:或者|或|要么)/y, operator: "or" },
|
|
1495
|
+
// English conjunctions, matched as whole words.
|
|
1496
|
+
{ pattern: /\band\b/iy, operator: "and" },
|
|
1497
|
+
{ pattern: /\bor\b/iy, operator: "or" }
|
|
1498
|
+
];
|
|
1499
|
+
var UnconvertibleClauseError = class extends Error {
|
|
1500
|
+
/** The clause texts that could not be converted. */
|
|
1501
|
+
unconvertible;
|
|
1502
|
+
/** The full input that was being decomposed. */
|
|
1503
|
+
input;
|
|
1504
|
+
constructor(input, unconvertible) {
|
|
1505
|
+
const detail = unconvertible.map((clause) => `'${clause}'`).join(", ");
|
|
1506
|
+
super(
|
|
1507
|
+
`Cannot decompose '${input}': no conversion for ${detail}. Refusing to emit a partial rule.`
|
|
1508
|
+
);
|
|
1509
|
+
this.name = "UnconvertibleClauseError";
|
|
1510
|
+
this.input = input;
|
|
1511
|
+
this.unconvertible = unconvertible;
|
|
1512
|
+
}
|
|
1513
|
+
};
|
|
1514
|
+
var QUOTES = /* @__PURE__ */ new Set(["'", '"']);
|
|
1515
|
+
var OPENERS = { "(": ")", "[": "]", "{": "}" };
|
|
1516
|
+
var CLOSERS = /* @__PURE__ */ new Set([")", "]", "}"]);
|
|
1517
|
+
var isDigit = (ch) => ch !== void 0 && ch >= "0" && ch <= "9";
|
|
1518
|
+
function nextConnector(input, from) {
|
|
1519
|
+
let depth = 0;
|
|
1520
|
+
let quote = null;
|
|
1521
|
+
for (let i = from; i < input.length; i += 1) {
|
|
1522
|
+
const ch = input[i];
|
|
1523
|
+
if (quote !== null) {
|
|
1524
|
+
if (ch === quote) {
|
|
1525
|
+
if (input[i + 1] === quote) {
|
|
1526
|
+
i += 1;
|
|
1527
|
+
continue;
|
|
1528
|
+
}
|
|
1529
|
+
quote = null;
|
|
1530
|
+
}
|
|
1531
|
+
continue;
|
|
1532
|
+
}
|
|
1533
|
+
if (QUOTES.has(ch)) {
|
|
1534
|
+
quote = ch;
|
|
1535
|
+
continue;
|
|
1536
|
+
}
|
|
1537
|
+
if (ch in OPENERS) {
|
|
1538
|
+
depth += 1;
|
|
1539
|
+
continue;
|
|
1540
|
+
}
|
|
1541
|
+
if (CLOSERS.has(ch)) {
|
|
1542
|
+
depth = Math.max(0, depth - 1);
|
|
1543
|
+
continue;
|
|
1544
|
+
}
|
|
1545
|
+
if (depth > 0) continue;
|
|
1546
|
+
for (const { pattern, operator } of CONNECTORS) {
|
|
1547
|
+
pattern.lastIndex = i;
|
|
1548
|
+
const match = pattern.exec(input);
|
|
1549
|
+
if (!match) continue;
|
|
1550
|
+
if (operator === "and") {
|
|
1551
|
+
let before = i - 1;
|
|
1552
|
+
while (before >= 0 && input[before] === " ") before -= 1;
|
|
1553
|
+
let after = i + match[0].length;
|
|
1554
|
+
while (after < input.length && input[after] === " ") after += 1;
|
|
1555
|
+
if (isDigit(input[before]) && isDigit(input[after])) continue;
|
|
1556
|
+
}
|
|
1557
|
+
return { index: i, length: match[0].length, operator };
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
return null;
|
|
1561
|
+
}
|
|
1562
|
+
function splitClauses(input) {
|
|
1563
|
+
const clauses = [];
|
|
1564
|
+
let start = 0;
|
|
1565
|
+
let connector = "and";
|
|
1566
|
+
let cursor = 0;
|
|
1567
|
+
for (; ; ) {
|
|
1568
|
+
const found = nextConnector(input, cursor);
|
|
1569
|
+
if (found === null) break;
|
|
1570
|
+
clauses.push({ text: input.slice(start, found.index).trim(), connector });
|
|
1571
|
+
connector = found.operator;
|
|
1572
|
+
start = found.index + found.length;
|
|
1573
|
+
cursor = start;
|
|
1574
|
+
}
|
|
1575
|
+
const tail = input.slice(start).trim();
|
|
1576
|
+
if (clauses.length > 0 || tail.length > 0) {
|
|
1577
|
+
clauses.push({ text: tail, connector });
|
|
1578
|
+
}
|
|
1579
|
+
if (clauses.length === 0) {
|
|
1580
|
+
clauses.push({ text: "", connector: "and" });
|
|
1581
|
+
}
|
|
1582
|
+
return clauses;
|
|
1583
|
+
}
|
|
1584
|
+
function groupByPrecedence(clauses) {
|
|
1585
|
+
const groups = [];
|
|
1586
|
+
let current = [];
|
|
1587
|
+
for (const clause of clauses) {
|
|
1588
|
+
if (clause.connector === "or" && current.length > 0) {
|
|
1589
|
+
groups.push(current);
|
|
1590
|
+
current = [];
|
|
1591
|
+
}
|
|
1592
|
+
current.push(clause);
|
|
1593
|
+
}
|
|
1594
|
+
if (current.length > 0) groups.push(current);
|
|
1595
|
+
return groups;
|
|
1596
|
+
}
|
|
1597
|
+
function decompose(input, convert) {
|
|
1598
|
+
const clauses = splitClauses(input);
|
|
1599
|
+
if (clauses.length <= 1) return null;
|
|
1600
|
+
const unconvertible = [];
|
|
1601
|
+
const resolved = clauses.map((clause) => {
|
|
1602
|
+
const expression = clause.text.length === 0 ? null : convert(clause.text);
|
|
1603
|
+
if (expression === null) {
|
|
1604
|
+
unconvertible.push(clause.text);
|
|
1605
|
+
return { ...clause, expression: "" };
|
|
1606
|
+
}
|
|
1607
|
+
return { ...clause, expression };
|
|
1608
|
+
});
|
|
1609
|
+
if (unconvertible.length > 0) {
|
|
1610
|
+
throw new UnconvertibleClauseError(input, unconvertible);
|
|
1611
|
+
}
|
|
1612
|
+
const groups = groupByPrecedence(resolved);
|
|
1613
|
+
const mixed = groups.length > 1;
|
|
1614
|
+
const rendered = groups.map((group) => {
|
|
1615
|
+
const parts = group.map((clause) => `(${clause.expression})`);
|
|
1616
|
+
const joined = parts.join(" and ");
|
|
1617
|
+
return mixed && group.length > 1 ? `(${joined})` : joined;
|
|
1618
|
+
});
|
|
1619
|
+
return {
|
|
1620
|
+
expression: rendered.join(" or "),
|
|
1621
|
+
clauses: clauses.map((clause) => clause.text)
|
|
1622
|
+
};
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1414
1625
|
// src/template/nl-intent.ts
|
|
1415
1626
|
var NLIntent = /* @__PURE__ */ ((NLIntent2) => {
|
|
1416
1627
|
NLIntent2["COMPARISON"] = "COMPARISON";
|
|
@@ -1449,7 +1660,7 @@ var INTENT_KEYWORDS = {
|
|
|
1449
1660
|
en: ["greater", "less", "equal", "above", "below", "exceed", ">", "<", "==", "!=", ">=", "<="]
|
|
1450
1661
|
},
|
|
1451
1662
|
["NULL_CHECK" /* NULL_CHECK */]: {
|
|
1452
|
-
zh: ["\u4E3A\u7A7A", "\u4E0D\u4E3A\u7A7A", "\u662F\u7A7A", "\u5B58\u5728", "\u4E0D\u5B58\u5728", "null", "\u6CA1\u6709\u503C"],
|
|
1663
|
+
zh: ["\u4E3A\u7A7A", "\u4E0D\u4E3A\u7A7A", "\u662F\u7A7A", "\u975E\u7A7A", "\u5B58\u5728", "\u4E0D\u5B58\u5728", "\u6709\u503C", "\u65E0\u503C", "null", "\u6CA1\u6709\u503C"],
|
|
1453
1664
|
en: ["null", "empty", "is null", "is not null", "is empty", "is not empty"]
|
|
1454
1665
|
},
|
|
1455
1666
|
["PERMISSION_CHECK" /* PERMISSION_CHECK */]: {
|
|
@@ -1486,7 +1697,10 @@ var INTENT_KEYWORDS = {
|
|
|
1486
1697
|
},
|
|
1487
1698
|
["BOOLEAN" /* BOOLEAN */]: {
|
|
1488
1699
|
zh: ["\u662F\u5426", "\u771F\u5047", "true", "false", "\u662F", "\u5426"],
|
|
1489
|
-
|
|
1700
|
+
// "no" is deliberately absent: it is a substring of "not", so it fired on
|
|
1701
|
+
// every negated null/emptiness phrase and made "remark is not empty" a
|
|
1702
|
+
// boolean check. "false"/"否" already cover the negative boolean spelling.
|
|
1703
|
+
en: ["true", "false", "yes", "is", "is not"]
|
|
1490
1704
|
},
|
|
1491
1705
|
["DATE" /* DATE */]: {
|
|
1492
1706
|
zh: ["\u65E5\u671F", "\u65F6\u95F4", "\u4E4B\u540E", "\u4E4B\u524D", "\u65E9\u4E8E", "\u665A\u4E8E"],
|
|
@@ -1505,6 +1719,38 @@ var INTENT_KEYWORDS = {
|
|
|
1505
1719
|
en: ["plus", "minus", "multiply", "divide", "mod", "sum", "average"]
|
|
1506
1720
|
}
|
|
1507
1721
|
};
|
|
1722
|
+
var NULL_NEGATED_SPECIFIC = [
|
|
1723
|
+
"\u4E0D\u4E3A\u7A7A",
|
|
1724
|
+
"\u4E0D\u4E3Anull",
|
|
1725
|
+
"\u4E0D\u662F\u7A7A",
|
|
1726
|
+
"\u975E\u7A7A",
|
|
1727
|
+
"is not null",
|
|
1728
|
+
"is not empty"
|
|
1729
|
+
];
|
|
1730
|
+
var NULL_AFFIRMATIVE = [
|
|
1731
|
+
"\u4E0D\u5B58\u5728",
|
|
1732
|
+
"\u65E0\u503C",
|
|
1733
|
+
"\u6CA1\u6709\u503C",
|
|
1734
|
+
"\u4E3A\u7A7A",
|
|
1735
|
+
"\u4E3Anull",
|
|
1736
|
+
"\u662F\u7A7A",
|
|
1737
|
+
"is null",
|
|
1738
|
+
"is empty"
|
|
1739
|
+
];
|
|
1740
|
+
var NULL_NEGATED_GENERIC = ["\u6709\u503C", "\u5B58\u5728"];
|
|
1741
|
+
function detectNullPredicate(input) {
|
|
1742
|
+
const text = input.trim().toLowerCase().replace(/[\uFF01-\uFF5E]/g, (ch) => String.fromCharCode(ch.charCodeAt(0) - 65248)).replace(/\s+/g, " ");
|
|
1743
|
+
for (const token of NULL_NEGATED_SPECIFIC) {
|
|
1744
|
+
if (text.includes(token)) return "negated";
|
|
1745
|
+
}
|
|
1746
|
+
for (const token of NULL_AFFIRMATIVE) {
|
|
1747
|
+
if (text.includes(token)) return "affirmative";
|
|
1748
|
+
}
|
|
1749
|
+
for (const token of NULL_NEGATED_GENERIC) {
|
|
1750
|
+
if (text.includes(token)) return "negated";
|
|
1751
|
+
}
|
|
1752
|
+
return null;
|
|
1753
|
+
}
|
|
1508
1754
|
var IntentClassifier = class {
|
|
1509
1755
|
/**
|
|
1510
1756
|
* Main method for classifying natural language input
|
|
@@ -1538,6 +1784,9 @@ var IntentClassifier = class {
|
|
|
1538
1784
|
if (/isempty|\.isEmpty|列表|数组|集合/.test(normalized)) {
|
|
1539
1785
|
intentScores.set("COLLECTION" /* COLLECTION */, (intentScores.get("COLLECTION" /* COLLECTION */) ?? 0) + 1);
|
|
1540
1786
|
}
|
|
1787
|
+
if (detectNullPredicate(normalized)) {
|
|
1788
|
+
intentScores.set("NULL_CHECK" /* NULL_CHECK */, (intentScores.get("NULL_CHECK" /* NULL_CHECK */) ?? 0) + 2);
|
|
1789
|
+
}
|
|
1541
1790
|
const entities = this.extractEntities(normalized);
|
|
1542
1791
|
const operators = this.extractOperators(normalized);
|
|
1543
1792
|
const logicalConnectors = this.extractLogicalConnectors(normalized);
|
|
@@ -1650,6 +1899,68 @@ var IntentClassifier = class {
|
|
|
1650
1899
|
};
|
|
1651
1900
|
|
|
1652
1901
|
// src/template/template-engine.ts
|
|
1902
|
+
var CHINESE_FIELD_MAP = {
|
|
1903
|
+
\u5907\u6CE8: "remark",
|
|
1904
|
+
\u8BF4\u660E: "description",
|
|
1905
|
+
\u63CF\u8FF0: "description",
|
|
1906
|
+
\u91D1\u989D: "amount",
|
|
1907
|
+
\u6570\u91CF: "count",
|
|
1908
|
+
\u4E2A\u6570: "count",
|
|
1909
|
+
\u72B6\u6001: "status",
|
|
1910
|
+
\u7C7B\u578B: "type",
|
|
1911
|
+
\u540D\u79F0: "name",
|
|
1912
|
+
\u6807\u9898: "title",
|
|
1913
|
+
\u5730\u5740: "address",
|
|
1914
|
+
\u90AE\u7BB1: "email",
|
|
1915
|
+
\u624B\u673A: "phone",
|
|
1916
|
+
\u7535\u8BDD: "phone",
|
|
1917
|
+
\u65E5\u671F: "date",
|
|
1918
|
+
\u65F6\u95F4: "time",
|
|
1919
|
+
\u5E74\u9F84: "age",
|
|
1920
|
+
\u4EF7\u683C: "price",
|
|
1921
|
+
\u7528\u6237\u540D: "name",
|
|
1922
|
+
\u6743\u9650: "role",
|
|
1923
|
+
\u6807\u7B7E: "tags",
|
|
1924
|
+
\u5217\u8868: "list",
|
|
1925
|
+
\u6570\u7EC4: "items",
|
|
1926
|
+
\u6587\u4EF6: "file",
|
|
1927
|
+
\u6587\u4EF6\u540D: "name",
|
|
1928
|
+
\u8FC7\u671F: "expiryDate",
|
|
1929
|
+
\u521B\u5EFA: "createdAt",
|
|
1930
|
+
\u6709\u6548: "valid",
|
|
1931
|
+
\u6D3B\u8DC3: "active",
|
|
1932
|
+
\u6FC0\u6D3B: "active"
|
|
1933
|
+
};
|
|
1934
|
+
var CHINESE_FIELDS_BY_LENGTH = Object.entries(CHINESE_FIELD_MAP).sort(
|
|
1935
|
+
(a, b) => b[0].length - a[0].length
|
|
1936
|
+
);
|
|
1937
|
+
var FIELD_STOPWORDS = /* @__PURE__ */ new Set([
|
|
1938
|
+
"a",
|
|
1939
|
+
"an",
|
|
1940
|
+
"account",
|
|
1941
|
+
"and",
|
|
1942
|
+
"are",
|
|
1943
|
+
"be",
|
|
1944
|
+
"between",
|
|
1945
|
+
"empty",
|
|
1946
|
+
"false",
|
|
1947
|
+
"file",
|
|
1948
|
+
"has",
|
|
1949
|
+
"have",
|
|
1950
|
+
"is",
|
|
1951
|
+
"no",
|
|
1952
|
+
"not",
|
|
1953
|
+
"null",
|
|
1954
|
+
"or",
|
|
1955
|
+
"order",
|
|
1956
|
+
"product",
|
|
1957
|
+
"than",
|
|
1958
|
+
"the",
|
|
1959
|
+
"true",
|
|
1960
|
+
"user",
|
|
1961
|
+
"value",
|
|
1962
|
+
"yes"
|
|
1963
|
+
]);
|
|
1653
1964
|
var TEMPLATE_LIBRARY = {
|
|
1654
1965
|
["COMPARISON" /* COMPARISON */]: [
|
|
1655
1966
|
{
|
|
@@ -1894,8 +2205,9 @@ var TemplateEngine = class {
|
|
|
1894
2205
|
selectBestTemplate(templates, intentResult, input) {
|
|
1895
2206
|
let bestScore = -1;
|
|
1896
2207
|
let bestTemplate = null;
|
|
1897
|
-
const
|
|
1898
|
-
const
|
|
2208
|
+
const polarity = detectNullPredicate(input);
|
|
2209
|
+
const isAffirmative = polarity === "affirmative";
|
|
2210
|
+
const isNegated = polarity === "negated";
|
|
1899
2211
|
for (const template of templates) {
|
|
1900
2212
|
const conditions = template.conditions;
|
|
1901
2213
|
let score = 0;
|
|
@@ -1904,8 +2216,10 @@ var TemplateEngine = class {
|
|
|
1904
2216
|
if (conditions.hasCollection) score += 0.5;
|
|
1905
2217
|
if (conditions.hasNull) score += 0.5;
|
|
1906
2218
|
if (conditions.hasString) score += 1;
|
|
1907
|
-
if (template.name.includes("IS_EMPTY") &&
|
|
1908
|
-
if (template.name.includes("IS_NOT_EMPTY") &&
|
|
2219
|
+
if (template.name.includes("IS_EMPTY") && isAffirmative) score += 2;
|
|
2220
|
+
if (template.name.includes("IS_NOT_EMPTY") && isNegated) score += 2;
|
|
2221
|
+
if (template.name === "NULL-IS_NULL" && isAffirmative) score += 2;
|
|
2222
|
+
if (template.name === "NULL-IS_NOT_NULL" && isNegated) score += 2;
|
|
1909
2223
|
if (conditions.entityCount) {
|
|
1910
2224
|
if (conditions.entityCount.min && intentResult.entities.length < conditions.entityCount.min) {
|
|
1911
2225
|
continue;
|
|
@@ -1921,37 +2235,8 @@ var TemplateEngine = class {
|
|
|
1921
2235
|
fillTemplate(template, input, intentResult) {
|
|
1922
2236
|
let expression = template;
|
|
1923
2237
|
const unfilledSlots = [];
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
if (this.contextSchema?.root) {
|
|
1927
|
-
rootName = this.contextSchema.root.name;
|
|
1928
|
-
const fields = Object.keys(this.contextSchema.root.fields ?? {});
|
|
1929
|
-
for (const f of fields) {
|
|
1930
|
-
if (input.includes(f)) {
|
|
1931
|
-
fieldName = f;
|
|
1932
|
-
break;
|
|
1933
|
-
}
|
|
1934
|
-
}
|
|
1935
|
-
} else {
|
|
1936
|
-
const rootMap = {
|
|
1937
|
-
\u8BA2\u5355: "order",
|
|
1938
|
-
order: "order",
|
|
1939
|
-
\u7528\u6237: "user",
|
|
1940
|
-
user: "user",
|
|
1941
|
-
\u6587\u4EF6: "file",
|
|
1942
|
-
file: "file",
|
|
1943
|
-
\u8D26\u53F7: "account",
|
|
1944
|
-
account: "account",
|
|
1945
|
-
\u5546\u54C1: "item",
|
|
1946
|
-
product: "item"
|
|
1947
|
-
};
|
|
1948
|
-
for (const [key, val] of Object.entries(rootMap)) {
|
|
1949
|
-
if (input.includes(key)) {
|
|
1950
|
-
rootName = val;
|
|
1951
|
-
break;
|
|
1952
|
-
}
|
|
1953
|
-
}
|
|
1954
|
-
}
|
|
2238
|
+
const rootName = this.resolveRootName(input);
|
|
2239
|
+
const fieldName = this.resolveFieldName(input);
|
|
1955
2240
|
expression = expression.replace(/\{root\}/g, rootName);
|
|
1956
2241
|
expression = expression.replace(/\{field\}/g, fieldName);
|
|
1957
2242
|
const fieldEntities = intentResult.entities.filter((e) => e.type === "field");
|
|
@@ -2020,6 +2305,58 @@ var TemplateEngine = class {
|
|
|
2020
2305
|
}
|
|
2021
2306
|
return { expression, unfilledSlots };
|
|
2022
2307
|
}
|
|
2308
|
+
/**
|
|
2309
|
+
* Resolve the SpEL root name for `input`: the configured schema root when one
|
|
2310
|
+
* exists, otherwise a keyword heuristic over the well-known roots.
|
|
2311
|
+
*/
|
|
2312
|
+
resolveRootName(input) {
|
|
2313
|
+
if (this.contextSchema?.root) {
|
|
2314
|
+
return this.contextSchema.root.name;
|
|
2315
|
+
}
|
|
2316
|
+
const rootMap = {
|
|
2317
|
+
\u8BA2\u5355: "order",
|
|
2318
|
+
order: "order",
|
|
2319
|
+
\u7528\u6237: "user",
|
|
2320
|
+
user: "user",
|
|
2321
|
+
\u6587\u4EF6: "file",
|
|
2322
|
+
file: "file",
|
|
2323
|
+
\u8D26\u53F7: "account",
|
|
2324
|
+
account: "account",
|
|
2325
|
+
\u5546\u54C1: "item",
|
|
2326
|
+
product: "item"
|
|
2327
|
+
};
|
|
2328
|
+
for (const [key, val] of Object.entries(rootMap)) {
|
|
2329
|
+
if (input.includes(key)) return val;
|
|
2330
|
+
}
|
|
2331
|
+
return "order";
|
|
2332
|
+
}
|
|
2333
|
+
/**
|
|
2334
|
+
* Resolve the field name for `input`.
|
|
2335
|
+
*
|
|
2336
|
+
* The previous implementation only compared schema *keys* and otherwise left
|
|
2337
|
+
* the literal placeholder default in place, which is how "#order.field == null"
|
|
2338
|
+
* reached callers. The lookup now falls back in order: schema key, schema
|
|
2339
|
+
* field description (Chinese inputs name the field by its description),
|
|
2340
|
+
* Chinese field word, first English identifier, and finally "value" — the
|
|
2341
|
+
* neutral default the pattern layer already uses for an unknown field.
|
|
2342
|
+
*/
|
|
2343
|
+
resolveFieldName(input) {
|
|
2344
|
+
const fields = this.contextSchema?.root?.fields;
|
|
2345
|
+
if (fields) {
|
|
2346
|
+
for (const [key, schema] of Object.entries(fields)) {
|
|
2347
|
+
if (input.includes(key)) return key;
|
|
2348
|
+
if (schema.description && input.includes(schema.description)) return key;
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
for (const [word, field] of CHINESE_FIELDS_BY_LENGTH) {
|
|
2352
|
+
if (input.includes(word)) return field;
|
|
2353
|
+
}
|
|
2354
|
+
const tokens = input.match(/[A-Za-z_][A-Za-z0-9_]*/g) ?? [];
|
|
2355
|
+
for (const token of tokens) {
|
|
2356
|
+
if (!FIELD_STOPWORDS.has(token.toLowerCase())) return token;
|
|
2357
|
+
}
|
|
2358
|
+
return "value";
|
|
2359
|
+
}
|
|
2023
2360
|
};
|
|
2024
2361
|
|
|
2025
2362
|
// src/template/prompts/prompt-builder.ts
|
|
@@ -2257,6 +2594,172 @@ ${userInput}
|
|
|
2257
2594
|
}
|
|
2258
2595
|
};
|
|
2259
2596
|
|
|
2597
|
+
// src/validation/validation-pipeline.ts
|
|
2598
|
+
var import_spel_ts = require("@agentix-e/spel-ts");
|
|
2599
|
+
|
|
2600
|
+
// src/validation/auto-fixer.ts
|
|
2601
|
+
function scanStringLiterals(expression) {
|
|
2602
|
+
const spans = [];
|
|
2603
|
+
let unterminatedQuote = null;
|
|
2604
|
+
let i = 0;
|
|
2605
|
+
while (i < expression.length) {
|
|
2606
|
+
const quote = expression[i];
|
|
2607
|
+
if (quote !== "'" && quote !== '"') {
|
|
2608
|
+
i += 1;
|
|
2609
|
+
continue;
|
|
2610
|
+
}
|
|
2611
|
+
const start = i;
|
|
2612
|
+
i += 1;
|
|
2613
|
+
let closed = false;
|
|
2614
|
+
while (i < expression.length) {
|
|
2615
|
+
if (expression[i] === quote) {
|
|
2616
|
+
if (expression[i + 1] === quote) {
|
|
2617
|
+
i += 2;
|
|
2618
|
+
continue;
|
|
2619
|
+
}
|
|
2620
|
+
i += 1;
|
|
2621
|
+
closed = true;
|
|
2622
|
+
break;
|
|
2623
|
+
}
|
|
2624
|
+
i += 1;
|
|
2625
|
+
}
|
|
2626
|
+
spans.push({ start, end: i });
|
|
2627
|
+
if (!closed) {
|
|
2628
|
+
unterminatedQuote = quote;
|
|
2629
|
+
break;
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
return { spans, unterminatedQuote };
|
|
2633
|
+
}
|
|
2634
|
+
function hasUnterminatedStringLiteral(expression) {
|
|
2635
|
+
return scanStringLiterals(expression).unterminatedQuote !== null;
|
|
2636
|
+
}
|
|
2637
|
+
function maskStringLiterals(expression) {
|
|
2638
|
+
const { spans } = scanStringLiterals(expression);
|
|
2639
|
+
if (spans.length === 0) return expression;
|
|
2640
|
+
const chars = expression.split("");
|
|
2641
|
+
for (const span of spans) {
|
|
2642
|
+
for (let i = span.start; i < span.end && i < chars.length; i++) {
|
|
2643
|
+
chars[i] = " ";
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
return chars.join("");
|
|
2647
|
+
}
|
|
2648
|
+
var AutoFixer = class {
|
|
2649
|
+
fix(expression) {
|
|
2650
|
+
const { chunks, unterminatedQuote } = this.split(expression);
|
|
2651
|
+
const changes = [];
|
|
2652
|
+
this.applyRule(
|
|
2653
|
+
chunks,
|
|
2654
|
+
/=== undefined/g,
|
|
2655
|
+
"== null",
|
|
2656
|
+
() => "Replaced === undefined with == null",
|
|
2657
|
+
changes
|
|
2658
|
+
);
|
|
2659
|
+
this.applyRule(
|
|
2660
|
+
chunks,
|
|
2661
|
+
/!== undefined/g,
|
|
2662
|
+
"!= null",
|
|
2663
|
+
() => "Replaced !== undefined with != null",
|
|
2664
|
+
changes
|
|
2665
|
+
);
|
|
2666
|
+
this.applyRule(chunks, /!==/g, "!=", (n) => `Replaced ${n}x !== with !=`, changes);
|
|
2667
|
+
this.applyRule(chunks, /===/g, "==", (n) => `Replaced ${n}x === with ==`, changes);
|
|
2668
|
+
this.applyRule(chunks, /&&/g, "and", (n) => `Replaced ${n}x && with and`, changes);
|
|
2669
|
+
this.applyRule(chunks, /\|\|/g, "or", (n) => `Replaced ${n}x || with or`, changes);
|
|
2670
|
+
this.applyRule(chunks, /> ==/g, ">=", () => "Replaced > == with >=", changes);
|
|
2671
|
+
this.applyRule(chunks, /< ==/g, "<=", () => "Replaced < == with <=", changes);
|
|
2672
|
+
this.applyElvisRule(chunks, changes);
|
|
2673
|
+
let fixed = chunks.map((chunk) => chunk.text).join("");
|
|
2674
|
+
if (unterminatedQuote !== null && !fixed.endsWith(unterminatedQuote)) {
|
|
2675
|
+
fixed += unterminatedQuote;
|
|
2676
|
+
changes.push(
|
|
2677
|
+
unterminatedQuote === "'" ? "Added missing closing single quote" : "Added missing closing double quote"
|
|
2678
|
+
);
|
|
2679
|
+
}
|
|
2680
|
+
const wasFixed = changes.length > 0;
|
|
2681
|
+
return {
|
|
2682
|
+
wasFixed,
|
|
2683
|
+
expression: wasFixed ? fixed : expression,
|
|
2684
|
+
changes
|
|
2685
|
+
};
|
|
2686
|
+
}
|
|
2687
|
+
/**
|
|
2688
|
+
* Apply one global replacement rule to every unprotected chunk.
|
|
2689
|
+
*
|
|
2690
|
+
* The replacement is only reported when it actually matched, and the count
|
|
2691
|
+
* is the number of matches across all chunks, so the human-readable change
|
|
2692
|
+
* log is unchanged from the previous implementation.
|
|
2693
|
+
*/
|
|
2694
|
+
applyRule(chunks, pattern, replacement, describe, changes) {
|
|
2695
|
+
let count = 0;
|
|
2696
|
+
for (const chunk of chunks) {
|
|
2697
|
+
if (chunk.protected) continue;
|
|
2698
|
+
const matches = chunk.text.match(pattern);
|
|
2699
|
+
if (matches === null) continue;
|
|
2700
|
+
count += matches.length;
|
|
2701
|
+
chunk.text = chunk.text.replace(pattern, replacement);
|
|
2702
|
+
}
|
|
2703
|
+
if (count > 0) {
|
|
2704
|
+
changes.push(describe(count));
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
/**
|
|
2708
|
+
* Normalise the Elvis operator only when it was written with whitespace
|
|
2709
|
+
* between `?` and `:`. A correctly written `?:` must be left untouched so
|
|
2710
|
+
* that `fix()` is a no-op on already-valid input.
|
|
2711
|
+
*/
|
|
2712
|
+
applyElvisRule(chunks, changes) {
|
|
2713
|
+
let touched = false;
|
|
2714
|
+
for (const chunk of chunks) {
|
|
2715
|
+
if (chunk.protected || !chunk.text.includes("? :")) continue;
|
|
2716
|
+
const next = chunk.text.replace(/\s*\?\s*:\s*/g, " ?: ");
|
|
2717
|
+
if (next !== chunk.text) {
|
|
2718
|
+
chunk.text = next;
|
|
2719
|
+
touched = true;
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
if (touched) {
|
|
2723
|
+
changes.push("Fixed Elvis operator spacing");
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
/**
|
|
2727
|
+
* Split the expression into alternating protected (string literal) and
|
|
2728
|
+
* unprotected chunks. Ranges come from spel-ts's tokenizer so they agree
|
|
2729
|
+
* exactly with what the parser considers opaque.
|
|
2730
|
+
*/
|
|
2731
|
+
split(expression) {
|
|
2732
|
+
const { spans, unterminatedQuote } = scanStringLiterals(expression);
|
|
2733
|
+
return { chunks: this.toChunks(expression, spans), unterminatedQuote };
|
|
2734
|
+
}
|
|
2735
|
+
/**
|
|
2736
|
+
* Turn protected spans into a chunk list covering the whole expression.
|
|
2737
|
+
*/
|
|
2738
|
+
toChunks(expression, spans) {
|
|
2739
|
+
const ordered = [...spans].sort((a, b) => a.start - b.start);
|
|
2740
|
+
const chunks = [];
|
|
2741
|
+
let cursor = 0;
|
|
2742
|
+
for (const span of ordered) {
|
|
2743
|
+
const start = Math.max(span.start, cursor);
|
|
2744
|
+
const end = Math.max(span.end, start);
|
|
2745
|
+
if (start > cursor) {
|
|
2746
|
+
chunks.push({ protected: false, text: expression.slice(cursor, start) });
|
|
2747
|
+
}
|
|
2748
|
+
if (end > start) {
|
|
2749
|
+
chunks.push({ protected: true, text: expression.slice(start, end) });
|
|
2750
|
+
}
|
|
2751
|
+
cursor = end;
|
|
2752
|
+
}
|
|
2753
|
+
if (cursor < expression.length) {
|
|
2754
|
+
chunks.push({ protected: false, text: expression.slice(cursor) });
|
|
2755
|
+
}
|
|
2756
|
+
if (chunks.length === 0) {
|
|
2757
|
+
chunks.push({ protected: false, text: "" });
|
|
2758
|
+
}
|
|
2759
|
+
return chunks;
|
|
2760
|
+
}
|
|
2761
|
+
};
|
|
2762
|
+
|
|
2260
2763
|
// src/validation/validation-pipeline.ts
|
|
2261
2764
|
var ValidationPipeline = class {
|
|
2262
2765
|
evaluator;
|
|
@@ -2276,25 +2779,29 @@ var ValidationPipeline = class {
|
|
|
2276
2779
|
const typeStage = this.validateTypes(expression, contextSchema);
|
|
2277
2780
|
const semanticStage = this.validateSemantic(expression);
|
|
2278
2781
|
const contextStage = this.validateContext(expression, contextSchema);
|
|
2782
|
+
const finalStage = this.validateFinal(expression);
|
|
2279
2783
|
errors.push(
|
|
2280
2784
|
...parseStage.errors,
|
|
2281
2785
|
...typeStage.errors,
|
|
2282
2786
|
...semanticStage.errors,
|
|
2283
|
-
...contextStage.errors
|
|
2787
|
+
...contextStage.errors,
|
|
2788
|
+
...finalStage.errors
|
|
2284
2789
|
);
|
|
2285
2790
|
warnings.push(
|
|
2286
2791
|
...parseStage.warnings,
|
|
2287
2792
|
...typeStage.warnings,
|
|
2288
2793
|
...semanticStage.warnings,
|
|
2289
|
-
...contextStage.warnings
|
|
2794
|
+
...contextStage.warnings,
|
|
2795
|
+
...finalStage.warnings
|
|
2290
2796
|
);
|
|
2291
2797
|
return {
|
|
2292
|
-
valid:
|
|
2798
|
+
valid: errors.length === 0,
|
|
2293
2799
|
stages: {
|
|
2294
2800
|
parse: parseStage,
|
|
2295
2801
|
type: typeStage,
|
|
2296
2802
|
semantic: semanticStage,
|
|
2297
|
-
context: contextStage
|
|
2803
|
+
context: contextStage,
|
|
2804
|
+
final: finalStage
|
|
2298
2805
|
},
|
|
2299
2806
|
errors,
|
|
2300
2807
|
warnings
|
|
@@ -2302,47 +2809,57 @@ var ValidationPipeline = class {
|
|
|
2302
2809
|
}
|
|
2303
2810
|
/**
|
|
2304
2811
|
* Stage 1: Parse Check — syntax validation
|
|
2812
|
+
*
|
|
2813
|
+
* Delimiter balance and JavaScript operators are checked against a copy of
|
|
2814
|
+
* the expression with string literals blanked out, so literal text can never
|
|
2815
|
+
* be mistaken for syntax.
|
|
2305
2816
|
*/
|
|
2306
2817
|
async validateParse(expression) {
|
|
2307
2818
|
const errors = [];
|
|
2308
2819
|
const warnings = [];
|
|
2820
|
+
const structural = maskStringLiterals(expression);
|
|
2309
2821
|
if (!expression || expression.trim().length === 0) {
|
|
2310
2822
|
errors.push({
|
|
2311
2823
|
code: "PARSE-EMPTY",
|
|
2312
2824
|
message: "Expression is empty",
|
|
2825
|
+
severity: "error",
|
|
2313
2826
|
stage: "parse",
|
|
2314
2827
|
requiresLLM: true
|
|
2315
2828
|
});
|
|
2316
2829
|
return { passed: false, errors, warnings };
|
|
2317
2830
|
}
|
|
2318
|
-
if (!this.hasBalancedParentheses(
|
|
2831
|
+
if (!this.hasBalancedParentheses(structural)) {
|
|
2319
2832
|
errors.push({
|
|
2320
2833
|
code: "PARSE-UNBALANCED_PARENS",
|
|
2321
2834
|
message: "Unbalanced parentheses in expression",
|
|
2835
|
+
severity: "error",
|
|
2322
2836
|
stage: "parse",
|
|
2323
2837
|
requiresLLM: true
|
|
2324
2838
|
});
|
|
2325
2839
|
}
|
|
2326
|
-
if (
|
|
2840
|
+
if (structural.includes("===") || structural.includes("!==")) {
|
|
2327
2841
|
errors.push({
|
|
2328
2842
|
code: "PARSE-JS_OPERATOR",
|
|
2329
2843
|
message: "JavaScript operators detected (=== or !==), use == or != in SpEL",
|
|
2844
|
+
severity: "error",
|
|
2330
2845
|
stage: "parse",
|
|
2331
2846
|
requiresLLM: true
|
|
2332
2847
|
});
|
|
2333
2848
|
}
|
|
2334
|
-
if (
|
|
2849
|
+
if (structural.includes("&&")) {
|
|
2335
2850
|
errors.push({
|
|
2336
2851
|
code: "PARSE-JS_LOGIC",
|
|
2337
2852
|
message: 'JavaScript && detected, use "and" in SpEL',
|
|
2853
|
+
severity: "error",
|
|
2338
2854
|
stage: "parse",
|
|
2339
2855
|
requiresLLM: true
|
|
2340
2856
|
});
|
|
2341
2857
|
}
|
|
2342
|
-
if (
|
|
2858
|
+
if (structural.includes("||")) {
|
|
2343
2859
|
errors.push({
|
|
2344
2860
|
code: "PARSE-JS_LOGIC",
|
|
2345
2861
|
message: 'JavaScript || detected, use "or" in SpEL',
|
|
2862
|
+
severity: "error",
|
|
2346
2863
|
stage: "parse",
|
|
2347
2864
|
requiresLLM: true
|
|
2348
2865
|
});
|
|
@@ -2355,6 +2872,7 @@ var ValidationPipeline = class {
|
|
|
2355
2872
|
errors.push({
|
|
2356
2873
|
code: `PARSE-${pe.code ?? "SYNTAX"}`,
|
|
2357
2874
|
message: pe.message,
|
|
2875
|
+
severity: "error",
|
|
2358
2876
|
position: pe.position,
|
|
2359
2877
|
stage: "parse",
|
|
2360
2878
|
requiresLLM: true
|
|
@@ -2365,6 +2883,7 @@ var ValidationPipeline = class {
|
|
|
2365
2883
|
errors.push({
|
|
2366
2884
|
code: "PARSE-EXCEPTION",
|
|
2367
2885
|
message: `Parse threw exception: ${err.message}`,
|
|
2886
|
+
severity: "error",
|
|
2368
2887
|
stage: "parse",
|
|
2369
2888
|
requiresLLM: true
|
|
2370
2889
|
});
|
|
@@ -2377,27 +2896,43 @@ var ValidationPipeline = class {
|
|
|
2377
2896
|
};
|
|
2378
2897
|
}
|
|
2379
2898
|
/**
|
|
2380
|
-
* Stage 2: Type Check — type validation
|
|
2899
|
+
* Stage 2: Type Check — advisory type validation
|
|
2381
2900
|
*/
|
|
2382
2901
|
validateTypes(expression, contextSchema) {
|
|
2383
2902
|
const errors = [];
|
|
2384
2903
|
const warnings = [];
|
|
2385
|
-
const
|
|
2904
|
+
const structural = maskStringLiterals(expression);
|
|
2905
|
+
const strNumMismatch = /'(?:\\.|[^'\\])*'\s*(?:>|<|>=|<=)\s*\d+|\d+\s*(?:>|<|>=|<=)\s*'(?:\\.|[^'\\])*'/;
|
|
2386
2906
|
if (strNumMismatch.test(expression)) {
|
|
2387
2907
|
warnings.push({
|
|
2388
2908
|
code: "TYPE-STR_NUM_CMP",
|
|
2389
2909
|
message: "String literal compared with number using arithmetic operator",
|
|
2910
|
+
severity: "warning",
|
|
2390
2911
|
stage: "type"
|
|
2391
2912
|
});
|
|
2392
2913
|
}
|
|
2393
2914
|
if (contextSchema?.root) {
|
|
2915
|
+
const rootRef = escapeRegExp(contextSchema.root.name);
|
|
2394
2916
|
for (const [fieldName, field] of Object.entries(contextSchema.root.fields)) {
|
|
2917
|
+
const fieldRef = `#(?:${rootRef}\\.)?${escapeRegExp(fieldName)}`;
|
|
2395
2918
|
if (field.type === "boolean") {
|
|
2396
|
-
const boolNumPattern = new RegExp(
|
|
2397
|
-
if (boolNumPattern.test(
|
|
2919
|
+
const boolNumPattern = new RegExp(`${fieldRef}\\s*(?:>|<|>=|<=)\\s*\\d+`);
|
|
2920
|
+
if (boolNumPattern.test(structural)) {
|
|
2398
2921
|
warnings.push({
|
|
2399
2922
|
code: "TYPE-BOOL_NUM_CMP",
|
|
2400
2923
|
message: `Boolean field '${fieldName}' compared with number`,
|
|
2924
|
+
severity: "warning",
|
|
2925
|
+
stage: "type"
|
|
2926
|
+
});
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
if (field.type === "number") {
|
|
2930
|
+
const numStrPattern = new RegExp(`${fieldRef}\\s*(?:>|<|>=|<=)\\s*'(?:\\\\.|[^'\\\\])*'`);
|
|
2931
|
+
if (numStrPattern.test(expression)) {
|
|
2932
|
+
warnings.push({
|
|
2933
|
+
code: "TYPE-STR_NUM_CMP",
|
|
2934
|
+
message: `Numeric field '${fieldName}' compared with a string literal`,
|
|
2935
|
+
severity: "warning",
|
|
2401
2936
|
stage: "type"
|
|
2402
2937
|
});
|
|
2403
2938
|
}
|
|
@@ -2411,23 +2946,26 @@ var ValidationPipeline = class {
|
|
|
2411
2946
|
};
|
|
2412
2947
|
}
|
|
2413
2948
|
/**
|
|
2414
|
-
* Stage 3: Semantic Check — semantic
|
|
2949
|
+
* Stage 3: Semantic Check — advisory semantic validation
|
|
2415
2950
|
*/
|
|
2416
2951
|
validateSemantic(expression) {
|
|
2417
2952
|
const errors = [];
|
|
2418
2953
|
const warnings = [];
|
|
2954
|
+
const structural = maskStringLiterals(expression);
|
|
2419
2955
|
const selfCompare = /(#\w+(?:\.\w+)*)\s*==\s*\1/;
|
|
2420
|
-
if (selfCompare.test(
|
|
2956
|
+
if (selfCompare.test(structural)) {
|
|
2421
2957
|
warnings.push({
|
|
2422
2958
|
code: "SEM-SELF_COMPARE",
|
|
2423
2959
|
message: "Self-comparison detected: expression is always true",
|
|
2960
|
+
severity: "warning",
|
|
2424
2961
|
stage: "semantic"
|
|
2425
2962
|
});
|
|
2426
2963
|
}
|
|
2427
|
-
if (
|
|
2964
|
+
if (structural.includes("!!")) {
|
|
2428
2965
|
warnings.push({
|
|
2429
2966
|
code: "SEM-DOUBLE_NEGATION",
|
|
2430
2967
|
message: "Double negation detected, consider simplifying",
|
|
2968
|
+
severity: "warning",
|
|
2431
2969
|
stage: "semantic"
|
|
2432
2970
|
});
|
|
2433
2971
|
}
|
|
@@ -2439,59 +2977,72 @@ var ValidationPipeline = class {
|
|
|
2439
2977
|
}
|
|
2440
2978
|
/**
|
|
2441
2979
|
* Stage 4: Context Check — context reference validation
|
|
2980
|
+
*
|
|
2981
|
+
* A supplied schema turns this stage into a real gate: an undeclared bean,
|
|
2982
|
+
* a missing root field or an undeclared variable is an error. Without a
|
|
2983
|
+
* schema nothing can be judged, so the stage stays advisory.
|
|
2442
2984
|
*/
|
|
2443
2985
|
validateContext(expression, contextSchema) {
|
|
2444
2986
|
const errors = [];
|
|
2445
2987
|
const warnings = [];
|
|
2988
|
+
const structural = maskStringLiterals(expression);
|
|
2446
2989
|
if (!contextSchema) {
|
|
2447
2990
|
warnings.push({
|
|
2448
2991
|
code: "CTX-NO_SCHEMA",
|
|
2449
2992
|
message: "No ContextSchema provided, skipping context validation",
|
|
2993
|
+
severity: "warning",
|
|
2450
2994
|
stage: "context"
|
|
2451
2995
|
});
|
|
2452
2996
|
return { passed: true, errors, warnings };
|
|
2453
2997
|
}
|
|
2454
|
-
const
|
|
2455
|
-
|
|
2456
|
-
|
|
2998
|
+
const variables = contextSchema.variables ?? {};
|
|
2999
|
+
const functions = contextSchema.functions ?? {};
|
|
3000
|
+
const beans = contextSchema.beans ?? {};
|
|
3001
|
+
const root = contextSchema.root;
|
|
3002
|
+
const rootFields = root?.fields ?? {};
|
|
3003
|
+
const refs = this.extractReferences(structural);
|
|
3004
|
+
if (root) {
|
|
2457
3005
|
for (const ref of refs) {
|
|
2458
|
-
if (ref.startsWith(`#${
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
}
|
|
3006
|
+
if (!ref.startsWith(`#${root.name}.`)) continue;
|
|
3007
|
+
const field = ref.split(".")[1];
|
|
3008
|
+
if (field && !(field in rootFields)) {
|
|
3009
|
+
errors.push({
|
|
3010
|
+
code: "CTX-UNKNOWN_FIELD",
|
|
3011
|
+
message: `Field '${field}' not found in root '${root.name}'`,
|
|
3012
|
+
severity: "error",
|
|
3013
|
+
stage: "context",
|
|
3014
|
+
requiresLLM: true
|
|
3015
|
+
});
|
|
2468
3016
|
}
|
|
2469
3017
|
}
|
|
2470
3018
|
}
|
|
2471
3019
|
for (const ref of refs) {
|
|
2472
|
-
if (ref.startsWith("#")
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
3020
|
+
if (!ref.startsWith("#") || ref.includes(".")) continue;
|
|
3021
|
+
const varName = ref.slice(1);
|
|
3022
|
+
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
|
|
3023
|
+
// as shorthand for `#root.name`.
|
|
3024
|
+
varName in rootFields;
|
|
3025
|
+
if (!known) {
|
|
3026
|
+
errors.push({
|
|
3027
|
+
code: "CTX-UNKNOWN_REF",
|
|
3028
|
+
message: `Unknown reference '${ref}'`,
|
|
3029
|
+
severity: "error",
|
|
3030
|
+
stage: "context",
|
|
3031
|
+
requiresLLM: true
|
|
3032
|
+
});
|
|
2484
3033
|
}
|
|
2485
3034
|
}
|
|
2486
|
-
const beanMatch =
|
|
2487
|
-
if (beanMatch
|
|
3035
|
+
const beanMatch = structural.match(/@(\w+)/g);
|
|
3036
|
+
if (beanMatch) {
|
|
2488
3037
|
for (const b of beanMatch) {
|
|
2489
3038
|
const beanName = b.slice(1);
|
|
2490
|
-
if (!(beanName in
|
|
2491
|
-
|
|
3039
|
+
if (!(beanName in beans)) {
|
|
3040
|
+
errors.push({
|
|
2492
3041
|
code: "CTX-UNKNOWN_BEAN",
|
|
2493
3042
|
message: `Bean '${beanName}' not found in ContextSchema`,
|
|
2494
|
-
|
|
3043
|
+
severity: "error",
|
|
3044
|
+
stage: "context",
|
|
3045
|
+
requiresLLM: true
|
|
2495
3046
|
});
|
|
2496
3047
|
}
|
|
2497
3048
|
}
|
|
@@ -2503,7 +3054,62 @@ var ValidationPipeline = class {
|
|
|
2503
3054
|
};
|
|
2504
3055
|
}
|
|
2505
3056
|
/**
|
|
2506
|
-
* Check
|
|
3057
|
+
* Stage 5: Final Check — the generated expression must parse.
|
|
3058
|
+
*
|
|
3059
|
+
* When an evaluator is configured the parse stage already performed a real
|
|
3060
|
+
* parse; this mandatory final gate adds the structural check that a truncated
|
|
3061
|
+
* tail (an expression ending on an operator) is never accepted, even when no
|
|
3062
|
+
* engine is wired in. It is deliberately conservative: only a trailing
|
|
3063
|
+
* operator or an unterminated literal fails, so no well-formed expression is
|
|
3064
|
+
* rejected.
|
|
3065
|
+
*/
|
|
3066
|
+
validateFinal(expression) {
|
|
3067
|
+
const errors = [];
|
|
3068
|
+
const warnings = [];
|
|
3069
|
+
if (this.isManifestlyIncomplete(expression)) {
|
|
3070
|
+
errors.push({
|
|
3071
|
+
code: "FINAL-TRUNCATED",
|
|
3072
|
+
message: "Expression is incomplete: it ends with an operator or is not terminated",
|
|
3073
|
+
severity: "error",
|
|
3074
|
+
stage: "final",
|
|
3075
|
+
requiresLLM: true
|
|
3076
|
+
});
|
|
3077
|
+
}
|
|
3078
|
+
return { passed: errors.length === 0, errors, warnings };
|
|
3079
|
+
}
|
|
3080
|
+
/**
|
|
3081
|
+
* Whether the expression is obviously incomplete.
|
|
3082
|
+
*
|
|
3083
|
+
* The last token is obtained from spel-ts's tokenizer, so a trailing
|
|
3084
|
+
* operator keyword or an unterminated string literal is detected exactly,
|
|
3085
|
+
* and text inside a literal is never read as a trailing operator.
|
|
3086
|
+
*/
|
|
3087
|
+
isManifestlyIncomplete(expression) {
|
|
3088
|
+
if (expression.trim().length === 0) return true;
|
|
3089
|
+
const tokenizer = new import_spel_ts.Tokenizer(expression);
|
|
3090
|
+
let lastKind = null;
|
|
3091
|
+
let lastLiteral;
|
|
3092
|
+
let previousKind = null;
|
|
3093
|
+
try {
|
|
3094
|
+
for (; ; ) {
|
|
3095
|
+
const token = tokenizer.nextToken();
|
|
3096
|
+
if (token.kind === import_spel_ts.TokenKind.EOF) break;
|
|
3097
|
+
previousKind = lastKind;
|
|
3098
|
+
lastKind = token.kind;
|
|
3099
|
+
lastLiteral = token.literal;
|
|
3100
|
+
}
|
|
3101
|
+
} catch {
|
|
3102
|
+
return hasUnterminatedStringLiteral(expression);
|
|
3103
|
+
}
|
|
3104
|
+
if (lastKind === null) return true;
|
|
3105
|
+
if (INCOMPLETE_TRAILING_TOKENS.has(lastKind)) return true;
|
|
3106
|
+
if (lastLiteral !== void 0 && /^[A-Za-z]+$/.test(lastLiteral) && lastKind !== import_spel_ts.TokenKind.LITERAL_STRING && previousKind !== import_spel_ts.TokenKind.DOT && previousKind !== import_spel_ts.TokenKind.SAFE_NAV && WORD_OPERATORS.has(lastLiteral.toLowerCase())) {
|
|
3107
|
+
return true;
|
|
3108
|
+
}
|
|
3109
|
+
return false;
|
|
3110
|
+
}
|
|
3111
|
+
/**
|
|
3112
|
+
* Check if parentheses are balanced. Expects a literal-masked expression.
|
|
2507
3113
|
*/
|
|
2508
3114
|
hasBalancedParentheses(expression) {
|
|
2509
3115
|
const stack = [];
|
|
@@ -2519,17 +3125,29 @@ var ValidationPipeline = class {
|
|
|
2519
3125
|
return stack.length === 0;
|
|
2520
3126
|
}
|
|
2521
3127
|
/**
|
|
2522
|
-
* Extract all identifier references from expression
|
|
3128
|
+
* Extract all identifier references from expression.
|
|
3129
|
+
*
|
|
3130
|
+
* A bare `#x` that is only the head of a dotted reference (`#x.y`) is not
|
|
3131
|
+
* emitted on its own: a dotted reference names a property of some object,
|
|
3132
|
+
* not a variable of that name.
|
|
2523
3133
|
*/
|
|
2524
3134
|
extractReferences(expression) {
|
|
2525
3135
|
const refs = [];
|
|
3136
|
+
const dottedHeads = /* @__PURE__ */ new Set();
|
|
2526
3137
|
const varMatch = expression.matchAll(/#(\w+(?:\.\w+(?:\.\w+)?)?)/g);
|
|
2527
3138
|
for (const m of varMatch) {
|
|
2528
|
-
|
|
3139
|
+
const ref = `#${m[1]}`;
|
|
3140
|
+
if (!refs.includes(ref)) {
|
|
3141
|
+
refs.push(ref);
|
|
3142
|
+
}
|
|
3143
|
+
if (ref.includes(".")) {
|
|
3144
|
+
dottedHeads.add(ref.split(".")[0]);
|
|
3145
|
+
}
|
|
2529
3146
|
}
|
|
2530
3147
|
const simpleMatch = expression.matchAll(/#(\w+)(?!\w*\()/g);
|
|
2531
3148
|
for (const m of simpleMatch) {
|
|
2532
3149
|
const ref = `#${m[1]}`;
|
|
3150
|
+
if (dottedHeads.has(ref)) continue;
|
|
2533
3151
|
if (!refs.includes(ref)) {
|
|
2534
3152
|
refs.push(ref);
|
|
2535
3153
|
}
|
|
@@ -2537,98 +3155,68 @@ var ValidationPipeline = class {
|
|
|
2537
3155
|
return refs;
|
|
2538
3156
|
}
|
|
2539
3157
|
};
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
fixed = fixed.replace(/\?\s*:\s*/g, " ?: ");
|
|
2603
|
-
changes.push("Fixed Elvis operator spacing");
|
|
2604
|
-
}
|
|
2605
|
-
const wasFixed = changes.length > 0;
|
|
2606
|
-
return {
|
|
2607
|
-
wasFixed,
|
|
2608
|
-
expression: wasFixed ? fixed : expression,
|
|
2609
|
-
changes
|
|
2610
|
-
};
|
|
2611
|
-
}
|
|
2612
|
-
fixAllBrackets(expr) {
|
|
2613
|
-
let result = expr;
|
|
2614
|
-
const openParen = (result.match(/\(/g) ?? []).length;
|
|
2615
|
-
const closeParen = (result.match(/\)/g) ?? []).length;
|
|
2616
|
-
if (openParen > closeParen) {
|
|
2617
|
-
result += ")".repeat(openParen - closeParen);
|
|
2618
|
-
}
|
|
2619
|
-
const openBracket = (result.match(/\[/g) ?? []).length;
|
|
2620
|
-
const closeBracket = (result.match(/\]/g) ?? []).length;
|
|
2621
|
-
if (openBracket > closeBracket) {
|
|
2622
|
-
result += "]".repeat(openBracket - closeBracket);
|
|
2623
|
-
}
|
|
2624
|
-
const openBrace = (result.match(/\{/g) ?? []).length;
|
|
2625
|
-
const closeBrace = (result.match(/\}/g) ?? []).length;
|
|
2626
|
-
if (openBrace > closeBrace) {
|
|
2627
|
-
result += "}".repeat(openBrace - closeBrace);
|
|
2628
|
-
}
|
|
2629
|
-
return result;
|
|
2630
|
-
}
|
|
2631
|
-
};
|
|
3158
|
+
var INCOMPLETE_TRAILING_TOKENS = /* @__PURE__ */ new Set([
|
|
3159
|
+
import_spel_ts.TokenKind.PLUS,
|
|
3160
|
+
import_spel_ts.TokenKind.MINUS,
|
|
3161
|
+
import_spel_ts.TokenKind.STAR,
|
|
3162
|
+
import_spel_ts.TokenKind.SLASH,
|
|
3163
|
+
import_spel_ts.TokenKind.PERCENT,
|
|
3164
|
+
import_spel_ts.TokenKind.MOD,
|
|
3165
|
+
import_spel_ts.TokenKind.POWER,
|
|
3166
|
+
import_spel_ts.TokenKind.INC,
|
|
3167
|
+
import_spel_ts.TokenKind.DEC,
|
|
3168
|
+
import_spel_ts.TokenKind.EQ,
|
|
3169
|
+
import_spel_ts.TokenKind.NE,
|
|
3170
|
+
import_spel_ts.TokenKind.LT,
|
|
3171
|
+
import_spel_ts.TokenKind.LE,
|
|
3172
|
+
import_spel_ts.TokenKind.GT,
|
|
3173
|
+
import_spel_ts.TokenKind.GE,
|
|
3174
|
+
import_spel_ts.TokenKind.AND,
|
|
3175
|
+
import_spel_ts.TokenKind.OR,
|
|
3176
|
+
import_spel_ts.TokenKind.NOT,
|
|
3177
|
+
import_spel_ts.TokenKind.ASSIGN,
|
|
3178
|
+
import_spel_ts.TokenKind.MATCHES,
|
|
3179
|
+
import_spel_ts.TokenKind.BETWEEN,
|
|
3180
|
+
import_spel_ts.TokenKind.INSTANCEOF,
|
|
3181
|
+
import_spel_ts.TokenKind.LPAREN,
|
|
3182
|
+
import_spel_ts.TokenKind.LBRACKET,
|
|
3183
|
+
import_spel_ts.TokenKind.LBRACE,
|
|
3184
|
+
import_spel_ts.TokenKind.COMMA,
|
|
3185
|
+
import_spel_ts.TokenKind.COLON,
|
|
3186
|
+
import_spel_ts.TokenKind.DOT,
|
|
3187
|
+
import_spel_ts.TokenKind.SAFE_NAV,
|
|
3188
|
+
import_spel_ts.TokenKind.QMARK,
|
|
3189
|
+
import_spel_ts.TokenKind.ELVIS,
|
|
3190
|
+
import_spel_ts.TokenKind.HASH,
|
|
3191
|
+
import_spel_ts.TokenKind.AT,
|
|
3192
|
+
import_spel_ts.TokenKind.AMP_AT,
|
|
3193
|
+
import_spel_ts.TokenKind.PROJECTION,
|
|
3194
|
+
import_spel_ts.TokenKind.SELECTION,
|
|
3195
|
+
import_spel_ts.TokenKind.SELECT_FIRST,
|
|
3196
|
+
import_spel_ts.TokenKind.SELECT_LAST,
|
|
3197
|
+
import_spel_ts.TokenKind.TYPE_START,
|
|
3198
|
+
import_spel_ts.TokenKind.NEW,
|
|
3199
|
+
import_spel_ts.TokenKind.DOTDOT
|
|
3200
|
+
]);
|
|
3201
|
+
var WORD_OPERATORS = /* @__PURE__ */ new Set([
|
|
3202
|
+
"and",
|
|
3203
|
+
"or",
|
|
3204
|
+
"not",
|
|
3205
|
+
"matches",
|
|
3206
|
+
"between",
|
|
3207
|
+
"instanceof",
|
|
3208
|
+
// The textual operators in Spring's ALTERNATIVE_OPERATOR_NAMES (`eq`, `ne`,
|
|
3209
|
+
// `div`, …) tokenize as their own kinds where the engine defines one, and as
|
|
3210
|
+
// plain identifiers where it does not. `div` is the one that has no kind on
|
|
3211
|
+
// the engine build this package compiles against — `1 div 2` tokenizes as
|
|
3212
|
+
// IDENTIFIER — so it is listed here to keep `#a div` from passing as complete.
|
|
3213
|
+
// A property named `div` is still safe: the preceding-dot guard below exempts
|
|
3214
|
+
// `#order.div`.
|
|
3215
|
+
"div"
|
|
3216
|
+
]);
|
|
3217
|
+
function escapeRegExp(value) {
|
|
3218
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3219
|
+
}
|
|
2632
3220
|
|
|
2633
3221
|
// src/validation/self-correction-loop.ts
|
|
2634
3222
|
var SelfCorrectionLoop = class {
|
|
@@ -2786,20 +3374,23 @@ var StrategyRouter = class {
|
|
|
2786
3374
|
if (contextSchema) {
|
|
2787
3375
|
this.templateEngine.setContext(contextSchema);
|
|
2788
3376
|
}
|
|
2789
|
-
const
|
|
2790
|
-
|
|
2791
|
-
|
|
3377
|
+
const isCompound = splitClauses(nl).length > 1;
|
|
3378
|
+
let clauseFailure = null;
|
|
3379
|
+
const wholeMatch = this.patternMatcher.match(nl);
|
|
3380
|
+
const wholeIsFaithful = wholeMatch.matched && wholeMatch.confidence >= this.config.patternMinConfidence && (!isCompound || (wholeMatch.pattern?.tags.includes("logic") ?? false));
|
|
3381
|
+
if (wholeIsFaithful) {
|
|
3382
|
+
const validation = await this.validationPipeline.validate(wholeMatch.spel, contextSchema);
|
|
2792
3383
|
if (validation.valid) {
|
|
2793
3384
|
return {
|
|
2794
|
-
expression:
|
|
3385
|
+
expression: wholeMatch.spel,
|
|
2795
3386
|
strategy: "pattern",
|
|
2796
|
-
confidence:
|
|
2797
|
-
metadata: { patternId:
|
|
3387
|
+
confidence: wholeMatch.confidence,
|
|
3388
|
+
metadata: { patternId: wholeMatch.pattern?.id },
|
|
2798
3389
|
latencyMs: Date.now() - startTime
|
|
2799
3390
|
};
|
|
2800
3391
|
}
|
|
2801
3392
|
try {
|
|
2802
|
-
const afResult = this.autoFixer.fix(
|
|
3393
|
+
const afResult = this.autoFixer.fix(wholeMatch.spel);
|
|
2803
3394
|
if (afResult.wasFixed) {
|
|
2804
3395
|
const afValidation = await this.validationPipeline.validate(
|
|
2805
3396
|
afResult.expression,
|
|
@@ -2809,8 +3400,8 @@ var StrategyRouter = class {
|
|
|
2809
3400
|
return {
|
|
2810
3401
|
expression: afResult.expression,
|
|
2811
3402
|
strategy: "pattern",
|
|
2812
|
-
confidence:
|
|
2813
|
-
metadata: { patternId:
|
|
3403
|
+
confidence: wholeMatch.confidence * 0.95,
|
|
3404
|
+
metadata: { patternId: wholeMatch.pattern?.id },
|
|
2814
3405
|
latencyMs: Date.now() - startTime
|
|
2815
3406
|
};
|
|
2816
3407
|
}
|
|
@@ -2818,6 +3409,30 @@ var StrategyRouter = class {
|
|
|
2818
3409
|
} catch {
|
|
2819
3410
|
}
|
|
2820
3411
|
}
|
|
3412
|
+
if (isCompound && !wholeIsFaithful) {
|
|
3413
|
+
let decomposition = null;
|
|
3414
|
+
try {
|
|
3415
|
+
decomposition = this.decomposeClauses(nl);
|
|
3416
|
+
} catch (error) {
|
|
3417
|
+
if (!(error instanceof UnconvertibleClauseError)) throw error;
|
|
3418
|
+
clauseFailure = error;
|
|
3419
|
+
}
|
|
3420
|
+
if (decomposition) {
|
|
3421
|
+
const validation = await this.validationPipeline.validate(
|
|
3422
|
+
decomposition.expression,
|
|
3423
|
+
contextSchema
|
|
3424
|
+
);
|
|
3425
|
+
if (validation.valid) {
|
|
3426
|
+
return {
|
|
3427
|
+
expression: decomposition.expression,
|
|
3428
|
+
strategy: "pattern",
|
|
3429
|
+
confidence: decomposition.confidence,
|
|
3430
|
+
metadata: { clauses: decomposition.clauses },
|
|
3431
|
+
latencyMs: Date.now() - startTime
|
|
3432
|
+
};
|
|
3433
|
+
}
|
|
3434
|
+
}
|
|
3435
|
+
}
|
|
2821
3436
|
const intentResult = this.intentClassifier.classify(nl);
|
|
2822
3437
|
let templateResult = null;
|
|
2823
3438
|
try {
|
|
@@ -2851,7 +3466,7 @@ var StrategyRouter = class {
|
|
|
2851
3466
|
}
|
|
2852
3467
|
}
|
|
2853
3468
|
if (providers.length === 0) {
|
|
2854
|
-
throw new Error("No LLM providers available");
|
|
3469
|
+
throw clauseFailure ?? new Error("No LLM providers available");
|
|
2855
3470
|
}
|
|
2856
3471
|
let lastError = null;
|
|
2857
3472
|
for (const provider of providers) {
|
|
@@ -2897,6 +3512,33 @@ var StrategyRouter = class {
|
|
|
2897
3512
|
/**
|
|
2898
3513
|
* Get PatternMatcher (for external testing/debugging)
|
|
2899
3514
|
*/
|
|
3515
|
+
/**
|
|
3516
|
+
* Convert a sentence that joins its clauses with a logical connector.
|
|
3517
|
+
*
|
|
3518
|
+
* Returns `null` when `nl` has no top-level connector, in which case the caller
|
|
3519
|
+
* should use its ordinary single-pass conversion. Throws
|
|
3520
|
+
* {@link UnconvertibleClauseError} when a clause cannot be converted, so the
|
|
3521
|
+
* caller can refuse instead of emitting a partial rule.
|
|
3522
|
+
*/
|
|
3523
|
+
decomposeClauses(nl) {
|
|
3524
|
+
if (splitClauses(nl).length <= 1) return null;
|
|
3525
|
+
const confidences = [];
|
|
3526
|
+
const convert = (clause) => {
|
|
3527
|
+
const result = this.patternMatcher.match(clause);
|
|
3528
|
+
if (!result.matched || result.confidence < this.config.patternMinConfidence) {
|
|
3529
|
+
return null;
|
|
3530
|
+
}
|
|
3531
|
+
confidences.push(result.confidence);
|
|
3532
|
+
return result.spel;
|
|
3533
|
+
};
|
|
3534
|
+
const decomposition = decompose(nl, convert);
|
|
3535
|
+
if (decomposition === null) return null;
|
|
3536
|
+
return {
|
|
3537
|
+
expression: decomposition.expression,
|
|
3538
|
+
clauses: decomposition.clauses,
|
|
3539
|
+
confidence: confidences.length > 0 ? Math.min(...confidences) : 0
|
|
3540
|
+
};
|
|
3541
|
+
}
|
|
2900
3542
|
getPatternMatcher() {
|
|
2901
3543
|
return this.patternMatcher;
|
|
2902
3544
|
}
|
|
@@ -2980,6 +3622,19 @@ var NL2SpelEngine = class {
|
|
|
2980
3622
|
if (options.offlineOnly) {
|
|
2981
3623
|
const patternMatcher = this.router.getPatternMatcher();
|
|
2982
3624
|
const patternResult = patternMatcher.match(nl);
|
|
3625
|
+
const isCompound = splitClauses(nl).length > 1;
|
|
3626
|
+
const wholeIsFaithful = patternResult.matched && (patternResult.pattern?.tags.includes("logic") ?? false);
|
|
3627
|
+
if (isCompound && !wholeIsFaithful) {
|
|
3628
|
+
const decomposition = this.router.decomposeClauses(nl);
|
|
3629
|
+
if (decomposition) {
|
|
3630
|
+
return {
|
|
3631
|
+
expression: decomposition.expression,
|
|
3632
|
+
strategy: "pattern",
|
|
3633
|
+
confidence: decomposition.confidence,
|
|
3634
|
+
latencyMs: Date.now() - startTime
|
|
3635
|
+
};
|
|
3636
|
+
}
|
|
3637
|
+
}
|
|
2983
3638
|
if (patternResult.matched) {
|
|
2984
3639
|
return {
|
|
2985
3640
|
expression: patternResult.spel,
|
|
@@ -3058,5 +3713,9 @@ var NL2SpelEngine = class {
|
|
|
3058
3713
|
SelfCorrectionLoop,
|
|
3059
3714
|
StrategyRouter,
|
|
3060
3715
|
TemplateEngine,
|
|
3061
|
-
|
|
3716
|
+
UnconvertibleClauseError,
|
|
3717
|
+
UnmappedFieldError,
|
|
3718
|
+
ValidationPipeline,
|
|
3719
|
+
decompose,
|
|
3720
|
+
splitClauses
|
|
3062
3721
|
});
|