@agentix-e/nl2spel 1.2.2 → 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 +4 -2
- package/dist/index.cjs +976 -295
- package/dist/index.d.cts +251 -36
- package/dist/index.d.ts +251 -36
- package/dist/index.js +971 -294
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -34,54 +34,80 @@ __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
|
|
|
41
45
|
// src/provider/provider-registry.ts
|
|
42
46
|
var ProviderRegistry = class {
|
|
43
47
|
_providers = [];
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
48
|
+
_nextIndex = 0;
|
|
49
|
+
/**
|
|
50
|
+
* Register a Provider.
|
|
51
|
+
* @param provider LLMProvider instance
|
|
52
|
+
* @param options.priority User-assigned priority (lower = preferred). Defaults to registration order.
|
|
53
|
+
*/
|
|
54
|
+
register(provider, options) {
|
|
55
|
+
if (this._providers.some((p) => p.provider.name === provider.name)) {
|
|
47
56
|
throw new Error(`Provider '${provider.name}' already registered`);
|
|
48
57
|
}
|
|
49
|
-
this._providers.push(
|
|
58
|
+
this._providers.push({
|
|
59
|
+
provider,
|
|
60
|
+
priority: options?.priority ?? this._nextIndex,
|
|
61
|
+
index: this._nextIndex
|
|
62
|
+
});
|
|
63
|
+
this._nextIndex++;
|
|
50
64
|
}
|
|
51
65
|
/** Unregister a Provider */
|
|
52
66
|
unregister(name) {
|
|
53
|
-
this._providers = this._providers.filter((p) => p.name !== name);
|
|
67
|
+
this._providers = this._providers.filter((p) => p.provider.name !== name);
|
|
54
68
|
}
|
|
55
69
|
/** Get a Provider by name */
|
|
56
70
|
get(name) {
|
|
57
|
-
return this._providers.find((p) => p.name === name);
|
|
71
|
+
return this._providers.find((p) => p.provider.name === name)?.provider;
|
|
58
72
|
}
|
|
59
73
|
/**
|
|
60
74
|
* Get available Providers sorted by priority.
|
|
61
|
-
* Sort rule: offline
|
|
75
|
+
* Sort rule: offline first → user priority (asc) → registration order (asc)
|
|
62
76
|
*/
|
|
63
77
|
async getPrioritized() {
|
|
64
78
|
const available = [];
|
|
65
|
-
for (const
|
|
66
|
-
if (await
|
|
67
|
-
available.push(
|
|
79
|
+
for (const entry of this._providers) {
|
|
80
|
+
if (await entry.provider.isAvailable()) {
|
|
81
|
+
available.push(entry);
|
|
68
82
|
}
|
|
69
83
|
}
|
|
70
84
|
return available.sort((a, b) => {
|
|
71
|
-
const aOffline = a.capabilities.offlineAvailable;
|
|
72
|
-
const bOffline = b.capabilities.offlineAvailable;
|
|
85
|
+
const aOffline = a.provider.capabilities.offlineAvailable;
|
|
86
|
+
const bOffline = b.provider.capabilities.offlineAvailable;
|
|
73
87
|
if (aOffline && !bOffline) return -1;
|
|
74
88
|
if (!aOffline && bOffline) return 1;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
89
|
+
if (a.priority !== b.priority) return a.priority - b.priority;
|
|
90
|
+
return a.index - b.index;
|
|
91
|
+
}).map((entry) => entry.provider);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Explicitly reorder providers by name.
|
|
95
|
+
* Providers not listed retain their position after the reordered ones.
|
|
96
|
+
*/
|
|
97
|
+
reorder(providerNames) {
|
|
98
|
+
const orderMap = new Map(providerNames.map((name, i) => [name, i]));
|
|
99
|
+
const maxExisting = this._providers.reduce(
|
|
100
|
+
(max, p) => Math.max(max, p.priority),
|
|
101
|
+
providerNames.length - 1
|
|
102
|
+
);
|
|
103
|
+
for (const entry of this._providers) {
|
|
104
|
+
const explicitIndex = orderMap.get(entry.provider.name);
|
|
105
|
+
entry.priority = explicitIndex ?? maxExisting + entry.index + 1;
|
|
106
|
+
}
|
|
81
107
|
}
|
|
82
108
|
/** List all registered Providers */
|
|
83
109
|
list() {
|
|
84
|
-
return
|
|
110
|
+
return this._providers.map((p) => p.provider);
|
|
85
111
|
}
|
|
86
112
|
/** Number of registered Providers */
|
|
87
113
|
get count() {
|
|
@@ -412,10 +438,55 @@ var ChineseNumberParser = class {
|
|
|
412
438
|
};
|
|
413
439
|
|
|
414
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
|
+
};
|
|
415
484
|
var PatternMatcher = class {
|
|
416
485
|
_patterns;
|
|
417
|
-
|
|
486
|
+
fieldPolicy;
|
|
487
|
+
constructor(patterns = [], options = {}) {
|
|
418
488
|
this._patterns = [...patterns];
|
|
489
|
+
this.fieldPolicy = options.fieldPolicy ?? "passthrough";
|
|
419
490
|
this.sortByPriority();
|
|
420
491
|
}
|
|
421
492
|
get patternCount() {
|
|
@@ -449,9 +520,22 @@ var PatternMatcher = class {
|
|
|
449
520
|
}
|
|
450
521
|
}
|
|
451
522
|
}
|
|
452
|
-
const
|
|
523
|
+
const { expression, unmappedFields } = this.fillTemplate(
|
|
524
|
+
pattern,
|
|
525
|
+
slots,
|
|
526
|
+
normalized,
|
|
527
|
+
matchResult
|
|
528
|
+
);
|
|
453
529
|
const latencyMs2 = Date.now() - startTime;
|
|
454
|
-
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
|
+
};
|
|
455
539
|
}
|
|
456
540
|
const latencyMs = Date.now() - startTime;
|
|
457
541
|
return { matched: false, confidence: 0, latencyMs };
|
|
@@ -472,13 +556,20 @@ var PatternMatcher = class {
|
|
|
472
556
|
if (value !== void 0) slots[key] = value;
|
|
473
557
|
}
|
|
474
558
|
}
|
|
559
|
+
const { expression, unmappedFields } = this.fillTemplate(
|
|
560
|
+
pattern,
|
|
561
|
+
slots,
|
|
562
|
+
normalized,
|
|
563
|
+
matchResult
|
|
564
|
+
);
|
|
475
565
|
results.push({
|
|
476
566
|
matched: true,
|
|
477
567
|
pattern,
|
|
478
|
-
spel:
|
|
568
|
+
spel: expression,
|
|
479
569
|
confidence: pattern.confidence,
|
|
480
570
|
latencyMs: 0,
|
|
481
|
-
slots
|
|
571
|
+
slots,
|
|
572
|
+
...unmappedFields.length > 0 ? { unmappedFields } : {}
|
|
482
573
|
});
|
|
483
574
|
}
|
|
484
575
|
return results;
|
|
@@ -500,46 +591,37 @@ var PatternMatcher = class {
|
|
|
500
591
|
if (/^(?:商品|product|item)/i.test(input)) return "product";
|
|
501
592
|
return "order";
|
|
502
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
|
+
}
|
|
503
601
|
/**
|
|
504
602
|
* Extract Chinese field names from input and map to SpEL fields
|
|
505
603
|
*/
|
|
506
604
|
extractChineseField(input) {
|
|
507
|
-
const
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
\u5E74\u9F84: "age",
|
|
528
|
-
\u4EF7\u683C: "price",
|
|
529
|
-
\u7528\u6237\u540D: "name",
|
|
530
|
-
\u6743\u9650: "role",
|
|
531
|
-
\u6807\u7B7E: "tags",
|
|
532
|
-
\u5217\u8868: "list",
|
|
533
|
-
\u6570\u7EC4: "items",
|
|
534
|
-
\u6587\u4EF6: "file",
|
|
535
|
-
\u6587\u4EF6\u540D: "name",
|
|
536
|
-
\u8FC7\u671F: "expiryDate",
|
|
537
|
-
\u521B\u5EFA: "createdAt",
|
|
538
|
-
\u6709\u6548: "valid",
|
|
539
|
-
\u6D3B\u8DC3: "active",
|
|
540
|
-
\u6FC0\u6D3B: "active"
|
|
541
|
-
};
|
|
542
|
-
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 };
|
|
543
625
|
}
|
|
544
626
|
/**
|
|
545
627
|
* Template filling and value transformation
|
|
@@ -547,7 +629,20 @@ var PatternMatcher = class {
|
|
|
547
629
|
fillTemplate(pattern, slots, originalInput, _matchResult) {
|
|
548
630
|
let result = pattern.spelTemplate;
|
|
549
631
|
const hasFieldSlot = "field" in slots;
|
|
550
|
-
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
|
+
}
|
|
551
646
|
const root = this.inferRoot(originalInput);
|
|
552
647
|
result = result.replace(/\{field\}/g, field);
|
|
553
648
|
result = result.replace(/\{root\}/g, root);
|
|
@@ -575,14 +670,7 @@ var PatternMatcher = class {
|
|
|
575
670
|
result = result.replace(`{${key}}`, transformedValue ?? "");
|
|
576
671
|
}
|
|
577
672
|
result = result.replace(/\{[a-zA-Z_]+\}/g, "");
|
|
578
|
-
return result.trim();
|
|
579
|
-
}
|
|
580
|
-
/**
|
|
581
|
-
* Infer SpEL field name from capture group
|
|
582
|
-
*/
|
|
583
|
-
inferFieldFromCapture(captured) {
|
|
584
|
-
if (/^[a-zA-Z_]\w*$/.test(captured)) return captured;
|
|
585
|
-
return this.extractChineseField(captured);
|
|
673
|
+
return { expression: result.trim(), unmappedFields };
|
|
586
674
|
}
|
|
587
675
|
};
|
|
588
676
|
|
|
@@ -648,7 +736,7 @@ var BUILTIN_PATTERNS = [
|
|
|
648
736
|
slots: {},
|
|
649
737
|
priority: 98,
|
|
650
738
|
tags: ["null", "isNotNull"],
|
|
651
|
-
examples: [{ nl: "\u5907\u6CE8\u4E0D\u4E3A\u7A7A", spel: "
|
|
739
|
+
examples: [{ nl: "\u5907\u6CE8\u4E0D\u4E3A\u7A7A", spel: "#remark != null" }],
|
|
652
740
|
difficulty: "easy",
|
|
653
741
|
confidence: 0.98
|
|
654
742
|
},
|
|
@@ -659,7 +747,7 @@ var BUILTIN_PATTERNS = [
|
|
|
659
747
|
slots: {},
|
|
660
748
|
priority: 97,
|
|
661
749
|
tags: ["null", "isNull"],
|
|
662
|
-
examples: [{ nl: "\u5907\u6CE8\u4E3A\u7A7A", spel: "
|
|
750
|
+
examples: [{ nl: "\u5907\u6CE8\u4E3A\u7A7A", spel: "#remark == null" }],
|
|
663
751
|
difficulty: "easy",
|
|
664
752
|
confidence: 0.98
|
|
665
753
|
},
|
|
@@ -690,29 +778,29 @@ var BUILTIN_PATTERNS = [
|
|
|
690
778
|
// ================================================================
|
|
691
779
|
{
|
|
692
780
|
id: "CN-CMP-GE",
|
|
693
|
-
match: /^(?<field>[^\s,,、]+?)\s*(
|
|
781
|
+
match: /^(?<field>[^\s,,、]+?)\s*(?:不小于|不低于|大于等于|>=)\s*(?<value>\d+(?:\.\d+)?)/,
|
|
694
782
|
spelTemplate: "#{field} >= {value}",
|
|
695
783
|
slots: { value: { key: "value", type: "number", transform: "toNumber" } },
|
|
696
784
|
priority: 93,
|
|
697
785
|
tags: ["comparison", "ge", "chinese"],
|
|
698
|
-
examples: [{ nl: "\u91D1\u989D\u4E0D\u5C0F\u4E8E100", spel: "
|
|
786
|
+
examples: [{ nl: "\u91D1\u989D\u4E0D\u5C0F\u4E8E100", spel: "#amount >= 100" }],
|
|
699
787
|
difficulty: "easy",
|
|
700
788
|
confidence: 0.95
|
|
701
789
|
},
|
|
702
790
|
{
|
|
703
791
|
id: "CN-CMP-LE",
|
|
704
|
-
match: /^(?<field>[^\s,,、]+?)\s*(
|
|
792
|
+
match: /^(?<field>[^\s,,、]+?)\s*(?:不大于|不超过|小于等于|<=)\s*(?<value>\d+(?:\.\d+)?)/,
|
|
705
793
|
spelTemplate: "#{field} <= {value}",
|
|
706
794
|
slots: { value: { key: "value", type: "number", transform: "toNumber" } },
|
|
707
795
|
priority: 93,
|
|
708
796
|
tags: ["comparison", "le", "chinese"],
|
|
709
|
-
examples: [{ nl: "\u91D1\u989D\u4E0D\u8D85\u8FC7500", spel: "
|
|
797
|
+
examples: [{ nl: "\u91D1\u989D\u4E0D\u8D85\u8FC7500", spel: "#amount <= 500" }],
|
|
710
798
|
difficulty: "easy",
|
|
711
799
|
confidence: 0.95
|
|
712
800
|
},
|
|
713
801
|
{
|
|
714
802
|
id: "CN-CMP-GT",
|
|
715
|
-
match: /^(?<field>[^\s,,、]+?)\s*(
|
|
803
|
+
match: /^(?<field>[^\s,,、]+?)\s*(?:大于|超过|高于)\s*(?<value>\d+(?:\.\d+)?)/,
|
|
716
804
|
spelTemplate: "#{field} > {value}",
|
|
717
805
|
slots: { value: { key: "value", type: "number", transform: "toNumber" } },
|
|
718
806
|
priority: 92,
|
|
@@ -723,7 +811,7 @@ var BUILTIN_PATTERNS = [
|
|
|
723
811
|
},
|
|
724
812
|
{
|
|
725
813
|
id: "CN-CMP-LT",
|
|
726
|
-
match: /^(?<field>[^\s,,、]+?)\s*(
|
|
814
|
+
match: /^(?<field>[^\s,,、]+?)\s*(?:小于|低于|不到)\s*(?<value>\d+(?:\.\d+)?)/,
|
|
727
815
|
spelTemplate: "#{field} < {value}",
|
|
728
816
|
slots: { value: { key: "value", type: "number", transform: "toNumber" } },
|
|
729
817
|
priority: 92,
|
|
@@ -737,7 +825,7 @@ var BUILTIN_PATTERNS = [
|
|
|
737
825
|
// ================================================================
|
|
738
826
|
{
|
|
739
827
|
id: "CN-EQ-STATUS",
|
|
740
|
-
match: /^(?<field>[^\s,,、]+?)\s*(
|
|
828
|
+
match: /^(?<field>[^\s,,、]+?)\s*(?:等于|是|为)\s*(?<value>[^\s,,、]+)$/,
|
|
741
829
|
spelTemplate: "#{field} == '{value}'",
|
|
742
830
|
slots: { value: { key: "value", type: "string" } },
|
|
743
831
|
priority: 85,
|
|
@@ -760,7 +848,7 @@ var BUILTIN_PATTERNS = [
|
|
|
760
848
|
// CN: "order status is not cancelled" — must be before NOT pattern
|
|
761
849
|
{
|
|
762
850
|
id: "CN-NE-STATUS",
|
|
763
|
-
match: /^(?<field>[^\s,,、]+?)\s*(
|
|
851
|
+
match: /^(?<field>[^\s,,、]+?)\s*(?:不等于|!=|不是)\s*(?<value>[^\s,,、]+)$/,
|
|
764
852
|
spelTemplate: "#{field} != '{value}'",
|
|
765
853
|
slots: { value: { key: "value", type: "string" } },
|
|
766
854
|
priority: 91,
|
|
@@ -783,12 +871,12 @@ var BUILTIN_PATTERNS = [
|
|
|
783
871
|
// CN/EN: count equality
|
|
784
872
|
{
|
|
785
873
|
id: "CN-EQ-COUNT",
|
|
786
|
-
match: /^(?<field>[^\s,,、]+?)\s*(
|
|
874
|
+
match: /^(?<field>[^\s,,、]+?)\s*(?:等于|==)\s*(?<value>\d+)/,
|
|
787
875
|
spelTemplate: "#{field} == {value}",
|
|
788
876
|
slots: { value: { key: "value", type: "number", transform: "toNumber" } },
|
|
789
877
|
priority: 91,
|
|
790
878
|
tags: ["comparison", "eq", "number"],
|
|
791
|
-
examples: [{ nl: "\u6570\u91CF\u7B49\u4E8E5", spel: "
|
|
879
|
+
examples: [{ nl: "\u6570\u91CF\u7B49\u4E8E5", spel: "#count == 5" }],
|
|
792
880
|
difficulty: "easy",
|
|
793
881
|
confidence: 0.95
|
|
794
882
|
},
|
|
@@ -830,7 +918,9 @@ var BUILTIN_PATTERNS = [
|
|
|
830
918
|
},
|
|
831
919
|
{
|
|
832
920
|
id: "CN-PERM-PERM",
|
|
833
|
-
|
|
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>.+)$/,
|
|
834
924
|
spelTemplate: "hasPermission('{permission}')",
|
|
835
925
|
slots: { permission: { key: "permission", type: "string" } },
|
|
836
926
|
priority: 88,
|
|
@@ -855,7 +945,7 @@ var BUILTIN_PATTERNS = [
|
|
|
855
945
|
// ================================================================
|
|
856
946
|
{
|
|
857
947
|
id: "CN-COLL-EMPTY",
|
|
858
|
-
match: /^(?<field>[^\s,,、]+?)\s*(
|
|
948
|
+
match: /^(?<field>[^\s,,、]+?)\s*(?:为空|是空的|没有|无)\s*(?:元素|数据|项)?$/,
|
|
859
949
|
spelTemplate: "#{field}.isEmpty()",
|
|
860
950
|
slots: {},
|
|
861
951
|
priority: 89,
|
|
@@ -877,7 +967,7 @@ var BUILTIN_PATTERNS = [
|
|
|
877
967
|
},
|
|
878
968
|
{
|
|
879
969
|
id: "CN-COLL-NOTEMPTY",
|
|
880
|
-
match: /^(?<field>[^\s,,、]+?)\s*(
|
|
970
|
+
match: /^(?<field>[^\s,,、]+?)\s*(?:不为空|有)\s*(?:元素|数据|项)?$/,
|
|
881
971
|
spelTemplate: "!#{field}.isEmpty()",
|
|
882
972
|
slots: {},
|
|
883
973
|
priority: 89,
|
|
@@ -899,7 +989,7 @@ var BUILTIN_PATTERNS = [
|
|
|
899
989
|
},
|
|
900
990
|
{
|
|
901
991
|
id: "CN-COLL-CONTAINS",
|
|
902
|
-
match: /^(?<field>[^\s,,、]+?)\s*(
|
|
992
|
+
match: /^(?<field>[^\s,,、]+?)\s*(?:中\s*)?(?:包含|含有|有)\s*(?<element>[^\s,,、]+)/,
|
|
903
993
|
spelTemplate: "#{field}.contains('{element}')",
|
|
904
994
|
slots: { element: { key: "element", type: "string" } },
|
|
905
995
|
priority: 87,
|
|
@@ -921,7 +1011,7 @@ var BUILTIN_PATTERNS = [
|
|
|
921
1011
|
},
|
|
922
1012
|
{
|
|
923
1013
|
id: "CN-COLL-SIZE",
|
|
924
|
-
match: /^(?<field>[^\s,,、]+?)\s*(
|
|
1014
|
+
match: /^(?<field>[^\s,,、]+?)\s*(?:数量|个数|大小|长度)\s*(?<op>大于|>|超过|小于|<|等于|==)\s*(?<value>\d+)/,
|
|
925
1015
|
spelTemplate: "#{field}.size() {op} {value}",
|
|
926
1016
|
slots: {
|
|
927
1017
|
value: { key: "value", type: "number", transform: "toNumber" },
|
|
@@ -952,7 +1042,7 @@ var BUILTIN_PATTERNS = [
|
|
|
952
1042
|
// ================================================================
|
|
953
1043
|
{
|
|
954
1044
|
id: "CN-STR-CONTAINS",
|
|
955
|
-
match: /^(?<field>[^\s,,、]+?)
|
|
1045
|
+
match: /^(?<field>[^\s,,、]+?)\s*(?:包含|含有|包括)\s*(?<substr>[^\s,,、]+)/,
|
|
956
1046
|
spelTemplate: "#{field}.contains('{substr}')",
|
|
957
1047
|
slots: { substr: { key: "substr", type: "string" } },
|
|
958
1048
|
priority: 85,
|
|
@@ -1001,7 +1091,7 @@ var BUILTIN_PATTERNS = [
|
|
|
1001
1091
|
slots: { suffix: { key: "suffix", type: "string" } },
|
|
1002
1092
|
priority: 85,
|
|
1003
1093
|
tags: ["string", "endsWith"],
|
|
1004
|
-
examples: [{ nl: "\u6587\u4EF6\u540D\u4EE5.pdf\u7ED3\u5C3E", spel: "
|
|
1094
|
+
examples: [{ nl: "\u6587\u4EF6\u540D\u4EE5.pdf\u7ED3\u5C3E", spel: "#name.endsWith('.pdf')" }],
|
|
1005
1095
|
difficulty: "easy",
|
|
1006
1096
|
confidence: 0.95
|
|
1007
1097
|
},
|
|
@@ -1044,21 +1134,21 @@ var BUILTIN_PATTERNS = [
|
|
|
1044
1134
|
{
|
|
1045
1135
|
id: "CN-RANGE-BETWEEN",
|
|
1046
1136
|
match: /^(?<field>[^\s,,、]+?)\s*(?:在|介于)\s*(?<min>\d+)\s*(?:和|到|~)\s*(?<max>\d+)\s*(?:之间|范围)?/,
|
|
1047
|
-
spelTemplate: "#{field} between {{
|
|
1137
|
+
spelTemplate: "#{field} between {{min}, {max}}",
|
|
1048
1138
|
slots: {
|
|
1049
1139
|
min: { key: "min", type: "number", transform: "toNumber" },
|
|
1050
1140
|
max: { key: "max", type: "number", transform: "toNumber" }
|
|
1051
1141
|
},
|
|
1052
1142
|
priority: 82,
|
|
1053
1143
|
tags: ["range", "between"],
|
|
1054
|
-
examples: [{ nl: "\u5E74\u9F84\u572818\u523060\u4E4B\u95F4", spel: "
|
|
1144
|
+
examples: [{ nl: "\u5E74\u9F84\u572818\u523060\u4E4B\u95F4", spel: "#age between {18, 60}" }],
|
|
1055
1145
|
difficulty: "easy",
|
|
1056
1146
|
confidence: 0.95
|
|
1057
1147
|
},
|
|
1058
1148
|
{
|
|
1059
1149
|
id: "EN-RANGE-BETWEEN",
|
|
1060
1150
|
match: /\b(?<field>\w+)\s+between\s+(?<min>\d+)\s+and\s+(?<max>\d+)/i,
|
|
1061
|
-
spelTemplate: "#{field} between {{
|
|
1151
|
+
spelTemplate: "#{field} between {{min}, {max}}",
|
|
1062
1152
|
slots: {
|
|
1063
1153
|
min: { key: "min", type: "number", transform: "toNumber" },
|
|
1064
1154
|
max: { key: "max", type: "number", transform: "toNumber" }
|
|
@@ -1079,7 +1169,7 @@ var BUILTIN_PATTERNS = [
|
|
|
1079
1169
|
slots: { default: { key: "default", type: "string" } },
|
|
1080
1170
|
priority: 78,
|
|
1081
1171
|
tags: ["elvis"],
|
|
1082
|
-
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'" }],
|
|
1083
1173
|
difficulty: "medium",
|
|
1084
1174
|
confidence: 0.85
|
|
1085
1175
|
},
|
|
@@ -1185,7 +1275,11 @@ var BUILTIN_PATTERNS = [
|
|
|
1185
1275
|
// ================================================================
|
|
1186
1276
|
{
|
|
1187
1277
|
id: "CN-BOOL-TRUE",
|
|
1188
|
-
|
|
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*)$/,
|
|
1189
1283
|
spelTemplate: "#{field} == true",
|
|
1190
1284
|
slots: {},
|
|
1191
1285
|
priority: 74,
|
|
@@ -1196,7 +1290,10 @@ var BUILTIN_PATTERNS = [
|
|
|
1196
1290
|
},
|
|
1197
1291
|
{
|
|
1198
1292
|
id: "CN-BOOL-FALSE",
|
|
1199
|
-
|
|
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*)$/,
|
|
1200
1297
|
spelTemplate: "#{field} == false",
|
|
1201
1298
|
slots: {},
|
|
1202
1299
|
priority: 73,
|
|
@@ -1304,7 +1401,7 @@ var BUILTIN_PATTERNS = [
|
|
|
1304
1401
|
// ================================================================
|
|
1305
1402
|
{
|
|
1306
1403
|
id: "CN-LOGIC-NOT",
|
|
1307
|
-
match: /^不是\s
|
|
1404
|
+
match: /^不是\s*(?<expr>.+)/,
|
|
1308
1405
|
spelTemplate: "!({expr})",
|
|
1309
1406
|
slots: { expr: { key: "expr", type: "variable" } },
|
|
1310
1407
|
priority: 62,
|
|
@@ -1329,8 +1426,8 @@ var BUILTIN_PATTERNS = [
|
|
|
1329
1426
|
// ================================================================
|
|
1330
1427
|
{
|
|
1331
1428
|
id: "CN-SELECT-FIRST",
|
|
1332
|
-
match: /^(?<root>[^\s,,、]+?)\s*中\s*第一[个位]\s*(?<field>[^\s,,、]+?)\s*(?:大于|>|超过|<|小于|等于|==)\s*(?<value>[^\s
|
|
1333
|
-
spelTemplate: "#{root}.items.^[#
|
|
1429
|
+
match: /^(?<root>[^\s,,、]+?)\s*中\s*第一[个位]\s*(?<field>[^\s,,、]+?)\s*(?:大于|>|超过|<|小于|等于|==)\s*(?<value>[^\s,,、的]+)/,
|
|
1430
|
+
spelTemplate: "#{root}.items.^[#this.{field} > {value}]",
|
|
1334
1431
|
slots: {
|
|
1335
1432
|
root: { key: "root", type: "variable" },
|
|
1336
1433
|
field: { key: "field", type: "variable" },
|
|
@@ -1338,14 +1435,14 @@ var BUILTIN_PATTERNS = [
|
|
|
1338
1435
|
},
|
|
1339
1436
|
priority: 60,
|
|
1340
1437
|
tags: ["selection", "first"],
|
|
1341
|
-
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]" }],
|
|
1342
1439
|
difficulty: "medium",
|
|
1343
1440
|
confidence: 0.8
|
|
1344
1441
|
},
|
|
1345
1442
|
{
|
|
1346
1443
|
id: "CN-SELECT-ALL",
|
|
1347
|
-
match: /^(?<root>[^\s,,、]+?)\s*中\s*所有\s*(?<field>[^\s,,、]+?)\s*(?:大于|>|超过|<|小于|等于|==|包含|满足)\s*(?<value>[^\s
|
|
1348
|
-
spelTemplate: "#{root}.items.?[#
|
|
1444
|
+
match: /^(?<root>[^\s,,、]+?)\s*中\s*所有\s*(?<field>[^\s,,、]+?)\s*(?:大于|>|超过|<|小于|等于|==|包含|满足)\s*(?<value>[^\s,,、的]+)/,
|
|
1445
|
+
spelTemplate: "#{root}.items.?[#this.{field} > {value}]",
|
|
1349
1446
|
slots: {
|
|
1350
1447
|
root: { key: "root", type: "variable" },
|
|
1351
1448
|
field: { key: "field", type: "variable" },
|
|
@@ -1353,28 +1450,28 @@ var BUILTIN_PATTERNS = [
|
|
|
1353
1450
|
},
|
|
1354
1451
|
priority: 60,
|
|
1355
1452
|
tags: ["selection", "all"],
|
|
1356
|
-
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]" }],
|
|
1357
1454
|
difficulty: "medium",
|
|
1358
1455
|
confidence: 0.8
|
|
1359
1456
|
},
|
|
1360
1457
|
{
|
|
1361
1458
|
id: "CN-PROJ",
|
|
1362
|
-
match: /^(?<root>[^\s,,、]+?)\s*中\s*每[个一]\s*(?:的)?(?<field>[^\s,,、]+?)\s*(?:值|金额|名称|价格|name|amount|price)
|
|
1363
|
-
spelTemplate: "#{root}.items.![#
|
|
1459
|
+
match: /^(?<root>[^\s,,、]+?)\s*中\s*每[个一]\s*(?:的)?(?<field>[^\s,,、]+?)\s*(?:的)?(?:值|金额|名称|价格|name|amount|price)?$/,
|
|
1460
|
+
spelTemplate: "#{root}.items.![#this.{field}]",
|
|
1364
1461
|
slots: {
|
|
1365
1462
|
root: { key: "root", type: "variable" },
|
|
1366
1463
|
field: { key: "field", type: "variable" }
|
|
1367
1464
|
},
|
|
1368
1465
|
priority: 55,
|
|
1369
1466
|
tags: ["projection"],
|
|
1370
|
-
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]" }],
|
|
1371
1468
|
difficulty: "medium",
|
|
1372
1469
|
confidence: 0.75
|
|
1373
1470
|
},
|
|
1374
1471
|
{
|
|
1375
1472
|
id: "EN-SELECT-ALL",
|
|
1376
1473
|
match: /all\s+(?<root>\w+)\s+with\s+(?<field>\w+)\s*>\s*(?<value>\d+)/i,
|
|
1377
|
-
spelTemplate: "#{root}.items.?[#
|
|
1474
|
+
spelTemplate: "#{root}.items.?[#this.{field} > {value}]",
|
|
1378
1475
|
slots: {
|
|
1379
1476
|
root: { key: "root", type: "variable" },
|
|
1380
1477
|
field: { key: "field", type: "variable" },
|
|
@@ -1382,13 +1479,149 @@ var BUILTIN_PATTERNS = [
|
|
|
1382
1479
|
},
|
|
1383
1480
|
priority: 50,
|
|
1384
1481
|
tags: ["selection", "all", "english"],
|
|
1385
|
-
examples: [{ nl: "all items with price > 100", spel: "#
|
|
1482
|
+
examples: [{ nl: "all items with price > 100", spel: "#order.items.?[#this.price > 100]" }],
|
|
1386
1483
|
difficulty: "medium",
|
|
1387
1484
|
confidence: 0.8
|
|
1388
1485
|
}
|
|
1389
1486
|
];
|
|
1390
1487
|
BUILTIN_PATTERNS.sort((a, b) => b.priority - a.priority);
|
|
1391
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
|
+
|
|
1392
1625
|
// src/template/nl-intent.ts
|
|
1393
1626
|
var NLIntent = /* @__PURE__ */ ((NLIntent2) => {
|
|
1394
1627
|
NLIntent2["COMPARISON"] = "COMPARISON";
|
|
@@ -1427,7 +1660,7 @@ var INTENT_KEYWORDS = {
|
|
|
1427
1660
|
en: ["greater", "less", "equal", "above", "below", "exceed", ">", "<", "==", "!=", ">=", "<="]
|
|
1428
1661
|
},
|
|
1429
1662
|
["NULL_CHECK" /* NULL_CHECK */]: {
|
|
1430
|
-
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"],
|
|
1431
1664
|
en: ["null", "empty", "is null", "is not null", "is empty", "is not empty"]
|
|
1432
1665
|
},
|
|
1433
1666
|
["PERMISSION_CHECK" /* PERMISSION_CHECK */]: {
|
|
@@ -1464,7 +1697,10 @@ var INTENT_KEYWORDS = {
|
|
|
1464
1697
|
},
|
|
1465
1698
|
["BOOLEAN" /* BOOLEAN */]: {
|
|
1466
1699
|
zh: ["\u662F\u5426", "\u771F\u5047", "true", "false", "\u662F", "\u5426"],
|
|
1467
|
-
|
|
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"]
|
|
1468
1704
|
},
|
|
1469
1705
|
["DATE" /* DATE */]: {
|
|
1470
1706
|
zh: ["\u65E5\u671F", "\u65F6\u95F4", "\u4E4B\u540E", "\u4E4B\u524D", "\u65E9\u4E8E", "\u665A\u4E8E"],
|
|
@@ -1483,6 +1719,38 @@ var INTENT_KEYWORDS = {
|
|
|
1483
1719
|
en: ["plus", "minus", "multiply", "divide", "mod", "sum", "average"]
|
|
1484
1720
|
}
|
|
1485
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
|
+
}
|
|
1486
1754
|
var IntentClassifier = class {
|
|
1487
1755
|
/**
|
|
1488
1756
|
* Main method for classifying natural language input
|
|
@@ -1516,6 +1784,9 @@ var IntentClassifier = class {
|
|
|
1516
1784
|
if (/isempty|\.isEmpty|列表|数组|集合/.test(normalized)) {
|
|
1517
1785
|
intentScores.set("COLLECTION" /* COLLECTION */, (intentScores.get("COLLECTION" /* COLLECTION */) ?? 0) + 1);
|
|
1518
1786
|
}
|
|
1787
|
+
if (detectNullPredicate(normalized)) {
|
|
1788
|
+
intentScores.set("NULL_CHECK" /* NULL_CHECK */, (intentScores.get("NULL_CHECK" /* NULL_CHECK */) ?? 0) + 2);
|
|
1789
|
+
}
|
|
1519
1790
|
const entities = this.extractEntities(normalized);
|
|
1520
1791
|
const operators = this.extractOperators(normalized);
|
|
1521
1792
|
const logicalConnectors = this.extractLogicalConnectors(normalized);
|
|
@@ -1628,6 +1899,68 @@ var IntentClassifier = class {
|
|
|
1628
1899
|
};
|
|
1629
1900
|
|
|
1630
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
|
+
]);
|
|
1631
1964
|
var TEMPLATE_LIBRARY = {
|
|
1632
1965
|
["COMPARISON" /* COMPARISON */]: [
|
|
1633
1966
|
{
|
|
@@ -1872,8 +2205,9 @@ var TemplateEngine = class {
|
|
|
1872
2205
|
selectBestTemplate(templates, intentResult, input) {
|
|
1873
2206
|
let bestScore = -1;
|
|
1874
2207
|
let bestTemplate = null;
|
|
1875
|
-
const
|
|
1876
|
-
const
|
|
2208
|
+
const polarity = detectNullPredicate(input);
|
|
2209
|
+
const isAffirmative = polarity === "affirmative";
|
|
2210
|
+
const isNegated = polarity === "negated";
|
|
1877
2211
|
for (const template of templates) {
|
|
1878
2212
|
const conditions = template.conditions;
|
|
1879
2213
|
let score = 0;
|
|
@@ -1882,8 +2216,10 @@ var TemplateEngine = class {
|
|
|
1882
2216
|
if (conditions.hasCollection) score += 0.5;
|
|
1883
2217
|
if (conditions.hasNull) score += 0.5;
|
|
1884
2218
|
if (conditions.hasString) score += 1;
|
|
1885
|
-
if (template.name.includes("IS_EMPTY") &&
|
|
1886
|
-
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;
|
|
1887
2223
|
if (conditions.entityCount) {
|
|
1888
2224
|
if (conditions.entityCount.min && intentResult.entities.length < conditions.entityCount.min) {
|
|
1889
2225
|
continue;
|
|
@@ -1899,37 +2235,8 @@ var TemplateEngine = class {
|
|
|
1899
2235
|
fillTemplate(template, input, intentResult) {
|
|
1900
2236
|
let expression = template;
|
|
1901
2237
|
const unfilledSlots = [];
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
if (this.contextSchema?.root) {
|
|
1905
|
-
rootName = this.contextSchema.root.name;
|
|
1906
|
-
const fields = Object.keys(this.contextSchema.root.fields ?? {});
|
|
1907
|
-
for (const f of fields) {
|
|
1908
|
-
if (input.includes(f)) {
|
|
1909
|
-
fieldName = f;
|
|
1910
|
-
break;
|
|
1911
|
-
}
|
|
1912
|
-
}
|
|
1913
|
-
} else {
|
|
1914
|
-
const rootMap = {
|
|
1915
|
-
\u8BA2\u5355: "order",
|
|
1916
|
-
order: "order",
|
|
1917
|
-
\u7528\u6237: "user",
|
|
1918
|
-
user: "user",
|
|
1919
|
-
\u6587\u4EF6: "file",
|
|
1920
|
-
file: "file",
|
|
1921
|
-
\u8D26\u53F7: "account",
|
|
1922
|
-
account: "account",
|
|
1923
|
-
\u5546\u54C1: "item",
|
|
1924
|
-
product: "item"
|
|
1925
|
-
};
|
|
1926
|
-
for (const [key, val] of Object.entries(rootMap)) {
|
|
1927
|
-
if (input.includes(key)) {
|
|
1928
|
-
rootName = val;
|
|
1929
|
-
break;
|
|
1930
|
-
}
|
|
1931
|
-
}
|
|
1932
|
-
}
|
|
2238
|
+
const rootName = this.resolveRootName(input);
|
|
2239
|
+
const fieldName = this.resolveFieldName(input);
|
|
1933
2240
|
expression = expression.replace(/\{root\}/g, rootName);
|
|
1934
2241
|
expression = expression.replace(/\{field\}/g, fieldName);
|
|
1935
2242
|
const fieldEntities = intentResult.entities.filter((e) => e.type === "field");
|
|
@@ -1998,6 +2305,58 @@ var TemplateEngine = class {
|
|
|
1998
2305
|
}
|
|
1999
2306
|
return { expression, unfilledSlots };
|
|
2000
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
|
+
}
|
|
2001
2360
|
};
|
|
2002
2361
|
|
|
2003
2362
|
// src/template/prompts/prompt-builder.ts
|
|
@@ -2235,6 +2594,172 @@ ${userInput}
|
|
|
2235
2594
|
}
|
|
2236
2595
|
};
|
|
2237
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
|
+
|
|
2238
2763
|
// src/validation/validation-pipeline.ts
|
|
2239
2764
|
var ValidationPipeline = class {
|
|
2240
2765
|
evaluator;
|
|
@@ -2254,25 +2779,29 @@ var ValidationPipeline = class {
|
|
|
2254
2779
|
const typeStage = this.validateTypes(expression, contextSchema);
|
|
2255
2780
|
const semanticStage = this.validateSemantic(expression);
|
|
2256
2781
|
const contextStage = this.validateContext(expression, contextSchema);
|
|
2782
|
+
const finalStage = this.validateFinal(expression);
|
|
2257
2783
|
errors.push(
|
|
2258
2784
|
...parseStage.errors,
|
|
2259
2785
|
...typeStage.errors,
|
|
2260
2786
|
...semanticStage.errors,
|
|
2261
|
-
...contextStage.errors
|
|
2787
|
+
...contextStage.errors,
|
|
2788
|
+
...finalStage.errors
|
|
2262
2789
|
);
|
|
2263
2790
|
warnings.push(
|
|
2264
2791
|
...parseStage.warnings,
|
|
2265
2792
|
...typeStage.warnings,
|
|
2266
2793
|
...semanticStage.warnings,
|
|
2267
|
-
...contextStage.warnings
|
|
2794
|
+
...contextStage.warnings,
|
|
2795
|
+
...finalStage.warnings
|
|
2268
2796
|
);
|
|
2269
2797
|
return {
|
|
2270
|
-
valid:
|
|
2798
|
+
valid: errors.length === 0,
|
|
2271
2799
|
stages: {
|
|
2272
2800
|
parse: parseStage,
|
|
2273
2801
|
type: typeStage,
|
|
2274
2802
|
semantic: semanticStage,
|
|
2275
|
-
context: contextStage
|
|
2803
|
+
context: contextStage,
|
|
2804
|
+
final: finalStage
|
|
2276
2805
|
},
|
|
2277
2806
|
errors,
|
|
2278
2807
|
warnings
|
|
@@ -2280,47 +2809,57 @@ var ValidationPipeline = class {
|
|
|
2280
2809
|
}
|
|
2281
2810
|
/**
|
|
2282
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.
|
|
2283
2816
|
*/
|
|
2284
2817
|
async validateParse(expression) {
|
|
2285
2818
|
const errors = [];
|
|
2286
2819
|
const warnings = [];
|
|
2820
|
+
const structural = maskStringLiterals(expression);
|
|
2287
2821
|
if (!expression || expression.trim().length === 0) {
|
|
2288
2822
|
errors.push({
|
|
2289
2823
|
code: "PARSE-EMPTY",
|
|
2290
2824
|
message: "Expression is empty",
|
|
2825
|
+
severity: "error",
|
|
2291
2826
|
stage: "parse",
|
|
2292
2827
|
requiresLLM: true
|
|
2293
2828
|
});
|
|
2294
2829
|
return { passed: false, errors, warnings };
|
|
2295
2830
|
}
|
|
2296
|
-
if (!this.hasBalancedParentheses(
|
|
2831
|
+
if (!this.hasBalancedParentheses(structural)) {
|
|
2297
2832
|
errors.push({
|
|
2298
2833
|
code: "PARSE-UNBALANCED_PARENS",
|
|
2299
2834
|
message: "Unbalanced parentheses in expression",
|
|
2835
|
+
severity: "error",
|
|
2300
2836
|
stage: "parse",
|
|
2301
2837
|
requiresLLM: true
|
|
2302
2838
|
});
|
|
2303
2839
|
}
|
|
2304
|
-
if (
|
|
2840
|
+
if (structural.includes("===") || structural.includes("!==")) {
|
|
2305
2841
|
errors.push({
|
|
2306
2842
|
code: "PARSE-JS_OPERATOR",
|
|
2307
2843
|
message: "JavaScript operators detected (=== or !==), use == or != in SpEL",
|
|
2844
|
+
severity: "error",
|
|
2308
2845
|
stage: "parse",
|
|
2309
2846
|
requiresLLM: true
|
|
2310
2847
|
});
|
|
2311
2848
|
}
|
|
2312
|
-
if (
|
|
2849
|
+
if (structural.includes("&&")) {
|
|
2313
2850
|
errors.push({
|
|
2314
2851
|
code: "PARSE-JS_LOGIC",
|
|
2315
2852
|
message: 'JavaScript && detected, use "and" in SpEL',
|
|
2853
|
+
severity: "error",
|
|
2316
2854
|
stage: "parse",
|
|
2317
2855
|
requiresLLM: true
|
|
2318
2856
|
});
|
|
2319
2857
|
}
|
|
2320
|
-
if (
|
|
2858
|
+
if (structural.includes("||")) {
|
|
2321
2859
|
errors.push({
|
|
2322
2860
|
code: "PARSE-JS_LOGIC",
|
|
2323
2861
|
message: 'JavaScript || detected, use "or" in SpEL',
|
|
2862
|
+
severity: "error",
|
|
2324
2863
|
stage: "parse",
|
|
2325
2864
|
requiresLLM: true
|
|
2326
2865
|
});
|
|
@@ -2333,6 +2872,7 @@ var ValidationPipeline = class {
|
|
|
2333
2872
|
errors.push({
|
|
2334
2873
|
code: `PARSE-${pe.code ?? "SYNTAX"}`,
|
|
2335
2874
|
message: pe.message,
|
|
2875
|
+
severity: "error",
|
|
2336
2876
|
position: pe.position,
|
|
2337
2877
|
stage: "parse",
|
|
2338
2878
|
requiresLLM: true
|
|
@@ -2343,6 +2883,7 @@ var ValidationPipeline = class {
|
|
|
2343
2883
|
errors.push({
|
|
2344
2884
|
code: "PARSE-EXCEPTION",
|
|
2345
2885
|
message: `Parse threw exception: ${err.message}`,
|
|
2886
|
+
severity: "error",
|
|
2346
2887
|
stage: "parse",
|
|
2347
2888
|
requiresLLM: true
|
|
2348
2889
|
});
|
|
@@ -2355,27 +2896,43 @@ var ValidationPipeline = class {
|
|
|
2355
2896
|
};
|
|
2356
2897
|
}
|
|
2357
2898
|
/**
|
|
2358
|
-
* Stage 2: Type Check — type validation
|
|
2899
|
+
* Stage 2: Type Check — advisory type validation
|
|
2359
2900
|
*/
|
|
2360
2901
|
validateTypes(expression, contextSchema) {
|
|
2361
2902
|
const errors = [];
|
|
2362
2903
|
const warnings = [];
|
|
2363
|
-
const
|
|
2904
|
+
const structural = maskStringLiterals(expression);
|
|
2905
|
+
const strNumMismatch = /'(?:\\.|[^'\\])*'\s*(?:>|<|>=|<=)\s*\d+|\d+\s*(?:>|<|>=|<=)\s*'(?:\\.|[^'\\])*'/;
|
|
2364
2906
|
if (strNumMismatch.test(expression)) {
|
|
2365
2907
|
warnings.push({
|
|
2366
2908
|
code: "TYPE-STR_NUM_CMP",
|
|
2367
2909
|
message: "String literal compared with number using arithmetic operator",
|
|
2910
|
+
severity: "warning",
|
|
2368
2911
|
stage: "type"
|
|
2369
2912
|
});
|
|
2370
2913
|
}
|
|
2371
2914
|
if (contextSchema?.root) {
|
|
2915
|
+
const rootRef = escapeRegExp(contextSchema.root.name);
|
|
2372
2916
|
for (const [fieldName, field] of Object.entries(contextSchema.root.fields)) {
|
|
2917
|
+
const fieldRef = `#(?:${rootRef}\\.)?${escapeRegExp(fieldName)}`;
|
|
2373
2918
|
if (field.type === "boolean") {
|
|
2374
|
-
const boolNumPattern = new RegExp(
|
|
2375
|
-
if (boolNumPattern.test(
|
|
2919
|
+
const boolNumPattern = new RegExp(`${fieldRef}\\s*(?:>|<|>=|<=)\\s*\\d+`);
|
|
2920
|
+
if (boolNumPattern.test(structural)) {
|
|
2376
2921
|
warnings.push({
|
|
2377
2922
|
code: "TYPE-BOOL_NUM_CMP",
|
|
2378
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",
|
|
2379
2936
|
stage: "type"
|
|
2380
2937
|
});
|
|
2381
2938
|
}
|
|
@@ -2389,23 +2946,26 @@ var ValidationPipeline = class {
|
|
|
2389
2946
|
};
|
|
2390
2947
|
}
|
|
2391
2948
|
/**
|
|
2392
|
-
* Stage 3: Semantic Check — semantic
|
|
2949
|
+
* Stage 3: Semantic Check — advisory semantic validation
|
|
2393
2950
|
*/
|
|
2394
2951
|
validateSemantic(expression) {
|
|
2395
2952
|
const errors = [];
|
|
2396
2953
|
const warnings = [];
|
|
2954
|
+
const structural = maskStringLiterals(expression);
|
|
2397
2955
|
const selfCompare = /(#\w+(?:\.\w+)*)\s*==\s*\1/;
|
|
2398
|
-
if (selfCompare.test(
|
|
2956
|
+
if (selfCompare.test(structural)) {
|
|
2399
2957
|
warnings.push({
|
|
2400
2958
|
code: "SEM-SELF_COMPARE",
|
|
2401
2959
|
message: "Self-comparison detected: expression is always true",
|
|
2960
|
+
severity: "warning",
|
|
2402
2961
|
stage: "semantic"
|
|
2403
2962
|
});
|
|
2404
2963
|
}
|
|
2405
|
-
if (
|
|
2964
|
+
if (structural.includes("!!")) {
|
|
2406
2965
|
warnings.push({
|
|
2407
2966
|
code: "SEM-DOUBLE_NEGATION",
|
|
2408
2967
|
message: "Double negation detected, consider simplifying",
|
|
2968
|
+
severity: "warning",
|
|
2409
2969
|
stage: "semantic"
|
|
2410
2970
|
});
|
|
2411
2971
|
}
|
|
@@ -2417,59 +2977,72 @@ var ValidationPipeline = class {
|
|
|
2417
2977
|
}
|
|
2418
2978
|
/**
|
|
2419
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.
|
|
2420
2984
|
*/
|
|
2421
2985
|
validateContext(expression, contextSchema) {
|
|
2422
2986
|
const errors = [];
|
|
2423
2987
|
const warnings = [];
|
|
2988
|
+
const structural = maskStringLiterals(expression);
|
|
2424
2989
|
if (!contextSchema) {
|
|
2425
2990
|
warnings.push({
|
|
2426
2991
|
code: "CTX-NO_SCHEMA",
|
|
2427
2992
|
message: "No ContextSchema provided, skipping context validation",
|
|
2993
|
+
severity: "warning",
|
|
2428
2994
|
stage: "context"
|
|
2429
2995
|
});
|
|
2430
2996
|
return { passed: true, errors, warnings };
|
|
2431
2997
|
}
|
|
2432
|
-
const
|
|
2433
|
-
|
|
2434
|
-
|
|
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) {
|
|
2435
3005
|
for (const ref of refs) {
|
|
2436
|
-
if (ref.startsWith(`#${
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
}
|
|
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
|
+
});
|
|
2446
3016
|
}
|
|
2447
3017
|
}
|
|
2448
3018
|
}
|
|
2449
3019
|
for (const ref of refs) {
|
|
2450
|
-
if (ref.startsWith("#")
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
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
|
+
});
|
|
2462
3033
|
}
|
|
2463
3034
|
}
|
|
2464
|
-
const beanMatch =
|
|
2465
|
-
if (beanMatch
|
|
3035
|
+
const beanMatch = structural.match(/@(\w+)/g);
|
|
3036
|
+
if (beanMatch) {
|
|
2466
3037
|
for (const b of beanMatch) {
|
|
2467
3038
|
const beanName = b.slice(1);
|
|
2468
|
-
if (!(beanName in
|
|
2469
|
-
|
|
3039
|
+
if (!(beanName in beans)) {
|
|
3040
|
+
errors.push({
|
|
2470
3041
|
code: "CTX-UNKNOWN_BEAN",
|
|
2471
3042
|
message: `Bean '${beanName}' not found in ContextSchema`,
|
|
2472
|
-
|
|
3043
|
+
severity: "error",
|
|
3044
|
+
stage: "context",
|
|
3045
|
+
requiresLLM: true
|
|
2473
3046
|
});
|
|
2474
3047
|
}
|
|
2475
3048
|
}
|
|
@@ -2481,7 +3054,62 @@ var ValidationPipeline = class {
|
|
|
2481
3054
|
};
|
|
2482
3055
|
}
|
|
2483
3056
|
/**
|
|
2484
|
-
* 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.
|
|
2485
3113
|
*/
|
|
2486
3114
|
hasBalancedParentheses(expression) {
|
|
2487
3115
|
const stack = [];
|
|
@@ -2497,17 +3125,29 @@ var ValidationPipeline = class {
|
|
|
2497
3125
|
return stack.length === 0;
|
|
2498
3126
|
}
|
|
2499
3127
|
/**
|
|
2500
|
-
* 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.
|
|
2501
3133
|
*/
|
|
2502
3134
|
extractReferences(expression) {
|
|
2503
3135
|
const refs = [];
|
|
3136
|
+
const dottedHeads = /* @__PURE__ */ new Set();
|
|
2504
3137
|
const varMatch = expression.matchAll(/#(\w+(?:\.\w+(?:\.\w+)?)?)/g);
|
|
2505
3138
|
for (const m of varMatch) {
|
|
2506
|
-
|
|
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
|
+
}
|
|
2507
3146
|
}
|
|
2508
3147
|
const simpleMatch = expression.matchAll(/#(\w+)(?!\w*\()/g);
|
|
2509
3148
|
for (const m of simpleMatch) {
|
|
2510
3149
|
const ref = `#${m[1]}`;
|
|
3150
|
+
if (dottedHeads.has(ref)) continue;
|
|
2511
3151
|
if (!refs.includes(ref)) {
|
|
2512
3152
|
refs.push(ref);
|
|
2513
3153
|
}
|
|
@@ -2515,98 +3155,68 @@ var ValidationPipeline = class {
|
|
|
2515
3155
|
return refs;
|
|
2516
3156
|
}
|
|
2517
3157
|
};
|
|
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
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
fixed = fixed.replace(/\?\s*:\s*/g, " ?: ");
|
|
2581
|
-
changes.push("Fixed Elvis operator spacing");
|
|
2582
|
-
}
|
|
2583
|
-
const wasFixed = changes.length > 0;
|
|
2584
|
-
return {
|
|
2585
|
-
wasFixed,
|
|
2586
|
-
expression: wasFixed ? fixed : expression,
|
|
2587
|
-
changes
|
|
2588
|
-
};
|
|
2589
|
-
}
|
|
2590
|
-
fixAllBrackets(expr) {
|
|
2591
|
-
let result = expr;
|
|
2592
|
-
const openParen = (result.match(/\(/g) ?? []).length;
|
|
2593
|
-
const closeParen = (result.match(/\)/g) ?? []).length;
|
|
2594
|
-
if (openParen > closeParen) {
|
|
2595
|
-
result += ")".repeat(openParen - closeParen);
|
|
2596
|
-
}
|
|
2597
|
-
const openBracket = (result.match(/\[/g) ?? []).length;
|
|
2598
|
-
const closeBracket = (result.match(/\]/g) ?? []).length;
|
|
2599
|
-
if (openBracket > closeBracket) {
|
|
2600
|
-
result += "]".repeat(openBracket - closeBracket);
|
|
2601
|
-
}
|
|
2602
|
-
const openBrace = (result.match(/\{/g) ?? []).length;
|
|
2603
|
-
const closeBrace = (result.match(/\}/g) ?? []).length;
|
|
2604
|
-
if (openBrace > closeBrace) {
|
|
2605
|
-
result += "}".repeat(openBrace - closeBrace);
|
|
2606
|
-
}
|
|
2607
|
-
return result;
|
|
2608
|
-
}
|
|
2609
|
-
};
|
|
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
|
+
}
|
|
2610
3220
|
|
|
2611
3221
|
// src/validation/self-correction-loop.ts
|
|
2612
3222
|
var SelfCorrectionLoop = class {
|
|
@@ -2764,20 +3374,23 @@ var StrategyRouter = class {
|
|
|
2764
3374
|
if (contextSchema) {
|
|
2765
3375
|
this.templateEngine.setContext(contextSchema);
|
|
2766
3376
|
}
|
|
2767
|
-
const
|
|
2768
|
-
|
|
2769
|
-
|
|
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);
|
|
2770
3383
|
if (validation.valid) {
|
|
2771
3384
|
return {
|
|
2772
|
-
expression:
|
|
3385
|
+
expression: wholeMatch.spel,
|
|
2773
3386
|
strategy: "pattern",
|
|
2774
|
-
confidence:
|
|
2775
|
-
metadata: { patternId:
|
|
3387
|
+
confidence: wholeMatch.confidence,
|
|
3388
|
+
metadata: { patternId: wholeMatch.pattern?.id },
|
|
2776
3389
|
latencyMs: Date.now() - startTime
|
|
2777
3390
|
};
|
|
2778
3391
|
}
|
|
2779
3392
|
try {
|
|
2780
|
-
const afResult = this.autoFixer.fix(
|
|
3393
|
+
const afResult = this.autoFixer.fix(wholeMatch.spel);
|
|
2781
3394
|
if (afResult.wasFixed) {
|
|
2782
3395
|
const afValidation = await this.validationPipeline.validate(
|
|
2783
3396
|
afResult.expression,
|
|
@@ -2787,8 +3400,8 @@ var StrategyRouter = class {
|
|
|
2787
3400
|
return {
|
|
2788
3401
|
expression: afResult.expression,
|
|
2789
3402
|
strategy: "pattern",
|
|
2790
|
-
confidence:
|
|
2791
|
-
metadata: { patternId:
|
|
3403
|
+
confidence: wholeMatch.confidence * 0.95,
|
|
3404
|
+
metadata: { patternId: wholeMatch.pattern?.id },
|
|
2792
3405
|
latencyMs: Date.now() - startTime
|
|
2793
3406
|
};
|
|
2794
3407
|
}
|
|
@@ -2796,6 +3409,30 @@ var StrategyRouter = class {
|
|
|
2796
3409
|
} catch {
|
|
2797
3410
|
}
|
|
2798
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
|
+
}
|
|
2799
3436
|
const intentResult = this.intentClassifier.classify(nl);
|
|
2800
3437
|
let templateResult = null;
|
|
2801
3438
|
try {
|
|
@@ -2829,7 +3466,7 @@ var StrategyRouter = class {
|
|
|
2829
3466
|
}
|
|
2830
3467
|
}
|
|
2831
3468
|
if (providers.length === 0) {
|
|
2832
|
-
throw new Error("No LLM providers available");
|
|
3469
|
+
throw clauseFailure ?? new Error("No LLM providers available");
|
|
2833
3470
|
}
|
|
2834
3471
|
let lastError = null;
|
|
2835
3472
|
for (const provider of providers) {
|
|
@@ -2875,6 +3512,33 @@ var StrategyRouter = class {
|
|
|
2875
3512
|
/**
|
|
2876
3513
|
* Get PatternMatcher (for external testing/debugging)
|
|
2877
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
|
+
}
|
|
2878
3542
|
getPatternMatcher() {
|
|
2879
3543
|
return this.patternMatcher;
|
|
2880
3544
|
}
|
|
@@ -2958,6 +3622,19 @@ var NL2SpelEngine = class {
|
|
|
2958
3622
|
if (options.offlineOnly) {
|
|
2959
3623
|
const patternMatcher = this.router.getPatternMatcher();
|
|
2960
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
|
+
}
|
|
2961
3638
|
if (patternResult.matched) {
|
|
2962
3639
|
return {
|
|
2963
3640
|
expression: patternResult.spel,
|
|
@@ -3036,5 +3713,9 @@ var NL2SpelEngine = class {
|
|
|
3036
3713
|
SelfCorrectionLoop,
|
|
3037
3714
|
StrategyRouter,
|
|
3038
3715
|
TemplateEngine,
|
|
3039
|
-
|
|
3716
|
+
UnconvertibleClauseError,
|
|
3717
|
+
UnmappedFieldError,
|
|
3718
|
+
ValidationPipeline,
|
|
3719
|
+
decompose,
|
|
3720
|
+
splitClauses
|
|
3040
3721
|
});
|