@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/signal-digest.ts
CHANGED
|
@@ -8,7 +8,6 @@ import type {
|
|
|
8
8
|
|
|
9
9
|
export interface CategoryAffinity {
|
|
10
10
|
category: string;
|
|
11
|
-
|
|
12
11
|
score: number;
|
|
13
12
|
}
|
|
14
13
|
|
|
@@ -62,27 +61,29 @@ export const DIGEST_LIMITS = {
|
|
|
62
61
|
const SIGNAL_WEIGHTS = {
|
|
63
62
|
purchase: 5,
|
|
64
63
|
like: 4,
|
|
64
|
+
browsing: 4,
|
|
65
65
|
cart: 3,
|
|
66
66
|
view: 1,
|
|
67
67
|
dislike: -6,
|
|
68
68
|
} as const;
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
interface MergedView extends ViewedProduct {
|
|
71
|
+
category?: string;
|
|
72
|
+
weight: number;
|
|
73
|
+
}
|
|
71
74
|
|
|
72
|
-
function
|
|
73
|
-
|
|
74
|
-
right: { at?: number | undefined },
|
|
75
|
-
): number {
|
|
76
|
-
return (right.at ?? 0) - (left.at ?? 0);
|
|
75
|
+
function weightOf(signal: SkuSignal): number {
|
|
76
|
+
return signal.weight ?? 1;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
function recentUniqueSkus(signals: SkuSignal[], limit: number): string[] {
|
|
80
|
+
const newestFirst = signals.toSorted((left, right) => (right.at ?? 0) - (left.at ?? 0));
|
|
80
81
|
const skus: string[] = [];
|
|
81
|
-
const
|
|
82
|
+
const seen = new Set<string>();
|
|
82
83
|
|
|
83
|
-
for (const signal of
|
|
84
|
-
if (
|
|
85
|
-
|
|
84
|
+
for (const signal of newestFirst) {
|
|
85
|
+
if (seen.has(signal.sku)) continue;
|
|
86
|
+
seen.add(signal.sku);
|
|
86
87
|
skus.push(signal.sku);
|
|
87
88
|
if (skus.length >= limit) break;
|
|
88
89
|
}
|
|
@@ -90,54 +91,34 @@ function recentUniqueSkus(signals: SkuSignal[], limit: number): string[] {
|
|
|
90
91
|
return skus;
|
|
91
92
|
}
|
|
92
93
|
|
|
93
|
-
function categoryOf(
|
|
94
|
-
signal: { sku: string; category?: string | undefined },
|
|
95
|
-
candidatesBySku: Map<string, Product>,
|
|
96
|
-
): string | undefined {
|
|
94
|
+
function categoryOf(signal: SkuSignal, candidatesBySku: Map<string, Product>): string | undefined {
|
|
97
95
|
return signal.category ?? candidatesBySku.get(signal.sku)?.category;
|
|
98
96
|
}
|
|
99
97
|
|
|
100
|
-
function
|
|
101
|
-
input
|
|
102
|
-
|
|
103
|
-
): CategoryAffinity[] {
|
|
104
|
-
const scoreByCategory = new Map<string, number>();
|
|
98
|
+
function scoreCategories(input: TrackingInput): CategoryAffinity[] {
|
|
99
|
+
const candidatesBySku = new Map(input.candidates.map((product) => [product.sku, product]));
|
|
100
|
+
const scores = new Map<string, number>();
|
|
105
101
|
|
|
106
|
-
const
|
|
102
|
+
const add = (category: string | undefined, score: number): void => {
|
|
107
103
|
if (!category) return;
|
|
108
|
-
|
|
104
|
+
scores.set(category, (scores.get(category) ?? 0) + score);
|
|
105
|
+
};
|
|
106
|
+
const addSignal = (signal: SkuSignal, base: number): void => {
|
|
107
|
+
add(categoryOf(signal, candidatesBySku), base * weightOf(signal));
|
|
109
108
|
};
|
|
110
109
|
|
|
111
110
|
const { signals } = input;
|
|
112
|
-
|
|
113
|
-
for (const
|
|
114
|
-
|
|
115
|
-
categoryOf(purchase, candidatesBySku),
|
|
116
|
-
SIGNAL_WEIGHTS.purchase * effectiveWeight(purchase),
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
for (const like of signals.likes) {
|
|
120
|
-
addScore(categoryOf(like, candidatesBySku), SIGNAL_WEIGHTS.like * effectiveWeight(like));
|
|
121
|
-
}
|
|
122
|
-
for (const inCart of signals.cart) {
|
|
123
|
-
addScore(categoryOf(inCart, candidatesBySku), SIGNAL_WEIGHTS.cart * effectiveWeight(inCart));
|
|
124
|
-
}
|
|
125
|
-
|
|
111
|
+
for (const purchase of signals.lastPurchased) addSignal(purchase, SIGNAL_WEIGHTS.purchase);
|
|
112
|
+
for (const like of signals.likes) addSignal(like, SIGNAL_WEIGHTS.like);
|
|
113
|
+
for (const inCart of signals.cart) addSignal(inCart, SIGNAL_WEIGHTS.cart);
|
|
126
114
|
for (const view of mergeViewsBySku(signals.mostViewed)) {
|
|
127
|
-
|
|
128
|
-
addScore(categoryOf(view, candidatesBySku), SIGNAL_WEIGHTS.view * scaledViews * view.weight);
|
|
115
|
+
addSignal(view, SIGNAL_WEIGHTS.view * Math.log2(1 + view.views));
|
|
129
116
|
}
|
|
130
|
-
for (const dislike of signals.dislikes)
|
|
131
|
-
|
|
132
|
-
categoryOf(dislike, candidatesBySku),
|
|
133
|
-
SIGNAL_WEIGHTS.dislike * effectiveWeight(dislike),
|
|
134
|
-
);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
addScore(input.context.currentCategory, SIGNAL_WEIGHTS.like);
|
|
117
|
+
for (const dislike of signals.dislikes) addSignal(dislike, SIGNAL_WEIGHTS.dislike);
|
|
118
|
+
add(input.context.currentCategory, SIGNAL_WEIGHTS.browsing);
|
|
138
119
|
|
|
139
120
|
const affinities: CategoryAffinity[] = [];
|
|
140
|
-
for (const [category, score] of
|
|
121
|
+
for (const [category, score] of scores) {
|
|
141
122
|
const rounded = Math.round(score * 100) / 100;
|
|
142
123
|
if (rounded > 0) affinities.push({ category, score: rounded });
|
|
143
124
|
}
|
|
@@ -147,7 +128,7 @@ function computeCategoryAffinity(
|
|
|
147
128
|
.slice(0, DIGEST_LIMITS.affinity);
|
|
148
129
|
}
|
|
149
130
|
|
|
150
|
-
export function
|
|
131
|
+
export function categoriesTouchedBySignals(input: TrackingInput): Set<string> {
|
|
151
132
|
const candidatesBySku = new Map(input.candidates.map((product) => [product.sku, product]));
|
|
152
133
|
const { signals } = input;
|
|
153
134
|
const categories = new Set<string>();
|
|
@@ -165,53 +146,44 @@ export function engagedCategories(input: TrackingInput): Set<string> {
|
|
|
165
146
|
return categories;
|
|
166
147
|
}
|
|
167
148
|
|
|
168
|
-
interface MergedView extends ViewedProduct {
|
|
169
|
-
category?: string;
|
|
170
|
-
weight: number;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
149
|
function mergeViewsBySku(views: ViewSignal[]): MergedView[] {
|
|
174
|
-
const
|
|
150
|
+
const totals = new Map<string, MergedView>();
|
|
175
151
|
|
|
176
152
|
for (const view of views) {
|
|
177
|
-
const
|
|
178
|
-
if (
|
|
179
|
-
|
|
180
|
-
if (view.dwellMs !== undefined)
|
|
181
|
-
if (view.category !== undefined)
|
|
182
|
-
|
|
153
|
+
const total = totals.get(view.sku);
|
|
154
|
+
if (!total) {
|
|
155
|
+
const first: MergedView = { sku: view.sku, views: view.views, weight: weightOf(view) };
|
|
156
|
+
if (view.dwellMs !== undefined) first.dwellMs = view.dwellMs;
|
|
157
|
+
if (view.category !== undefined) first.category = view.category;
|
|
158
|
+
totals.set(view.sku, first);
|
|
183
159
|
continue;
|
|
184
160
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
weight: effectiveWeight(view),
|
|
191
|
-
});
|
|
161
|
+
|
|
162
|
+
total.views += view.views;
|
|
163
|
+
if (view.dwellMs !== undefined) total.dwellMs = (total.dwellMs ?? 0) + view.dwellMs;
|
|
164
|
+
if (view.category !== undefined) total.category ??= view.category;
|
|
165
|
+
total.weight = Math.max(total.weight, weightOf(view));
|
|
192
166
|
}
|
|
193
167
|
|
|
194
|
-
return [...
|
|
168
|
+
return [...totals.values()];
|
|
195
169
|
}
|
|
196
170
|
|
|
197
|
-
function mostViewedProducts(
|
|
198
|
-
const top = mergeViewsBySku(
|
|
171
|
+
function mostViewedProducts(viewSignals: ViewSignal[]): ViewedProduct[] {
|
|
172
|
+
const top = mergeViewsBySku(viewSignals)
|
|
199
173
|
.toSorted((left, right) => right.views - left.views)
|
|
200
174
|
.slice(0, DIGEST_LIMITS.viewed);
|
|
201
175
|
|
|
202
|
-
return top.map(({ sku, views
|
|
203
|
-
dwellMs === undefined ? { sku, views
|
|
176
|
+
return top.map(({ sku, views, dwellMs }) =>
|
|
177
|
+
dwellMs === undefined ? { sku, views } : { sku, views, dwellMs },
|
|
204
178
|
);
|
|
205
179
|
}
|
|
206
180
|
|
|
207
181
|
function countByInteractionType(interactions: Interaction[]): InteractionCount[] {
|
|
208
182
|
const countByType = new Map<string, number>();
|
|
183
|
+
for (const { type } of interactions) countByType.set(type, (countByType.get(type) ?? 0) + 1);
|
|
209
184
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const counts = [...countByType.entries()].map(([type, count]) => ({ type, count }));
|
|
185
|
+
const counts: InteractionCount[] = [];
|
|
186
|
+
for (const [type, count] of countByType) counts.push({ type, count });
|
|
215
187
|
|
|
216
188
|
return counts
|
|
217
189
|
.toSorted((left, right) => right.count - left.count)
|
|
@@ -219,7 +191,6 @@ function countByInteractionType(interactions: Interaction[]): InteractionCount[]
|
|
|
219
191
|
}
|
|
220
192
|
|
|
221
193
|
export function buildDigest(input: TrackingInput): SignalDigest {
|
|
222
|
-
const candidatesBySku = new Map(input.candidates.map((product) => [product.sku, product]));
|
|
223
194
|
const { signals, context, user } = input;
|
|
224
195
|
|
|
225
196
|
const likedSkus = recentUniqueSkus(signals.likes, DIGEST_LIMITS.liked);
|
|
@@ -254,7 +225,7 @@ export function buildDigest(input: TrackingInput): SignalDigest {
|
|
|
254
225
|
cartSkus,
|
|
255
226
|
topViewed,
|
|
256
227
|
recentSearches: signals.recentSearches.slice(0, DIGEST_LIMITS.searches),
|
|
257
|
-
categoryAffinity:
|
|
228
|
+
categoryAffinity: scoreCategories(input),
|
|
258
229
|
interactionCounts: countByInteractionType(signals.interactions),
|
|
259
230
|
|
|
260
231
|
isColdStart: evidenceCount === 0,
|
package/src/spec-cache.ts
CHANGED
|
@@ -5,7 +5,6 @@ import type { SignalDigest } from './signal-digest.js';
|
|
|
5
5
|
|
|
6
6
|
export interface CachedSpec {
|
|
7
7
|
spec: GeneratedSpec;
|
|
8
|
-
|
|
9
8
|
generatedAt: number;
|
|
10
9
|
}
|
|
11
10
|
|
|
@@ -17,9 +16,7 @@ export interface SpecCache {
|
|
|
17
16
|
|
|
18
17
|
export interface MemorySpecCacheOptions {
|
|
19
18
|
ttlMs?: number;
|
|
20
|
-
|
|
21
19
|
maxEntries?: number;
|
|
22
|
-
|
|
23
20
|
now?: () => number;
|
|
24
21
|
}
|
|
25
22
|
|
|
@@ -55,12 +52,9 @@ export function createMemorySpecCache(options: MemorySpecCacheOptions = {}): Spe
|
|
|
55
52
|
const entry = entries.get(key);
|
|
56
53
|
if (!entry) return undefined;
|
|
57
54
|
|
|
58
|
-
|
|
59
|
-
entries.delete(key);
|
|
60
|
-
return undefined;
|
|
61
|
-
}
|
|
62
|
-
|
|
55
|
+
const isExpired = entry.expiresAt <= now();
|
|
63
56
|
entries.delete(key);
|
|
57
|
+
if (isExpired) return undefined;
|
|
64
58
|
entries.set(key, entry);
|
|
65
59
|
return entry.cached;
|
|
66
60
|
},
|
|
@@ -69,10 +63,9 @@ export function createMemorySpecCache(options: MemorySpecCacheOptions = {}): Spe
|
|
|
69
63
|
entries.delete(key);
|
|
70
64
|
entries.set(key, { cached, expiresAt: now() + ttlMs });
|
|
71
65
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
entries.delete(oldest.value);
|
|
66
|
+
for (const oldest of entries.keys()) {
|
|
67
|
+
if (entries.size <= maxEntries) break;
|
|
68
|
+
entries.delete(oldest);
|
|
76
69
|
}
|
|
77
70
|
},
|
|
78
71
|
|
|
@@ -83,23 +76,22 @@ export function createMemorySpecCache(options: MemorySpecCacheOptions = {}): Spe
|
|
|
83
76
|
}
|
|
84
77
|
|
|
85
78
|
export function createNullSpecCache(): SpecCache {
|
|
86
|
-
return {
|
|
87
|
-
async get() {
|
|
88
|
-
return undefined;
|
|
89
|
-
},
|
|
90
|
-
async set() {},
|
|
91
|
-
};
|
|
79
|
+
return { get: async () => undefined, set: async () => {} };
|
|
92
80
|
}
|
|
93
81
|
|
|
94
82
|
function canonicalise(value: unknown): string {
|
|
95
83
|
if (value === null || typeof value !== 'object') return JSON.stringify(value) ?? 'null';
|
|
96
84
|
if (Array.isArray(value)) return `[${value.map(canonicalise).join(',')}]`;
|
|
97
85
|
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
86
|
+
const sorted = Object.entries(value as Record<string, unknown>).toSorted(([left], [right]) =>
|
|
87
|
+
left < right ? -1 : 1,
|
|
88
|
+
);
|
|
89
|
+
const fields: string[] = [];
|
|
90
|
+
for (const [name, fieldValue] of sorted) {
|
|
91
|
+
if (fieldValue === undefined) continue;
|
|
92
|
+
fields.push(`${JSON.stringify(name)}:${canonicalise(fieldValue)}`);
|
|
93
|
+
}
|
|
94
|
+
return `{${fields.join(',')}}`;
|
|
103
95
|
}
|
|
104
96
|
|
|
105
97
|
const PROMPT_FINGERPRINT = createHash('sha256').update(SYSTEM_PROMPT).digest('hex').slice(0, 16);
|
|
@@ -119,28 +111,3 @@ export function specCacheKey(
|
|
|
119
111
|
|
|
120
112
|
return createHash('sha256').update(material).digest('hex').slice(0, 32);
|
|
121
113
|
}
|
|
122
|
-
|
|
123
|
-
export function cohortCacheKey(
|
|
124
|
-
digest: SignalDigest,
|
|
125
|
-
candidateSkus: readonly string[],
|
|
126
|
-
provider: { name: string; model: string },
|
|
127
|
-
): string {
|
|
128
|
-
const material = canonicalise({
|
|
129
|
-
specVersion: SPEC_VERSION,
|
|
130
|
-
prompt: PROMPT_FINGERPRINT,
|
|
131
|
-
provider,
|
|
132
|
-
segment: digest.segment ?? null,
|
|
133
|
-
surface: digest.surface,
|
|
134
|
-
slot: digest.slot,
|
|
135
|
-
locale: digest.locale,
|
|
136
|
-
maxItems: digest.maxItems,
|
|
137
|
-
isColdStart: digest.isColdStart,
|
|
138
|
-
|
|
139
|
-
currentCategory: digest.currentCategory ?? null,
|
|
140
|
-
topCategory: digest.categoryAffinity[0]?.category ?? null,
|
|
141
|
-
|
|
142
|
-
candidates: candidateSkus.toSorted(),
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
return createHash('sha256').update(material).digest('hex').slice(0, 32);
|
|
146
|
-
}
|
package/src/tracking-input.ts
CHANGED
|
@@ -13,7 +13,6 @@ export const FIELD_LIMITS = {
|
|
|
13
13
|
bundles: 20,
|
|
14
14
|
localeTag: 35,
|
|
15
15
|
maxItems: 12,
|
|
16
|
-
|
|
17
16
|
reason: 120,
|
|
18
17
|
} as const;
|
|
19
18
|
|
|
@@ -22,13 +21,21 @@ const RESERVED_META_KEY = '__proto__';
|
|
|
22
21
|
const MAX_EPOCH_MS = Date.UTC(2100, 0, 1);
|
|
23
22
|
|
|
24
23
|
const identifier = () => z.string().min(1).max(FIELD_LIMITS.identifier);
|
|
25
|
-
const optionalIdentifier = () =>
|
|
24
|
+
const optionalIdentifier = () => identifier().optional();
|
|
25
|
+
|
|
26
|
+
const currencyCode = () =>
|
|
27
|
+
z
|
|
28
|
+
.string()
|
|
29
|
+
.regex(/^[A-Z]{3}$/, 'expected a three-letter ISO 4217 code')
|
|
30
|
+
.default('USD');
|
|
31
|
+
|
|
32
|
+
const isUnique = (values: string[]) => new Set(values).size === values.length;
|
|
26
33
|
|
|
27
34
|
const epochMs = () => z.number().int().min(0).max(MAX_EPOCH_MS).optional();
|
|
28
35
|
|
|
29
36
|
const isRootRelative = (value: string) => {
|
|
30
|
-
const
|
|
31
|
-
return value.startsWith('/') && /^\/(?![/\\])/.test(
|
|
37
|
+
const asBrowserReadsIt = value.replace(/[\t\n\r]/g, '');
|
|
38
|
+
return value.startsWith('/') && /^\/(?![/\\])/.test(asBrowserReadsIt);
|
|
32
39
|
};
|
|
33
40
|
|
|
34
41
|
const imageReference = () =>
|
|
@@ -36,8 +43,7 @@ const imageReference = () =>
|
|
|
36
43
|
.string()
|
|
37
44
|
.max(FIELD_LIMITS.shortText)
|
|
38
45
|
.refine(
|
|
39
|
-
(value) =>
|
|
40
|
-
isRootRelative(value) ? true : z.url({ protocol: /^https?$/ }).safeParse(value).success,
|
|
46
|
+
(value) => isRootRelative(value) || z.url({ protocol: /^https?$/ }).safeParse(value).success,
|
|
41
47
|
{ message: 'expected an http(s) URL or a root-relative path' },
|
|
42
48
|
);
|
|
43
49
|
|
|
@@ -46,14 +52,9 @@ export const productSchema = z.strictObject({
|
|
|
46
52
|
title: z.string().min(1).max(FIELD_LIMITS.shortText),
|
|
47
53
|
category: identifier(),
|
|
48
54
|
price: z.number().nonnegative(),
|
|
49
|
-
currency:
|
|
50
|
-
.string()
|
|
51
|
-
.regex(/^[A-Z]{3}$/, 'expected a three-letter ISO 4217 code')
|
|
52
|
-
.default('USD'),
|
|
53
|
-
|
|
55
|
+
currency: currencyCode(),
|
|
54
56
|
imageUrl: imageReference().optional(),
|
|
55
57
|
rating: z.number().min(0).max(5).optional(),
|
|
56
|
-
|
|
57
58
|
reason: z.string().min(1).max(FIELD_LIMITS.reason).optional(),
|
|
58
59
|
isInStock: z.boolean().default(true),
|
|
59
60
|
tags: z
|
|
@@ -67,7 +68,6 @@ export const skuSignalSchema = z.strictObject({
|
|
|
67
68
|
sku: identifier(),
|
|
68
69
|
category: optionalIdentifier(),
|
|
69
70
|
at: epochMs(),
|
|
70
|
-
|
|
71
71
|
weight: z.number().min(0).max(1).optional(),
|
|
72
72
|
});
|
|
73
73
|
export type SkuSignal = z.infer<typeof skuSignalSchema>;
|
|
@@ -93,7 +93,7 @@ const metaKeysAreSafe = z.custom<Record<string, string | number | boolean>>(
|
|
|
93
93
|
const metaSchema = metaKeysAreSafe.pipe(
|
|
94
94
|
z
|
|
95
95
|
.record(
|
|
96
|
-
|
|
96
|
+
identifier(),
|
|
97
97
|
z.union([z.string().max(FIELD_LIMITS.shortText), z.number(), z.boolean()]),
|
|
98
98
|
)
|
|
99
99
|
.refine((entries) => Object.keys(entries).length <= FIELD_LIMITS.metaEntries, {
|
|
@@ -113,18 +113,15 @@ export type Interaction = z.infer<typeof interactionSchema>;
|
|
|
113
113
|
|
|
114
114
|
export const renderContextSchema = z.strictObject({
|
|
115
115
|
surface: identifier(),
|
|
116
|
-
|
|
117
|
-
slot: z.string().min(1).max(FIELD_LIMITS.identifier).default('recommendations'),
|
|
116
|
+
slot: identifier().default('recommendations'),
|
|
118
117
|
currentSku: optionalIdentifier(),
|
|
119
118
|
currentCategory: optionalIdentifier(),
|
|
120
119
|
searchQuery: z.string().max(FIELD_LIMITS.searchQuery).optional(),
|
|
121
|
-
|
|
122
120
|
locale: z
|
|
123
121
|
.string()
|
|
124
122
|
.max(FIELD_LIMITS.localeTag)
|
|
125
123
|
.regex(/^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*$/, 'expected one language tag, such as en-US')
|
|
126
124
|
.default('en-US'),
|
|
127
|
-
|
|
128
125
|
maxItems: z.number().int().min(1).max(FIELD_LIMITS.maxItems).default(4),
|
|
129
126
|
});
|
|
130
127
|
export type RenderContext = z.infer<typeof renderContextSchema>;
|
|
@@ -149,15 +146,9 @@ export const bundleSchema = z.strictObject({
|
|
|
149
146
|
.array(identifier())
|
|
150
147
|
.min(2)
|
|
151
148
|
.max(FIELD_LIMITS.productsPerBundle)
|
|
152
|
-
.refine(
|
|
153
|
-
message: 'a bundle must not list the same product twice',
|
|
154
|
-
}),
|
|
155
|
-
|
|
149
|
+
.refine(isUnique, { message: 'a bundle must not list the same product twice' }),
|
|
156
150
|
price: z.number().nonnegative(),
|
|
157
|
-
currency:
|
|
158
|
-
.string()
|
|
159
|
-
.regex(/^[A-Z]{3}$/, 'expected a three-letter ISO 4217 code')
|
|
160
|
-
.default('USD'),
|
|
151
|
+
currency: currencyCode(),
|
|
161
152
|
label: z.string().min(1).max(FIELD_LIMITS.shortText).optional(),
|
|
162
153
|
});
|
|
163
154
|
export type Bundle = z.infer<typeof bundleSchema>;
|
|
@@ -171,24 +162,18 @@ export const trackingInputSchema = z
|
|
|
171
162
|
isReturning: z.boolean().optional(),
|
|
172
163
|
}),
|
|
173
164
|
context: renderContextSchema,
|
|
174
|
-
|
|
175
165
|
signals: trackingSignalsSchema.prefault({}),
|
|
176
|
-
|
|
177
166
|
candidates: z
|
|
178
167
|
.array(productSchema)
|
|
179
168
|
.min(1)
|
|
180
169
|
.max(FIELD_LIMITS.candidates)
|
|
181
|
-
.refine(
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
message: 'candidates must have unique SKUs',
|
|
185
|
-
},
|
|
186
|
-
),
|
|
187
|
-
|
|
170
|
+
.refine((products) => isUnique(products.map((product) => product.sku)), {
|
|
171
|
+
message: 'candidates must have unique SKUs',
|
|
172
|
+
}),
|
|
188
173
|
bundles: z
|
|
189
174
|
.array(bundleSchema)
|
|
190
175
|
.max(FIELD_LIMITS.bundles)
|
|
191
|
-
.refine((bundles) =>
|
|
176
|
+
.refine((bundles) => isUnique(bundles.map((bundle) => bundle.id)), {
|
|
192
177
|
message: 'bundles must have unique ids',
|
|
193
178
|
})
|
|
194
179
|
.default([]),
|