@rudra-js/core 0.1.0 → 0.2.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.
@@ -230,14 +230,17 @@ function mergeViewsBySku(views: ViewSignal[]): MergedView[] {
230
230
 
231
231
  /** The most-viewed few, as the digest reports them. */
232
232
  function mostViewedProducts(views: ViewSignal[]): ViewedProduct[] {
233
- return mergeViewsBySku(views)
233
+ const top = mergeViewsBySku(views)
234
234
  .toSorted((left, right) => right.views - left.views)
235
- .slice(0, DIGEST_LIMITS.viewed)
236
- .map(({ sku, views: viewCount, dwellMs }) => ({
237
- sku,
238
- views: viewCount,
239
- ...(dwellMs !== undefined ? { dwellMs } : {}),
240
- }));
235
+ .slice(0, DIGEST_LIMITS.viewed);
236
+
237
+ const products: ViewedProduct[] = [];
238
+ for (const merged of top) {
239
+ const product: ViewedProduct = { sku: merged.sku, views: merged.views };
240
+ if (merged.dwellMs !== undefined) product.dwellMs = merged.dwellMs;
241
+ products.push(product);
242
+ }
243
+ return products;
241
244
  }
242
245
 
243
246
  /** The open-vocabulary long tail, reduced to "which kinds, and how often". */
package/src/spec-cache.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { SPEC_VERSION, type GeneratedSpec } from './component-spec.js';
3
+ import { SYSTEM_PROMPT } from './model-prompt.js';
3
4
  import type { SignalDigest } from './signal-digest.js';
4
5
 
5
6
  /**
@@ -15,11 +16,6 @@ import type { SignalDigest } from './signal-digest.js';
15
16
  * instance count.
16
17
  */
17
18
 
18
- /**
19
- * Asynchronous on purpose. A synchronous `get` would look simpler and would
20
- * make the shared store the docs recommend impossible to write, since no Redis
21
- * or Memcached client can return a value without awaiting.
22
- */
23
19
  /**
24
20
  * A stored generation, with the moment it was produced.
25
21
  *
@@ -33,9 +29,15 @@ export interface CachedSpec {
33
29
  generatedAt: number;
34
30
  }
35
31
 
32
+ /**
33
+ * Asynchronous on purpose. A synchronous `get` would look simpler and would
34
+ * make the shared store the docs recommend impossible to write, since no Redis
35
+ * or Memcached client can return a value without awaiting.
36
+ */
36
37
  export interface SpecCache {
37
38
  get(key: string): Promise<CachedSpec | undefined>;
38
39
  set(key: string, cached: CachedSpec): Promise<void>;
40
+ delete?(key: string): Promise<void>;
39
41
  }
40
42
 
41
43
  export interface MemorySpecCacheOptions {
@@ -115,6 +117,10 @@ export function createMemorySpecCache(options: MemorySpecCacheOptions = {}): Spe
115
117
  entries.delete(oldest.value);
116
118
  }
117
119
  },
120
+
121
+ async delete(key) {
122
+ entries.delete(key);
123
+ },
118
124
  };
119
125
  }
120
126
 
@@ -148,6 +154,8 @@ function canonicalise(value: unknown): string {
148
154
  return `{${entries.map(([name, fieldValue]) => `${JSON.stringify(name)}:${canonicalise(fieldValue)}`).join(',')}}`;
149
155
  }
150
156
 
157
+ const PROMPT_FINGERPRINT = createHash('sha256').update(SYSTEM_PROMPT).digest('hex').slice(0, 16);
158
+
151
159
  /**
152
160
  * Derives the cache key.
153
161
  *
@@ -182,6 +190,7 @@ export function specCacheKey(
182
190
  // The spec's own version, so a shape change cannot read entries written by
183
191
  // the previous shape out of a shared store that outlives a deploy.
184
192
  specVersion: SPEC_VERSION,
193
+ prompt: PROMPT_FINGERPRINT,
185
194
  provider: providerId,
186
195
  digest,
187
196
  candidates: candidateSkus.toSorted(),
@@ -201,6 +210,7 @@ export function cohortCacheKey(
201
210
  ): string {
202
211
  const material = canonicalise({
203
212
  specVersion: SPEC_VERSION,
213
+ prompt: PROMPT_FINGERPRINT,
204
214
  provider: providerId,
205
215
  segment: digest.segment ?? null,
206
216
  surface: digest.surface,
@@ -42,6 +42,8 @@ export const FIELD_LIMITS = {
42
42
  candidates: 200,
43
43
  productsPerBundle: 5,
44
44
  bundles: 20,
45
+ localeTag: 35,
46
+ maxItems: 12,
45
47
  } as const;
46
48
 
47
49
  /** Assigning this as an object key mutates the prototype instead of the object. */
@@ -179,9 +181,15 @@ export const renderContextSchema = z.strictObject({
179
181
  currentSku: optionalIdentifier(),
180
182
  currentCategory: optionalIdentifier(),
181
183
  searchQuery: z.string().max(FIELD_LIMITS.searchQuery).optional(),
182
- locale: z.string().min(2).max(35).default('en-US'),
184
+ // One language tag, not the Accept-Language header it is often copied from:
185
+ // the locale is part of the cohort cache key, so a list makes its own cohort.
186
+ locale: z
187
+ .string()
188
+ .max(FIELD_LIMITS.localeTag)
189
+ .regex(/^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*$/, 'expected one language tag, such as en-US')
190
+ .default('en-US'),
183
191
  /** Upper bound on products across the whole generated component. */
184
- maxItems: z.number().int().min(1).max(12).default(4),
192
+ maxItems: z.number().int().min(1).max(FIELD_LIMITS.maxItems).default(4),
185
193
  });
186
194
  export type RenderContext = z.infer<typeof renderContextSchema>;
187
195