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