@rudra-js/core 0.7.4 → 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.
Files changed (54) hide show
  1. package/dist/claim-screening.d.ts +3 -3
  2. package/dist/claim-screening.d.ts.map +1 -1
  3. package/dist/claim-screening.js +30 -93
  4. package/dist/claim-screening.js.map +1 -1
  5. package/dist/component-generator.d.ts.map +1 -1
  6. package/dist/component-generator.js +161 -176
  7. package/dist/component-generator.js.map +1 -1
  8. package/dist/component-spec.d.ts +1 -0
  9. package/dist/component-spec.d.ts.map +1 -1
  10. package/dist/component-spec.js +3 -0
  11. package/dist/component-spec.js.map +1 -1
  12. package/dist/fallback-component.d.ts +1 -1
  13. package/dist/fallback-component.d.ts.map +1 -1
  14. package/dist/fallback-component.js +9 -17
  15. package/dist/fallback-component.js.map +1 -1
  16. package/dist/fit-to-shopper.d.ts.map +1 -1
  17. package/dist/fit-to-shopper.js +19 -19
  18. package/dist/fit-to-shopper.js.map +1 -1
  19. package/dist/model-prompt.d.ts.map +1 -1
  20. package/dist/model-prompt.js +56 -39
  21. package/dist/model-prompt.js.map +1 -1
  22. package/dist/product-selection.d.ts.map +1 -1
  23. package/dist/product-selection.js +15 -31
  24. package/dist/product-selection.js.map +1 -1
  25. package/dist/provider.d.ts.map +1 -1
  26. package/dist/provider.js.map +1 -1
  27. package/dist/reconciliation.d.ts +3 -3
  28. package/dist/reconciliation.d.ts.map +1 -1
  29. package/dist/reconciliation.js +133 -171
  30. package/dist/reconciliation.js.map +1 -1
  31. package/dist/signal-digest.d.ts +1 -1
  32. package/dist/signal-digest.d.ts.map +1 -1
  33. package/dist/signal-digest.js +51 -52
  34. package/dist/signal-digest.js.map +1 -1
  35. package/dist/spec-cache.d.ts +0 -4
  36. package/dist/spec-cache.d.ts.map +1 -1
  37. package/dist/spec-cache.js +15 -35
  38. package/dist/spec-cache.js.map +1 -1
  39. package/dist/tracking-input.d.ts.map +1 -1
  40. package/dist/tracking-input.js +16 -19
  41. package/dist/tracking-input.js.map +1 -1
  42. package/package.json +3 -3
  43. package/src/claim-screening.ts +34 -114
  44. package/src/component-generator.ts +223 -220
  45. package/src/component-spec.ts +4 -15
  46. package/src/fallback-component.ts +11 -20
  47. package/src/fit-to-shopper.ts +19 -21
  48. package/src/model-prompt.ts +55 -54
  49. package/src/product-selection.ts +21 -50
  50. package/src/provider.ts +0 -4
  51. package/src/reconciliation.ts +159 -268
  52. package/src/signal-digest.ts +50 -79
  53. package/src/spec-cache.ts +15 -48
  54. package/src/tracking-input.ts +21 -36
@@ -1,5 +1,6 @@
1
+ import { foldLookalikes, verify } from '@rudra-js/attested';
1
2
  import { offeredCandidates } from './model-prompt.js';
2
- import type { Product, TrackingInput } from './tracking-input.js';
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]\.\d|(?:[0-5]|three|four|five)\s+(?:stars?|out of))\b/,
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
- /\bprice reduced\b|\breduced price\b|\breduced by \d+\s?(?:%|percent)|\breduced\s+(?:this|next|last)\s+week\b/,
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,7 +69,6 @@ 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/,
85
73
  /\bonly\s+(?:two|three|four|five|six|seven|eight|nine|ten)\s+(?:left|remain(?:s|ing)?)\b/,
86
74
  /\bselling fast\b|\b(?:almost|nearly) gone\b|\bwhile stocks last\b/,
@@ -88,81 +76,6 @@ const CLAIM_PATTERNS: { kind: string; patterns: RegExp[] }[] = [
88
76
  },
89
77
  ];
90
78
 
