@rudra-js/core 0.1.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.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +189 -0
  3. package/dist/component-generator.d.ts +84 -0
  4. package/dist/component-generator.d.ts.map +1 -0
  5. package/dist/component-generator.js +331 -0
  6. package/dist/component-generator.js.map +1 -0
  7. package/dist/component-spec.d.ts +426 -0
  8. package/dist/component-spec.d.ts.map +1 -0
  9. package/dist/component-spec.js +170 -0
  10. package/dist/component-spec.js.map +1 -0
  11. package/dist/fallback-component.d.ts +11 -0
  12. package/dist/fallback-component.d.ts.map +1 -0
  13. package/dist/fallback-component.js +69 -0
  14. package/dist/fallback-component.js.map +1 -0
  15. package/dist/fit-to-shopper.d.ts +4 -0
  16. package/dist/fit-to-shopper.d.ts.map +1 -0
  17. package/dist/fit-to-shopper.js +36 -0
  18. package/dist/fit-to-shopper.js.map +1 -0
  19. package/dist/index.d.ts +19 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +19 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/model-prompt.d.ts +29 -0
  24. package/dist/model-prompt.d.ts.map +1 -0
  25. package/dist/model-prompt.js +227 -0
  26. package/dist/model-prompt.js.map +1 -0
  27. package/dist/product-selection.d.ts +33 -0
  28. package/dist/product-selection.d.ts.map +1 -0
  29. package/dist/product-selection.js +102 -0
  30. package/dist/product-selection.js.map +1 -0
  31. package/dist/provider.d.ts +80 -0
  32. package/dist/provider.d.ts.map +1 -0
  33. package/dist/provider.js +23 -0
  34. package/dist/provider.js.map +1 -0
  35. package/dist/reconciliation.d.ts +49 -0
  36. package/dist/reconciliation.d.ts.map +1 -0
  37. package/dist/reconciliation.js +564 -0
  38. package/dist/reconciliation.js.map +1 -0
  39. package/dist/signal-digest.d.ts +66 -0
  40. package/dist/signal-digest.d.ts.map +1 -0
  41. package/dist/signal-digest.js +224 -0
  42. package/dist/signal-digest.js.map +1 -0
  43. package/dist/spec-cache.d.ts +88 -0
  44. package/dist/spec-cache.d.ts.map +1 -0
  45. package/dist/spec-cache.js +152 -0
  46. package/dist/spec-cache.js.map +1 -0
  47. package/dist/tracking-input.d.ts +258 -0
  48. package/dist/tracking-input.d.ts.map +1 -0
  49. package/dist/tracking-input.js +241 -0
  50. package/dist/tracking-input.js.map +1 -0
  51. package/package.json +60 -0
  52. package/src/component-generator.ts +521 -0
  53. package/src/component-spec.ts +243 -0
  54. package/src/fallback-component.ts +77 -0
  55. package/src/fit-to-shopper.ts +45 -0
  56. package/src/index.ts +102 -0
  57. package/src/model-prompt.ts +258 -0
  58. package/src/product-selection.ts +153 -0
  59. package/src/provider.ts +98 -0
  60. package/src/reconciliation.ts +675 -0
  61. package/src/signal-digest.ts +335 -0
  62. package/src/spec-cache.ts +223 -0
  63. package/src/tracking-input.ts +300 -0
