@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.
Files changed (55) hide show
  1. package/README.md +113 -22
  2. package/dist/claim-screening.d.ts +3 -3
  3. package/dist/claim-screening.d.ts.map +1 -1
  4. package/dist/claim-screening.js +37 -102
  5. package/dist/claim-screening.js.map +1 -1
  6. package/dist/component-generator.d.ts.map +1 -1
  7. package/dist/component-generator.js +161 -176
  8. package/dist/component-generator.js.map +1 -1
  9. package/dist/component-spec.d.ts +1 -0
  10. package/dist/component-spec.d.ts.map +1 -1
  11. package/dist/component-spec.js +3 -0
  12. package/dist/component-spec.js.map +1 -1
  13. package/dist/fallback-component.d.ts +1 -1
  14. package/dist/fallback-component.d.ts.map +1 -1
  15. package/dist/fallback-component.js +9 -17
  16. package/dist/fallback-component.js.map +1 -1
  17. package/dist/fit-to-shopper.d.ts.map +1 -1
  18. package/dist/fit-to-shopper.js +19 -19
  19. package/dist/fit-to-shopper.js.map +1 -1
  20. package/dist/model-prompt.d.ts.map +1 -1
  21. package/dist/model-prompt.js +56 -39
  22. package/dist/model-prompt.js.map +1 -1
  23. package/dist/product-selection.d.ts.map +1 -1
  24. package/dist/product-selection.js +15 -31
  25. package/dist/product-selection.js.map +1 -1
  26. package/dist/provider.d.ts.map +1 -1
  27. package/dist/provider.js.map +1 -1
  28. package/dist/reconciliation.d.ts +3 -3
  29. package/dist/reconciliation.d.ts.map +1 -1
  30. package/dist/reconciliation.js +133 -171
  31. package/dist/reconciliation.js.map +1 -1
  32. package/dist/signal-digest.d.ts +1 -1
  33. package/dist/signal-digest.d.ts.map +1 -1
  34. package/dist/signal-digest.js +51 -52
  35. package/dist/signal-digest.js.map +1 -1
  36. package/dist/spec-cache.d.ts +0 -4
  37. package/dist/spec-cache.d.ts.map +1 -1
  38. package/dist/spec-cache.js +15 -35
  39. package/dist/spec-cache.js.map +1 -1
  40. package/dist/tracking-input.d.ts.map +1 -1
  41. package/dist/tracking-input.js +16 -19
  42. package/dist/tracking-input.js.map +1 -1
  43. package/package.json +3 -3
  44. package/src/claim-screening.ts +41 -125
  45. package/src/component-generator.ts +223 -220
  46. package/src/component-spec.ts +4 -15
  47. package/src/fallback-component.ts +11 -20
  48. package/src/fit-to-shopper.ts +19 -21
  49. package/src/model-prompt.ts +55 -54
  50. package/src/product-selection.ts +21 -50
  51. package/src/provider.ts +0 -4
  52. package/src/reconciliation.ts +159 -268
  53. package/src/signal-digest.ts +50 -79
  54. package/src/spec-cache.ts +15 -48
  55. 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,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 CONFUSABLES: Record<string, string> = {
91
- а: 'a',
92
- в: 'b',
93
- е: 'e',
94
- к: 'k',
95
- м: 'm',
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
- function canLayOut(fact: string): boolean {
174
- const match = BARE_EXPONENT.exec(fact.trim());
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 function productFacts(product: Product): string[] {
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 = productFacts(product);
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
- export function claimIn(text: string): string | null {
207
- // Read twice, since Ν reads as N but ν as v.
208
- for (const byLook of [false, true]) {
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
+ }