91
- const CONFUSABLES: Record<string, string> = {
92
- а: 'a',
93
- в: 'b',
94
- е: 'e',
95
- к: 'k',
96
- м: 'm',
97
- н: 'h',
98
- о: 'o',
99
- р: 'p',
100
- с: 'c',
101
- т: 't',
102
- х: 'x',
103
- у: 'y',
104
- ѕ: 's',
105
- і: 'i',
106
- ј: 'j',
107
- ԁ: 'd',
108
- һ: 'h',
109
- ӏ: 'l',
110
- α: 'a',
111
- ε: 'e',
112
- ι: 'i',
113
- κ: 'k',
114
- ν: 'v',
115
- ο: 'o',
116
- ρ: 'p',
117
- τ: 't',
118
- υ: 'u',
119
- χ: 'x',
120
- ϳ: 'j',
121
- ѵ: 'v',
122
- };
123
-
124
- // Capitals that lowercasing reads wrong: Ν becomes ν, which reads as v.
125
- const CAPITALS: Record<string, string> = {
126
- Β: 'b',
127
- Ζ: 'z',
128
- Η: 'h',
129
- Μ: 'm',
130
- Ν: 'n',
131
- Υ: 'y',
132
- Ү: 'y',
133
- Ӏ: 'i',
134
- Ԛ: 'q',
135
- Ԝ: 'w',
136
- Ϻ: 'm',
137
- Ϝ: 'f',
138
- Ԍ: 'g',
139
- };
140
-
141
- const INVISIBLE = /[\p{Cf}\p{Default_Ignorable_Code_Point}\p{Cc}]/gu;
142
-
143
- const SPACING = /[\t\n\v\f\r\u0085]/;
144
-
145
- const MARKS = /[\p{Mn}\p{Me}]/gu;
146
-
147
- function normaliseForClaims(text: string, byLook: boolean): string {
148
- // NFKC would turn the lunate sigmas into ς and Σ, which the fold can't read as c.
149
- const compatible = text
150
- .replace(INVISIBLE, (char) => (SPACING.test(char) ? char : ''))
151
- .replace(/[ϲϹ]/g, 'c')
152
- .normalize('NFKC');
153
-
154
- let cased = compatible;
155
- if (byLook) {
156
- cased = '';
157
- for (const char of compatible) cased += CAPITALS[char] ?? char;
158
- }
159
- const lowered = cased.toLowerCase().normalize('NFC').replace(MARKS, '');
160
-
161
- let folded = '';
162
- for (const char of lowered) folded += CONFUSABLES[char] ?? char;
163
- return folded;
164
- }
165
-
166
79
  export const ALLOWED_PHRASES = [
167
80
  'does not feel cheap',
168
81
  "doesn't feel cheap",
@@ -170,16 +83,10 @@ export const ALLOWED_PHRASES = [
170
83
  'sustainable pace',
171
84
  ];
172
85
 
173
- const DIGIT = /\p{Nd}/u;
86
+ const FACT_DIGIT = /\p{Nd}/u;
87
+ const NUMERAL = /[\p{Nd}\p{No}\p{Nl}]/u;
174
88
 
175
- export function productFacts(product: Product): string[] {
176
- const facts: string[] = [];
177
- if (DIGIT.test(product.category)) facts.push(product.category);
178
- for (const tag of product.tags) {
179
- if (DIGIT.test(tag)) facts.push(tag);
180
- }
181
- return facts;
182
- }
89
+ export const NO_FACTS: readonly string[] = [];
183
90
 
184
91
  export interface HostFacts {
185
92
  pooled: string[];
@@ -191,7 +98,7 @@ export function hostFacts(input: TrackingInput): HostFacts {
191
98
  const bySku = new Map<string, string[]>();
192
99
 
193
100
  for (const product of offeredCandidates(input)) {
194
- const own = productFacts(product);
101
+ const own = [product.category, ...product.tags].filter((text) => FACT_DIGIT.test(text));
195
102
  bySku.set(product.sku, own);
196
103
  for (const fact of own) pooled.add(fact);
197
104
  }
@@ -199,15 +106,28 @@ export function hostFacts(input: TrackingInput): HostFacts {
199
106
  return { pooled: [...pooled], bySku };
200
107
  }
201
108
 
202
- export function claimIn(text: string): string | null {
203
- // Read twice, since Ν reads as N but ν as v.
204
- for (const byLook of [false, true]) {
205
- const normalised = normaliseForClaims(text, byLook);
206
- for (const claim of CLAIM_PATTERNS) {
207
- for (const pattern of claim.patterns) {
208
- if (pattern.test(normalised)) return claim.kind;
209
- }
210
- }
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;
211
112
  }
212
113
  return null;
213
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
+ }