@@ -0,0 +1,224 @@
1
+ /**
2
+ * How much of each signal category survives into the digest. Chosen to keep the
3
+ * per-shopper part of a prompt in the low hundreds of tokens.
4
+ */
5
+ export const DIGEST_LIMITS = {
6
+ liked: 12,
7
+ disliked: 12,
8
+ purchased: 8,
9
+ cart: 8,
10
+ viewed: 10,
11
+ searches: 5,
12
+ affinity: 6,
13
+ interactionTypes: 8,
14
+ };
15
+ /**
16
+ * How much intent each kind of signal implies. A purchase says more about a
17
+ * shopper than a view; an explicit dislike says the most, and says it in the
18
+ * opposite direction.
19
+ */
20
+ const SIGNAL_WEIGHTS = {
21
+ purchase: 5,
22
+ like: 4,
23
+ cart: 3,
24
+ view: 1,
25
+ dislike: -6,
26
+ };
27
+ /**
28
+ * A signal that does not state a weight counts at full strength. This lives in
29
+ * one place on purpose: when the default was written out at each call site, the
30
+ * merge step defaulted to 0 while scoring defaulted to 1, and an unweighted view
31
+ * merged with a `weight: 0.2` view scored as though both were 0.2.
32
+ */
33
+ const effectiveWeight = (signal) => signal.weight ?? 1;
34
+ /** Most recent first. A signal with no timestamp sorts last. */
35
+ function byMostRecent(left, right) {
36
+ return (right.at ?? 0) - (left.at ?? 0);
37
+ }
38
+ /** The most recent `limit` SKUs, each appearing once. */
39
+ function recentUniqueSkus(signals, limit) {
40
+ const skus = [];
41
+ const alreadySeen = new Set();
42
+ for (const signal of signals.toSorted(byMostRecent)) {
43
+ if (alreadySeen.has(signal.sku))
44
+ continue;
45
+ alreadySeen.add(signal.sku);
46
+ skus.push(signal.sku);
47
+ if (skus.length >= limit)
48
+ break;
49
+ }
50
+ return skus;
51
+ }
52
+ /**
53
+ * A signal may name its own category; otherwise we look the SKU up in the
54
+ * candidate set. A signal for a product that is not a candidate today and
55
+ * carries no category simply contributes nothing.
56
+ */
57
+ function categoryOf(signal, candidatesBySku) {
58
+ return signal.category ?? candidatesBySku.get(signal.sku)?.category;
59
+ }
60
+ function computeCategoryAffinity(input, candidatesBySku) {
61
+ const scoreByCategory = new Map();
62
+ const addScore = (category, score) => {
63
+ if (!category)
64
+ return;
65
+ scoreByCategory.set(category, (scoreByCategory.get(category) ?? 0) + score);
66
+ };
67
+ const { signals } = input;
68
+ for (const purchase of signals.lastPurchased) {
69
+ addScore(categoryOf(purchase, candidatesBySku), SIGNAL_WEIGHTS.purchase * effectiveWeight(purchase));
70
+ }
71
+ for (const like of signals.likes) {
72
+ addScore(categoryOf(like, candidatesBySku), SIGNAL_WEIGHTS.like * effectiveWeight(like));
73
+ }
74
+ for (const inCart of signals.cart) {
75
+ addScore(categoryOf(inCart, candidatesBySku), SIGNAL_WEIGHTS.cart * effectiveWeight(inCart));
76
+ }
77
+ // Merged first, deliberately — see `mergeViewsBySku`. Views are noisy and
78
+ // repeat cheaply, so the tenth view counts for far less than the second, and
79
+ // log scaling keeps a single obsessive session from drowning out a purchase.
80
+ for (const view of mergeViewsBySku(signals.mostViewed)) {
81
+ const scaledViews = Math.log2(1 + view.views);
82
+ addScore(categoryOf(view, candidatesBySku), SIGNAL_WEIGHTS.view * scaledViews * view.weight);
83
+ }
84
+ for (const dislike of signals.dislikes) {
85
+ addScore(categoryOf(dislike, candidatesBySku), SIGNAL_WEIGHTS.dislike * effectiveWeight(dislike));
86
+ }
87
+ // The category the shopper is standing in right now is itself evidence.
88
+ addScore(input.context.currentCategory, SIGNAL_WEIGHTS.like);
89
+ return [...scoreByCategory.entries()]
90
+ .map(([category, score]) => ({
91
+ category,
92
+ score: Math.round(score * 100) / 100,
93
+ }))
94
+ .filter((affinity) => affinity.score > 0)
95
+ .toSorted((left, right) => right.score - left.score)
96
+ .slice(0, DIGEST_LIMITS.affinity);
97
+ }
98
+ /**
99
+ * Collapses every view record for one SKU into a single running total.
100
+ *
101
+ * This has to happen before any scoring. A host is free to emit one record per
102
+ * page view rather than a running count, and scoring each record separately
103
+ * would let thirty `views: 1` records outweigh one `views: 30` record six times
104
+ * over, defeating the sub-linear scaling in `computeCategoryAffinity` entirely.
105
+ * Merging first makes the score depend on how much someone looked, not on how
106
+ * their tracking pipeline happens to batch.
107
+ *
108
+ * Where records disagree on `weight`, the strongest wins.
109
+ */
110
+ function mergeViewsBySku(views) {
111
+ const totalsBySku = new Map();
112
+ for (const view of views) {
113
+ const running = totalsBySku.get(view.sku);
114
+ if (running) {
115
+ running.views += view.views;
116
+ if (view.dwellMs !== undefined)
117
+ running.dwellMs = (running.dwellMs ?? 0) + view.dwellMs;
118
+ if (view.category !== undefined)
119
+ running.category ??= view.category;
120
+ running.weight = Math.max(running.weight, effectiveWeight(view));
121
+ continue;
122
+ }
123
+ totalsBySku.set(view.sku, {
124
+ sku: view.sku,
125
+ views: view.views,
126
+ ...(view.dwellMs !== undefined ? { dwellMs: view.dwellMs } : {}),
127
+ ...(view.category !== undefined ? { category: view.category } : {}),
128
+ weight: effectiveWeight(view),
129
+ });
130
+ }
131
+ return [...totalsBySku.values()];
132
+ }
133
+ /** The most-viewed few, as the digest reports them. */
134
+ function mostViewedProducts(views) {
135
+ return mergeViewsBySku(views)
136
+ .toSorted((left, right) => right.views - left.views)
137
+ .slice(0, DIGEST_LIMITS.viewed)
138
+ .map(({ sku, views: viewCount, dwellMs }) => ({
139
+ sku,
140
+ views: viewCount,
141
+ ...(dwellMs !== undefined ? { dwellMs } : {}),
142
+ }));
143
+ }
144
+ /** The open-vocabulary long tail, reduced to "which kinds, and how often". */
145
+ function countByInteractionType(interactions) {
146
+ const countByType = new Map();
147
+ for (const interaction of interactions) {
148
+ countByType.set(interaction.type, (countByType.get(interaction.type) ?? 0) + 1);
149
+ }
150
+ return [...countByType.entries()]
151
+ .map(([type, count]) => ({ type, count }))
152
+ .toSorted((left, right) => right.count - left.count)
153
+ .slice(0, DIGEST_LIMITS.interactionTypes);
154
+ }
155
+ function recentPurchasedSkus(purchases) {
156
+ return recentUniqueSkus(purchases, DIGEST_LIMITS.purchased);
157
+ }
158
+ export function buildDigest(input) {
159
+ const candidatesBySku = new Map(input.candidates.map((product) => [product.sku, product]));
160
+ const { signals, context, user } = input;
161
+ const likedSkus = recentUniqueSkus(signals.likes, DIGEST_LIMITS.liked);
162
+ const dislikedSkus = recentUniqueSkus(signals.dislikes, DIGEST_LIMITS.disliked);
163
+ const purchasedSkus = recentPurchasedSkus(signals.lastPurchased);
164
+ const cartSkus = recentUniqueSkus(signals.cart, DIGEST_LIMITS.cart);
165
+ const topViewed = mostViewedProducts(signals.mostViewed);
166
+ // Searches say what a shopper wants; they do not say they engaged with any
167
+ // product, so they do not lift a shopper out of cold start on their own.
168
+ const evidenceCount = likedSkus.length +
169
+ dislikedSkus.length +
170
+ purchasedSkus.length +
171
+ cartSkus.length +
172
+ topViewed.length;
173
+ return {
174
+ userId: user.id,
175
+ ...(user.segment !== undefined ? { segment: user.segment } : {}),
176
+ isReturning: user.isReturning ?? purchasedSkus.length > 0,
177
+ surface: context.surface,
178
+ slot: context.slot,
179
+ locale: context.locale,
180
+ maxItems: context.maxItems,
181
+ ...(context.currentSku !== undefined ? { currentSku: context.currentSku } : {}),
182
+ ...(context.currentCategory !== undefined ? { currentCategory: context.currentCategory } : {}),
183
+ ...(context.searchQuery !== undefined ? { searchQuery: context.searchQuery } : {}),
184
+ likedSkus,
185
+ dislikedSkus,
186
+ purchasedSkus,
187
+ cartSkus,
188
+ topViewed,
189
+ recentSearches: signals.recentSearches.slice(0, DIGEST_LIMITS.searches),
190
+ categoryAffinity: computeCategoryAffinity(input, candidatesBySku),
191
+ interactionCounts: countByInteractionType(signals.interactions),
192
+ isColdStart: evidenceCount === 0,
193
+ };
194
+ }
195
+ // Everything the cohort key leaves out has to leave the prompt too, or the
196
+ // first shopper's searches and history end up shaping copy that is cached and
197
+ // served to everyone else in their cohort.
198
+ export function toCohortDigest(digest) {
199
+ const top = digest.categoryAffinity[0];
200
+ // Listed rather than spread, so what survives is the thing you read.
201
+ const cohort = {
202
+ userId: 'cohort',
203
+ isReturning: false,
204
+ surface: digest.surface,
205
+ slot: digest.slot,
206
+ locale: digest.locale,
207
+ maxItems: digest.maxItems,
208
+ likedSkus: [],
209
+ dislikedSkus: [],
210
+ purchasedSkus: [],
211
+ cartSkus: [],
212
+ topViewed: [],
213
+ recentSearches: [],
214
+ categoryAffinity: top ? [{ category: top.category, score: 0 }] : [],
215
+ interactionCounts: [],
216
+ isColdStart: digest.isColdStart,
217
+ };
218
+ if (digest.segment !== undefined)
219
+ cohort.segment = digest.segment;
220
+ if (digest.currentCategory !== undefined)
221
+ cohort.currentCategory = digest.currentCategory;
222
+ return cohort;
223
+ }
224
+ //# sourceMappingURL=signal-digest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signal-digest.js","sourceRoot":"","sources":["../src/signal-digest.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,88 @@
1
+ import { type GeneratedSpec } from './component-spec.js';
2
+ import type { SignalDigest } from './signal-digest.js';
3
+ /**
4
+ * Where a generated component is kept between requests.
5
+ *
6
+ * A generation is stable for as long as the shopper's signals are, so asking a
7
+ * model again on the next page view buys nothing and costs a round trip on the
8
+ * render path — which is the latency this whole design exists to avoid.
9
+ *
10
+ * The store is a port rather than an implementation. A single process can use
11
+ * the in-memory one; anything running more than one instance needs a shared
12
+ * store, or each instance keeps its own copy and the hit rate divides by the
13
+ * instance count.
14
+ */
15
+ /**
16
+ * Asynchronous on purpose. A synchronous `get` would look simpler and would
17
+ * make the shared store the docs recommend impossible to write, since no Redis
18
+ * or Memcached client can return a value without awaiting.
19
+ */
20
+ /**
21
+ * A stored generation, with the moment it was produced.
22
+ *
23
+ * The timestamp travels with the spec because it cannot be recovered later: a
24
+ * component served from cache is not newly generated, and `generatedAt` is the
25
+ * only way anything downstream can tell how stale what it is showing has become.
26
+ */
27
+ export interface CachedSpec {
28
+ spec: GeneratedSpec;
29
+ /** Epoch milliseconds at which the model produced this. */
30
+ generatedAt: number;
31
+ }
32
+ export interface SpecCache {
33
+ get(key: string): Promise<CachedSpec | undefined>;
34
+ set(key: string, cached: CachedSpec): Promise<void>;
35
+ }
36
+ export interface MemorySpecCacheOptions {
37
+ /** How long an entry stays valid. Defaults to 60 seconds. */
38
+ ttlMs?: number;
39
+ /** Hard ceiling on entries. Least recently read is evicted first. */
40
+ maxEntries?: number;
41
+ /** Injectable clock, so tests do not have to wait. */
42
+ now?: () => number;
43
+ }
44
+ /**
45
+ * A bounded in-process cache.
46
+ *
47
+ * `maxEntries` is generous relative to the default TTL for a reason: a ceiling
48
+ * low enough to evict entries before they expire silently turns a longer TTL
49
+ * into a setting that does nothing, and the person who raised it has no way to
50
+ * tell.
51
+ */
52
+ export declare function createMemorySpecCache(options?: MemorySpecCacheOptions): SpecCache;
53
+ /**
54
+ * A cache that stores nothing, so every request generates.
55
+ *
56
+ * This is not a stub. It is the control for any measurement of generation cost
57
+ * or latency: with a cache in front, a benchmark reports how often it hit, not
58
+ * what generating costs.
59
+ */
60
+ export declare function createNullSpecCache(): SpecCache;
61
+ /**
62
+ * Derives the cache key.
63
+ *
64
+ * The digest goes in **whole**, rather than as a hand-picked list of fields.
65
+ * That is the entire point of this function. The previous implementation listed
66
+ * the fields it thought mattered, and four of them drifted: they reached the
67
+ * model but not the key, so two shoppers with genuinely different histories
68
+ * collided and were served each other's component. Hashing the whole digest
69
+ * makes that class of bug unreachable — a field cannot be forgotten here,
70
+ * because nothing is named here.
71
+ *
72
+ * The cost is a lower hit rate than a hand-tuned key would give, since every
73
+ * signal now moves the key. That is the right way round: a key that is too
74
+ * specific wastes money, and a key that is too loose shows one shopper another
75
+ * shopper's page. Raising the hit rate means deliberately coarsening the digest
76
+ * itself, which is a decision to make against measurements rather than by
77
+ * guessing here.
78
+ *
79
+ * Candidates enter as SKUs only. Their titles, prices and ratings reach the
80
+ * model too, but including them would churn the key on every price change for
81
+ * copy that would almost always come back the same. The consequence is bounded
82
+ * and worth stating: within one TTL, a price change does not refresh the
83
+ * generated copy. It cannot show a stale price — prices are read from the live
84
+ * catalog at render time, never from the model.
85
+ */
86
+ export declare function specCacheKey(digest: SignalDigest, candidateSkus: readonly string[], providerId: string): string;
87
+ export declare function cohortCacheKey(digest: SignalDigest, candidateSkus: readonly string[], providerId: string): string;
88
+ //# sourceMappingURL=spec-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spec-cache.d.ts","sourceRoot":"","sources":["../src/spec-cache.ts"],"names":[],"mappings":"AACA,OAAO,EAAgB,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACvE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD;;;;;;;;;;;GAWG;AAEH;;;;GAIG;AACH;;;;;;GAMG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,aAAa,CAAC;IACpB,2DAA2D;IAC3D,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,SAAS;IACxB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC,CAAC;IAClD,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACrD;AAED,MAAM,WAAW,sBAAsB;IACrC,6DAA6D;IAC7D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sDAAsD;IACtD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAgBD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,GAAE,sBAA2B,GAAG,SAAS,CA+CrF;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,IAAI,SAAS,CAS/C;AAcD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,YAAY,EACpB,aAAa,EAAE,SAAS,MAAM,EAAE,EAChC,UAAU,EAAE,MAAM,GACjB,MAAM,CAWR;AAMD,wBAAgB,cAAc,CAC5B,MAAM,EAAE,YAAY,EACpB,aAAa,EAAE,SAAS,MAAM,EAAE,EAChC,UAAU,EAAE,MAAM,GACjB,MAAM,CAsBR"}
@@ -0,0 +1,152 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { SPEC_VERSION } from './component-spec.js';
3
+ function assertFiniteAtLeastZero(name, value) {
4
+ if (Number.isFinite(value) && value >= 0)
5
+ return;
6
+ const hint = Number.isNaN(value)
7
+ ? ' (a common cause is Number() on an environment variable that is not set)'
8
+ : '';
9
+ throw new RangeError(`${name} must be a finite number of at least 0, received ${value}${hint}`);
10
+ }
11
+ /**
12
+ * A bounded in-process cache.
13
+ *
14
+ * `maxEntries` is generous relative to the default TTL for a reason: a ceiling
15
+ * low enough to evict entries before they expire silently turns a longer TTL
16
+ * into a setting that does nothing, and the person who raised it has no way to
17
+ * tell.
18
+ */
19
+ export function createMemorySpecCache(options = {}) {
20
+ const ttlMs = options.ttlMs ?? 60_000;
21
+ const maxEntries = options.maxEntries ?? 10_000;
22
+ // Checked rather than trusted, because the way these are usually supplied is
23
+ // `Number(process.env.SOMETHING)`, and an unset or misspelled variable makes
24
+ // that NaN. Every comparison against NaN is false, so the cache would then
25
+ // never expire an entry and never evict one — it would grow forever while
26
+ // serving a shopper the component they were given last week, and nothing
27
+ // would report it. Failing at construction is the only loud option.
28
+ assertFiniteAtLeastZero('ttlMs', ttlMs);
29
+ assertFiniteAtLeastZero('maxEntries', maxEntries);
30
+ if (!Number.isSafeInteger(maxEntries)) {
31
+ throw new RangeError(`maxEntries must be a whole number, received ${maxEntries}`);
32
+ }
33
+ const now = options.now ?? Date.now;
34
+ const entries = new Map();
35
+ return {
36
+ async get(key) {
37
+ const entry = entries.get(key);
38
+ if (!entry)
39
+ return undefined;
40
+ if (entry.expiresAt <= now()) {
41
+ entries.delete(key);
42
+ return undefined;
43
+ }
44
+ // Re-insert so insertion order tracks recency of use, which is what makes
45
+ // the eviction below least-recently-used rather than oldest-written.
46
+ entries.delete(key);
47
+ entries.set(key, entry);
48
+ return entry.cached;
49
+ },
50
+ async set(key, cached) {
51
+ entries.delete(key);
52
+ entries.set(key, { cached, expiresAt: now() + ttlMs });
53
+ while (entries.size > maxEntries) {
54
+ const oldest = entries.keys().next();
55
+ if (oldest.done)
56
+ break;
57
+ entries.delete(oldest.value);
58
+ }
59
+ },
60
+ };
61
+ }
62
+ /**
63
+ * A cache that stores nothing, so every request generates.
64
+ *
65
+ * This is not a stub. It is the control for any measurement of generation cost
66
+ * or latency: with a cache in front, a benchmark reports how often it hit, not
67
+ * what generating costs.
68
+ */
69
+ export function createNullSpecCache() {
70
+ return {
71
+ async get() {
72
+ return undefined;
73
+ },
74
+ async set() {
75
+ // Deliberately nothing.
76
+ },
77
+ };
78
+ }
79
+ /** Stable JSON: object keys sorted, so key order in a digest cannot matter. */
80
+ function canonicalise(value) {
81
+ if (value === null || typeof value !== 'object')
82
+ return JSON.stringify(value) ?? 'null';
83
+ if (Array.isArray(value))
84
+ return `[${value.map(canonicalise).join(',')}]`;
85
+ const entries = Object.entries(value)
86
+ .filter(([, fieldValue]) => fieldValue !== undefined)
87
+ .toSorted(([left], [right]) => (left < right ? -1 : 1));
88
+ return `{${entries.map(([name, fieldValue]) => `${JSON.stringify(name)}:${canonicalise(fieldValue)}`).join(',')}}`;
89
+ }
90
+ /**
91
+ * Derives the cache key.
92
+ *
93
+ * The digest goes in **whole**, rather than as a hand-picked list of fields.
94
+ * That is the entire point of this function. The previous implementation listed
95
+ * the fields it thought mattered, and four of them drifted: they reached the
96
+ * model but not the key, so two shoppers with genuinely different histories
97
+ * collided and were served each other's component. Hashing the whole digest
98
+ * makes that class of bug unreachable — a field cannot be forgotten here,
99
+ * because nothing is named here.
100
+ *
101
+ * The cost is a lower hit rate than a hand-tuned key would give, since every
102
+ * signal now moves the key. That is the right way round: a key that is too
103
+ * specific wastes money, and a key that is too loose shows one shopper another
104
+ * shopper's page. Raising the hit rate means deliberately coarsening the digest
105
+ * itself, which is a decision to make against measurements rather than by
106
+ * guessing here.
107
+ *
108
+ * Candidates enter as SKUs only. Their titles, prices and ratings reach the
109
+ * model too, but including them would churn the key on every price change for
110
+ * copy that would almost always come back the same. The consequence is bounded
111
+ * and worth stating: within one TTL, a price change does not refresh the
112
+ * generated copy. It cannot show a stale price — prices are read from the live
113
+ * catalog at render time, never from the model.
114
+ */
115
+ export function specCacheKey(digest, candidateSkus, providerId) {
116
+ const material = canonicalise({
117
+ // The spec's own version, so a shape change cannot read entries written by
118
+ // the previous shape out of a shared store that outlives a deploy.
119
+ specVersion: SPEC_VERSION,
120
+ provider: providerId,
121
+ digest,
122
+ candidates: candidateSkus.toSorted(),
123
+ });
124
+ return createHash('sha256').update(material).digest('hex').slice(0, 32);
125
+ }
126
+ // Leaves out the fields that make a key personal: who the shopper is, what they
127
+ // liked, viewed or searched for. Candidates go too, because every shopper's list
128
+ // is different — that is safe because the SKUs in a cohort spec get replaced
129
+ // before the page is served.
130
+ export function cohortCacheKey(digest, candidateSkus, providerId) {
131
+ const material = canonicalise({
132
+ specVersion: SPEC_VERSION,
133
+ provider: providerId,
134
+ segment: digest.segment ?? null,
135
+ surface: digest.surface,
136
+ slot: digest.slot,
137
+ locale: digest.locale,
138
+ maxItems: digest.maxItems,
139
+ isColdStart: digest.isColdStart,
140
+ // The page being looked at, so copy written for a backpack page is not
141
+ // served on a tent page.
142
+ currentCategory: digest.currentCategory ?? null,
143
+ topCategory: digest.categoryAffinity[0]?.category ?? null,
144
+ // The model is shown these, so they belong in the key. Normally they come
145
+ // from the page and everyone on it shares them. A shop that picks
146
+ // candidates per shopper gets smaller cohorts, which is the honest result:
147
+ // its prompt really is personal.
148
+ candidates: candidateSkus.toSorted(),
149
+ });
150
+ return createHash('sha256').update(material).digest('hex').slice(0, 32);
151
+ }
152
+ //# sourceMappingURL=spec-cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spec-cache.js","sourceRoot":"","sources":["../src/spec-cache.ts"],"names":[],"mappings":"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"}