@rudra-js/core 0.7.3 → 0.7.5
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 +113 -22
- package/dist/claim-screening.d.ts +3 -3
- package/dist/claim-screening.d.ts.map +1 -1
- package/dist/claim-screening.js +37 -102
- package/dist/claim-screening.js.map +1 -1
- package/dist/component-generator.d.ts.map +1 -1
- package/dist/component-generator.js +161 -176
- package/dist/component-generator.js.map +1 -1
- package/dist/component-spec.d.ts +1 -0
- package/dist/component-spec.d.ts.map +1 -1
- package/dist/component-spec.js +3 -0
- package/dist/component-spec.js.map +1 -1
- package/dist/fallback-component.d.ts +1 -1
- package/dist/fallback-component.d.ts.map +1 -1
- package/dist/fallback-component.js +9 -17
- package/dist/fallback-component.js.map +1 -1
- package/dist/fit-to-shopper.d.ts.map +1 -1
- package/dist/fit-to-shopper.js +19 -19
- package/dist/fit-to-shopper.js.map +1 -1
- package/dist/model-prompt.d.ts.map +1 -1
- package/dist/model-prompt.js +56 -39
- package/dist/model-prompt.js.map +1 -1
- package/dist/product-selection.d.ts.map +1 -1
- package/dist/product-selection.js +15 -31
- package/dist/product-selection.js.map +1 -1
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js.map +1 -1
- package/dist/reconciliation.d.ts +3 -3
- package/dist/reconciliation.d.ts.map +1 -1
- package/dist/reconciliation.js +133 -171
- package/dist/reconciliation.js.map +1 -1
- package/dist/signal-digest.d.ts +1 -1
- package/dist/signal-digest.d.ts.map +1 -1
- package/dist/signal-digest.js +51 -52
- package/dist/signal-digest.js.map +1 -1
- package/dist/spec-cache.d.ts +0 -4
- package/dist/spec-cache.d.ts.map +1 -1
- package/dist/spec-cache.js +15 -35
- package/dist/spec-cache.js.map +1 -1
- package/dist/tracking-input.d.ts.map +1 -1
- package/dist/tracking-input.js +16 -19
- package/dist/tracking-input.js.map +1 -1
- package/package.json +3 -3
- package/src/claim-screening.ts +41 -125
- package/src/component-generator.ts +223 -220
- package/src/component-spec.ts +4 -15
- package/src/fallback-component.ts +11 -20
- package/src/fit-to-shopper.ts +19 -21
- package/src/model-prompt.ts +55 -54
- package/src/product-selection.ts +21 -50
- package/src/provider.ts +0 -4
- package/src/reconciliation.ts +159 -268
- package/src/signal-digest.ts +50 -79
- package/src/spec-cache.ts +15 -48
- package/src/tracking-input.ts +21 -36
package/src/claim-screening.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { foldLookalikes, verify } from '@rudra-js/attested';
|
|
1
2
|
import { offeredCandidates } from './model-prompt.js';
|
|
2
|
-
import type {
|
|
3
|
+
import type { TrackingInput } from './tracking-input.js';
|
|
3
4
|
|
|
4
5
|
const CLAIM_PATTERNS: { kind: string; patterns: RegExp[] }[] = [
|
|
5
6
|
{
|
|
@@ -8,18 +9,13 @@ const CLAIM_PATTERNS: { kind: string; patterns: RegExp[] }[] = [
|
|
|
8
9
|
/\breviews?\b|\breviewed\b/,
|
|
9
10
|
/\b(?:\d+(?:\.\d+)?|three|four|five)[\s-]?stars?\b/,
|
|
10
11
|
/\bstars?[\s-]?ratings?\b/,
|
|
11
|
-
|
|
12
12
|
/\b(?:customer|shopper|buyer|user|average|overall)[\s-]ratings?\b/,
|
|
13
|
-
|
|
14
13
|
/\bratings?\s+of\s+[0-5]\.\d\b/,
|
|
15
|
-
|
|
16
14
|
/\b[0-5](?:\.\d)?\s+out of\s+(?:5|five)\b/,
|
|
17
15
|
/\b(?:highly|top|best|well|poorly|five|four)[\s-]rated\b/,
|
|
18
|
-
|
|
19
|
-
/\brated\s+(?:[0-5]
|
|
20
|
-
|
|
16
|
+
/\brated\s+[0-5]\.\d\b/,
|
|
17
|
+
/\brated\s+(?:[0-5]|three|four|five)\s+(?:stars?|out of)\b/,
|
|
21
18
|
/\bbest[\s-]?sell(?:er|ers|ing)\b/,
|
|
22
|
-
|
|
23
19
|
/(?:\bnumber one|\bno\.? ?1|#\s?1)[\s-]?sell(?:er|ers|ing)\b/,
|
|
24
20
|
/\bloved by (?:thousands|hundreds|millions|\d)/,
|
|
25
21
|
],
|
|
@@ -29,22 +25,15 @@ const CLAIM_PATTERNS: { kind: string; patterns: RegExp[] }[] = [
|
|
|
29
25
|
patterns: [
|
|
30
26
|
/[$£€¥₹]/,
|
|
31
27
|
/\b\d+(?:\.\d+)?\s?(?:usd|eur|gbp|dollars?|pounds?|euros?)\b/,
|
|
32
|
-
|
|
33
28
|
/\bdollars?\b|\beuros?\b/,
|
|
34
|
-
|
|
35
29
|
/\b(?:usd|eur|gbp)\s?\d/,
|
|
36
|
-
|
|
37
30
|
/\bkr\s?\d|\d\s?kr\b/,
|
|
38
31
|
/\bpric(?:e|es|ed|ing)\b/,
|
|
39
|
-
|
|
40
32
|
/\bwas\s+[$£€¥]?\s?\d[\d,.]*\s*[,;–—-]?\s*now\s+[$£€¥]?\s?\d/,
|
|
41
|
-
|
|
42
33
|
/\bcheap(?:er|est)\b/,
|
|
43
34
|
/\baffordable\b|\bbargain\b/,
|
|
44
|
-
|
|
45
35
|
/\bcosts?\s+(?:less|more|only|just|about|around|[$£€¥]?\d)/,
|
|
46
36
|
/\blow(?:er)?[\s-]cost\b/,
|
|
47
|
-
|
|
48
37
|
/\bsaves?\s+(?:you\s+)?(?:money|cash|[$£€¥]\s?\d)/,
|
|
49
38
|
],
|
|
50
39
|
},
|
|
@@ -56,12 +45,12 @@ const CLAIM_PATTERNS: { kind: string; patterns: RegExp[] }[] = [
|
|
|
56
45
|
/\bdiscount(?:s|ed)?\b/,
|
|
57
46
|
/\bsale\b|\bmarked down\b|\bdeal of\b/,
|
|
58
47
|
/\bhalf[\s-]?(?:price|off)\b/,
|
|
59
|
-
|
|
60
48
|
/\bsaves?\s+\d[\d.,]*\s+off\b/,
|
|
61
|
-
|
|
62
49
|
/\bclearance\s+(?:sale|price|event|deal)\b|\bon clearance\b/,
|
|
63
|
-
|
|
64
|
-
/\
|
|
50
|
+
/\bprice reduced\b/,
|
|
51
|
+
/\breduced price\b/,
|
|
52
|
+
/\breduced by \d+\s?(?:%|percent)/,
|
|
53
|
+
/\breduced\s+(?:this|next|last)\s+week\b/,
|
|
65
54
|
],
|
|
66
55
|
},
|
|
67
56
|
{
|
|
@@ -80,110 +69,24 @@ const CLAIM_PATTERNS: { kind: string; patterns: RegExp[] }[] = [
|
|
|
80
69
|
/\b(?:in|out of|low on) stock\b/,
|
|
81
70
|
/\blimited stock\b/,
|
|
82
71
|
/\brestocked?\b|\bsold out\b/,
|
|
83
|
-
|
|
84
72
|
/\b(?:only\s+)?(?:\d+|a few|a handful|a couple|one|few)\s+(?:left|remain(?:s|ing)?)\b/,
|
|
73
|
+
/\bonly\s+(?:two|three|four|five|six|seven|eight|nine|ten)\s+(?:left|remain(?:s|ing)?)\b/,
|
|
85
74
|
/\bselling fast\b|\b(?:almost|nearly) gone\b|\bwhile stocks last\b/,
|
|
86
75
|
],
|
|
87
76
|
},
|
|
88
77
|
];
|
|
89
78
|
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
н: 'h',
|
|
97
|
-
о: 'o',
|
|
98
|
-
р: 'p',
|
|
99
|
-
с: 'c',
|
|
100
|
-
т: 't',
|
|
101
|
-
х: 'x',
|
|
102
|
-
у: 'y',
|
|
103
|
-
ѕ: 's',
|
|
104
|
-
і: 'i',
|
|
105
|
-
ј: 'j',
|
|
106
|
-
ԁ: 'd',
|
|
107
|
-
һ: 'h',
|
|
108
|
-
ӏ: 'l',
|
|
109
|
-
α: 'a',
|
|
110
|
-
ε: 'e',
|
|
111
|
-
ι: 'i',
|
|
112
|
-
κ: 'k',
|
|
113
|
-
ν: 'v',
|
|
114
|
-
ο: 'o',
|
|
115
|
-
ρ: 'p',
|
|
116
|
-
τ: 't',
|
|
117
|
-
υ: 'u',
|
|
118
|
-
χ: 'x',
|
|
119
|
-
ϳ: 'j',
|
|
120
|
-
ѵ: 'v',
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
// Capitals that lowercasing reads wrong: Ν becomes ν, which reads as v.
|
|
124
|
-
const CAPITALS: Record<string, string> = {
|
|
125
|
-
Β: 'b',
|
|
126
|
-
Ζ: 'z',
|
|
127
|
-
Η: 'h',
|
|
128
|
-
Μ: 'm',
|
|
129
|
-
Ν: 'n',
|
|
130
|
-
Υ: 'y',
|
|
131
|
-
Ү: 'y',
|
|
132
|
-
Ӏ: 'i',
|
|
133
|
-
Ԛ: 'q',
|
|
134
|
-
Ԝ: 'w',
|
|
135
|
-
Ϻ: 'm',
|
|
136
|
-
Ϝ: 'f',
|
|
137
|
-
Ԍ: 'g',
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
const INVISIBLE = /[\p{Cf}\p{Default_Ignorable_Code_Point}\p{Cc}]/gu;
|
|
141
|
-
|
|
142
|
-
const SPACING = /[\t\n\v\f\r\u0085]/;
|
|
143
|
-
|
|
144
|
-
const MARKS = /[\p{Mn}\p{Me}]/gu;
|
|
145
|
-
|
|
146
|
-
function normaliseForClaims(text: string, byLook: boolean): string {
|
|
147
|
-
// NFKC would turn the lunate sigmas into ς and Σ, which the fold can't read as c.
|
|
148
|
-
const compatible = text
|
|
149
|
-
.replace(INVISIBLE, (char) => (SPACING.test(char) ? char : ''))
|
|
150
|
-
.replace(/[ϲϹ]/g, 'c')
|
|
151
|
-
.normalize('NFKC');
|
|
152
|
-
|
|
153
|
-
let cased = compatible;
|
|
154
|
-
if (byLook) {
|
|
155
|
-
cased = '';
|
|
156
|
-
for (const char of compatible) cased += CAPITALS[char] ?? char;
|
|
157
|
-
}
|
|
158
|
-
const lowered = cased.toLowerCase().normalize('NFC').replace(MARKS, '');
|
|
159
|
-
|
|
160
|
-
let folded = '';
|
|
161
|
-
for (const char of lowered) folded += CONFUSABLES[char] ?? char;
|
|
162
|
-
return folded;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
export const ALLOWED_PHRASES = ['does not feel cheap', "doesn't feel cheap", 'last few miles'];
|
|
166
|
-
|
|
167
|
-
const DIGIT = /\p{Nd}/u;
|
|
168
|
-
|
|
169
|
-
const BARE_EXPONENT = /^[+-]?\d+(?:\.\d+)?[eE]([+-]?\d+)$/;
|
|
170
|
-
|
|
171
|
-
const MAX_EXPONENT = 1000;
|
|
79
|
+
export const ALLOWED_PHRASES = [
|
|
80
|
+
'does not feel cheap',
|
|
81
|
+
"doesn't feel cheap",
|
|
82
|
+
'last few miles',
|
|
83
|
+
'sustainable pace',
|
|
84
|
+
];
|
|
172
85
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
if (match === null) return true;
|
|
176
|
-
return Math.abs(Number(match[1] ?? '')) <= MAX_EXPONENT;
|
|
177
|
-
}
|
|
86
|
+
const FACT_DIGIT = /\p{Nd}/u;
|
|
87
|
+
const NUMERAL = /[\p{Nd}\p{No}\p{Nl}]/u;
|
|
178
88
|
|
|
179
|
-
export
|
|
180
|
-
const facts: string[] = [];
|
|
181
|
-
if (DIGIT.test(product.category) && canLayOut(product.category)) facts.push(product.category);
|
|
182
|
-
for (const tag of product.tags) {
|
|
183
|
-
if (DIGIT.test(tag) && canLayOut(tag)) facts.push(tag);
|
|
184
|
-
}
|
|
185
|
-
return facts;
|
|
186
|
-
}
|
|
89
|
+
export const NO_FACTS: readonly string[] = [];
|
|
187
90
|
|
|
188
91
|
export interface HostFacts {
|
|
189
92
|
pooled: string[];
|
|
@@ -195,7 +98,7 @@ export function hostFacts(input: TrackingInput): HostFacts {
|
|
|
195
98
|
const bySku = new Map<string, string[]>();
|
|
196
99
|
|
|
197
100
|
for (const product of offeredCandidates(input)) {
|
|
198
|
-
const own =
|
|
101
|
+
const own = [product.category, ...product.tags].filter((text) => FACT_DIGIT.test(text));
|
|
199
102
|
bySku.set(product.sku, own);
|
|
200
103
|
for (const fact of own) pooled.add(fact);
|
|
201
104
|
}
|
|
@@ -203,15 +106,28 @@ export function hostFacts(input: TrackingInput): HostFacts {
|
|
|
203
106
|
return { pooled: [...pooled], bySku };
|
|
204
107
|
}
|
|
205
108
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
const normalised = normaliseForClaims(text, byLook);
|
|
210
|
-
for (const claim of CLAIM_PATTERNS) {
|
|
211
|
-
for (const pattern of claim.patterns) {
|
|
212
|
-
if (pattern.test(normalised)) return claim.kind;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
109
|
+
function kindIn(reading: string): string | null {
|
|
110
|
+
for (const claim of CLAIM_PATTERNS) {
|
|
111
|
+
if (claim.patterns.some((pattern) => pattern.test(reading))) return claim.kind;
|
|
215
112
|
}
|
|
216
113
|
return null;
|
|
217
114
|
}
|
|
115
|
+
|
|
116
|
+
function claimIn(text: string): string | null {
|
|
117
|
+
return kindIn(foldLookalikes(text)) ?? kindIn(foldLookalikes(text, true));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function unbackedClaim(
|
|
121
|
+
raw: string,
|
|
122
|
+
clamped: string,
|
|
123
|
+
facts: readonly string[],
|
|
124
|
+
): string | null {
|
|
125
|
+
const kind = claimIn(raw) ?? claimIn(clamped);
|
|
126
|
+
if (kind !== null) return kind;
|
|
127
|
+
|
|
128
|
+
const values = NUMERAL.test(clamped) ? facts : NO_FACTS;
|
|
129
|
+
const result = verify(clamped, { values, allowedPhrases: ALLOWED_PHRASES });
|
|
130
|
+
if (!result.quantity.supported) return 'quantity';
|
|
131
|
+
if (!result.wording.supported) return 'wording';
|
|
132
|
+
return null;
|
|
133
|
+
}
|