@rudra-js/core 0.6.0 → 0.7.1

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 (66) hide show
  1. package/README.md +13 -12
  2. package/dist/claim-screening.d.ts +0 -23
  3. package/dist/claim-screening.d.ts.map +1 -1
  4. package/dist/claim-screening.js +0 -69
  5. package/dist/claim-screening.js.map +1 -1
  6. package/dist/component-generator.d.ts +0 -70
  7. package/dist/component-generator.d.ts.map +1 -1
  8. package/dist/component-generator.js +10 -52
  9. package/dist/component-generator.js.map +1 -1
  10. package/dist/component-spec.d.ts +2 -110
  11. package/dist/component-spec.d.ts.map +1 -1
  12. package/dist/component-spec.js +0 -78
  13. package/dist/component-spec.js.map +1 -1
  14. package/dist/fallback-component.d.ts +0 -9
  15. package/dist/fallback-component.d.ts.map +1 -1
  16. package/dist/fallback-component.js +0 -11
  17. package/dist/fallback-component.js.map +1 -1
  18. package/dist/fit-to-shopper.d.ts.map +1 -1
  19. package/dist/fit-to-shopper.js +1 -11
  20. package/dist/fit-to-shopper.js.map +1 -1
  21. package/dist/index.d.ts +0 -7
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +0 -7
  24. package/dist/index.js.map +1 -1
  25. package/dist/model-prompt.d.ts +0 -29
  26. package/dist/model-prompt.d.ts.map +1 -1
  27. package/dist/model-prompt.js +0 -79
  28. package/dist/model-prompt.js.map +1 -1
  29. package/dist/product-selection.d.ts +0 -23
  30. package/dist/product-selection.d.ts.map +1 -1
  31. package/dist/product-selection.js +0 -19
  32. package/dist/product-selection.js.map +1 -1
  33. package/dist/provider.d.ts +0 -48
  34. package/dist/provider.d.ts.map +1 -1
  35. package/dist/provider.js +0 -5
  36. package/dist/provider.js.map +1 -1
  37. package/dist/reconciliation.d.ts +1 -44
  38. package/dist/reconciliation.d.ts.map +1 -1
  39. package/dist/reconciliation.js +2 -109
  40. package/dist/reconciliation.js.map +1 -1
  41. package/dist/signal-digest.d.ts +0 -27
  42. package/dist/signal-digest.d.ts.map +1 -1
  43. package/dist/signal-digest.js +1 -50
  44. package/dist/signal-digest.js.map +1 -1
  45. package/dist/spec-cache.d.ts +6 -80
  46. package/dist/spec-cache.d.ts.map +1 -1
  47. package/dist/spec-cache.js +1 -54
  48. package/dist/spec-cache.js.map +1 -1
  49. package/dist/tracking-input.d.ts +0 -49
  50. package/dist/tracking-input.d.ts.map +1 -1
  51. package/dist/tracking-input.js +0 -102
  52. package/dist/tracking-input.js.map +1 -1
  53. package/package.json +8 -7
  54. package/src/claim-screening.ts +17 -69
  55. package/src/component-generator.ts +24 -133
  56. package/src/component-spec.ts +15 -103
  57. package/src/fallback-component.ts +0 -11
  58. package/src/fit-to-shopper.ts +3 -12
  59. package/src/index.ts +0 -8
  60. package/src/model-prompt.ts +1 -82
  61. package/src/product-selection.ts +5 -43
  62. package/src/provider.ts +4 -53
  63. package/src/reconciliation.ts +12 -112
  64. package/src/signal-digest.ts +5 -64
  65. package/src/spec-cache.ts +8 -98
  66. package/src/tracking-input.ts +11 -108
@@ -1,36 +1,5 @@
1
1
  import { z } from 'zod';
2
2
 
3
- /**
4
- * The tracking-input contract — the boundary between the host application and
5
- * rudra-js.
6
- *
7
- * rudra-js does not collect, store, or aggregate anything. The host owns its
8
- * tracking pipeline (an event stream, a CDP, a warehouse) and hands the
9
- * framework one JSON object per render. This module is that contract: one
10
- * schema, validated at the edge, so a malformed payload fails loudly here
11
- * rather than quietly producing a bad prompt several layers later.
12
- *
13
- * Every fixed-shape object below is a `strictObject`, so an unrecognised field
14
- * is an error rather than being dropped. That matters more than it looks: with
15
- * a lenient object, a host that misspells `recentSearches` gets a shopper who
16
- * silently looks like a first-time visitor, and nothing anywhere reports it.
17
- * The only dynamic shape is `interaction.meta`, which is a record by design.
18
- */
19
-
20
- /**
21
- * Every free-text field and every array is length-capped.
22
- *
23
- * These caps are not cosmetic. Host-supplied strings end up inside the prompt
24
- * we send to a language model, and a model is billed per token — so an
25
- * unbounded string is an unbounded bill, and an unbounded array of candidates
26
- * is the same problem multiplied.
27
- *
28
- * What this buys is that no single field is unbounded. It is deliberately not
29
- * an aggregate budget: the caps multiply out to far more than any context
30
- * window, because rejecting a large-but-legitimate payload is the wrong
31
- * response to one. Fitting a payload into a prompt is `digest`'s job, and it
32
- * trims rather than throws.
33
- */
34
3
  export const FIELD_LIMITS = {
35
4
  identifier: 128,
36
5
  shortText: 200,
@@ -44,12 +13,10 @@ export const FIELD_LIMITS = {
44
13
  bundles: 20,
45
14
  localeTag: 35,
46
15
  maxItems: 12,
47
- // Matches CLAMP.reason in reconciliation: a host reason is rendered in the
48
- // same place a model's is, so the same length has to hold.
16
+
49
17
  reason: 120,
50
18
  } as const;
51
19
 
52
- /** Assigning this as an object key mutates the prototype instead of the object. */
53
20
  const RESERVED_META_KEY = '__proto__';
54
21
 
55
22
  const MAX_EPOCH_MS = Date.UTC(2100, 0, 1);
@@ -57,32 +24,13 @@ const MAX_EPOCH_MS = Date.UTC(2100, 0, 1);
57
24
  const identifier = () => z.string().min(1).max(FIELD_LIMITS.identifier);
58
25
  const optionalIdentifier = () => z.string().min(1).max(FIELD_LIMITS.identifier).optional();
59
26
 
60
- /**
61
- * Epoch milliseconds, used only for recency ordering. Bounded because a
62
- * negative or year-3000 timestamp does not fail anywhere downstream — it just
63
- * sorts to one end and silently reorders the shopper's history.
64
- */
65
27
  const epochMs = () => z.number().int().min(0).max(MAX_EPOCH_MS).optional();
66
28
 
67
- /**
68
- * Whether a path stays on the page's own origin.
69
- *
70
- * Read the way a browser reads it, not the way it looks. A browser drops tab,
71
- * carriage return and newline from a URL before parsing it, and treats a
72
- * backslash as a slash, so `/\evil.example/x.png` and `/<tab>//evil.example/x.png`
73
- * both point at someone else's host while passing a plain "starts with one
74
- * slash" test.
75
- */
76
29
  const isRootRelative = (value: string) => {
77
30
  const asParsed = value.replace(/[\t\n\r]/g, '');
78
31
  return value.startsWith('/') && /^\/(?![/\\])/.test(asParsed);
79
32
  };
80
33
 
81
- /**
82
- * An absolute http(s) URL, or a root-relative path such as `/images/tr-102.png`.
83
- * Anything else — a bare word, a `javascript:` URI, a protocol-relative `//host`
84
- * — is rejected before it can reach an `<img src>`.
85
- */
86
34
  const imageReference = () =>
87
35
  z
88
36
  .string()
@@ -93,7 +41,6 @@ const imageReference = () =>
93
41
  { message: 'expected an http(s) URL or a root-relative path' },
94
42
  );
95
43
 
96
- /** A product the generated component is permitted to place. */
97
44
  export const productSchema = z.strictObject({
98
45
  sku: identifier(),
99
46
  title: z.string().min(1).max(FIELD_LIMITS.shortText),
@@ -103,15 +50,10 @@ export const productSchema = z.strictObject({
103
50
  .string()
104
51
  .regex(/^[A-Z]{3}$/, 'expected a three-letter ISO 4217 code')
105
52
  .default('USD'),
106
- // This lands in an `<img src>` the host did not write, so the scheme matters:
107
- // a bare capped string would accept '' and 'javascript:'. Root-relative paths
108
- // are allowed because most catalogs store images that way, and they carry no
109
- // scheme to abuse.
53
+
110
54
  imageUrl: imageReference().optional(),
111
55
  rating: z.number().min(0).max(5).optional(),
112
- // Your phrase for why this product is here, when your own ranking already has
113
- // one. It renders as the item's reason and is not screened: these are your
114
- // words about your product, like the title, so you stand behind them.
56
+
115
57
  reason: z.string().min(1).max(FIELD_LIMITS.reason).optional(),
116
58
  isInStock: z.boolean().default(true),
117
59
  tags: z
@@ -121,12 +63,11 @@ export const productSchema = z.strictObject({
121
63
  });
122
64
  export type Product = z.infer<typeof productSchema>;
123
65
 
124
- /** Base shape for any signal that points at a single SKU. */
125
66
  export const skuSignalSchema = z.strictObject({
126
67
  sku: identifier(),
127
68
  category: optionalIdentifier(),
128
69
  at: epochMs(),
129
- /** Caller-supplied strength, 0..1. Defaults to 1 when absent. */
70
+
130
71
  weight: z.number().min(0).max(1).optional(),
131
72
  });
132
73
  export type SkuSignal = z.infer<typeof skuSignalSchema>;
@@ -143,21 +84,12 @@ export const purchaseSignalSchema = skuSignalSchema.extend({
143
84
  });
144
85
  export type PurchaseSignal = z.infer<typeof purchaseSignalSchema>;
145
86
 
146
- /**
147
- * `meta` is the one dynamic shape in the contract, so its key is checked before
148
- * the record is parsed rather than after. Zod builds its result by assigning
149
- * each key, and assigning `__proto__` sets the prototype instead of adding a
150
- * key — so the entry would vanish from the parsed output with no error, which
151
- * is the silent drop this module exists to prevent.
152
- */
153
87
  const metaKeysAreSafe = z.custom<Record<string, string | number | boolean>>(
154
88
  (value) =>
155
89
  typeof value === 'object' && value !== null && !Object.hasOwn(value, RESERVED_META_KEY),
156
90
  { message: `meta may not use the reserved key ${RESERVED_META_KEY}` },
157
91
  );
158
92
 
159
- // `z.record` bounds key and value shape but not how many entries a record may
160
- // carry, so the count is checked separately.
161
93
  const metaSchema = metaKeysAreSafe.pipe(
162
94
  z
163
95
  .record(
@@ -169,11 +101,6 @@ const metaSchema = metaKeysAreSafe.pipe(
169
101
  }),
170
102
  );
171
103
 
172
- /**
173
- * Catch-all for everything else the shopper did. `type` is an open vocabulary
174
- * on purpose — 'scroll_depth', 'wishlist', 'filter_applied', whatever the host
175
- * already emits — so hosts do not have to map their events onto ours.
176
- */
177
104
  export const interactionSchema = z.strictObject({
178
105
  type: identifier(),
179
106
  sku: optionalIdentifier(),
@@ -184,23 +111,20 @@ export const interactionSchema = z.strictObject({
184
111
  });
185
112
  export type Interaction = z.infer<typeof interactionSchema>;
186
113
 
187
- /** Where on the site this component is being rendered. */
188
114
  export const renderContextSchema = z.strictObject({
189
- /** 'pdp', 'home', 'cart', 'search', or any host-defined surface. */
190
115
  surface: identifier(),
191
- /** Named placement, e.g. 'below-fold-recommendations'. */
116
+
192
117
  slot: z.string().min(1).max(FIELD_LIMITS.identifier).default('recommendations'),
193
118
  currentSku: optionalIdentifier(),
194
119
  currentCategory: optionalIdentifier(),
195
120
  searchQuery: z.string().max(FIELD_LIMITS.searchQuery).optional(),
196
- // One language tag, not the Accept-Language header it is often copied from:
197
- // the locale is part of the cohort cache key, so a list makes its own cohort.
121
+
198
122
  locale: z
199
123
  .string()
200
124
  .max(FIELD_LIMITS.localeTag)
201
125
  .regex(/^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*$/, 'expected one language tag, such as en-US')
202
126
  .default('en-US'),
203
- /** Upper bound on products across the whole generated component. */
127
+
204
128
  maxItems: z.number().int().min(1).max(FIELD_LIMITS.maxItems).default(4),
205
129
  });
206
130
  export type RenderContext = z.infer<typeof renderContextSchema>;
@@ -219,7 +143,6 @@ export const trackingSignalsSchema = z.strictObject({
219
143
  });
220
144
  export type TrackingSignals = z.infer<typeof trackingSignalsSchema>;
221
145
 
222
- /** A set the shop sells together. The model never picks or invents one. */
223
146
  export const bundleSchema = z.strictObject({
224
147
  id: identifier(),
225
148
  skus: z
@@ -229,8 +152,7 @@ export const bundleSchema = z.strictObject({
229
152
  .refine((skus) => new Set(skus).size === skus.length, {
230
153
  message: 'a bundle must not list the same product twice',
231
154
  }),
232
- // The shop's price for the set — not derived from the parts, so the set says
233
- // which money it is in rather than borrowing it from a member.
155
+
234
156
  price: z.number().nonnegative(),
235
157
  currency: z
236
158
  .string()
@@ -249,17 +171,9 @@ export const trackingInputSchema = z
249
171
  isReturning: z.boolean().optional(),
250
172
  }),
251
173
  context: renderContextSchema,
252
- // A payload with no `signals` block at all is the cold-start case, not an error.
174
+
253
175
  signals: trackingSignalsSchema.prefault({}),
254
- /**
255
- * The only products the generated component may place. Merchandising rules
256
- * belong here: whatever the host leaves out cannot be recommended, which is
257
- * what makes it impossible to surface a product that does not exist or is not
258
- * merchandised for this shopper.
259
- *
260
- * SKUs must be unique — a duplicate is a host bug that spends prompt budget
261
- * twice and invites the same product in two slots.
262
- */
176
+
263
177
  candidates: z
264
178
  .array(productSchema)
265
179
  .min(1)
@@ -270,8 +184,7 @@ export const trackingInputSchema = z
270
184
  message: 'candidates must have unique SKUs',
271
185
  },
272
186
  ),
273
- // Ids must be unique — the renderer looks a bundle up by id alone, so a
274
- // duplicate would let it draw the wrong set at the wrong price.
187
+
275
188
  bundles: z
276
189
  .array(bundleSchema)
277
190
  .max(FIELD_LIMITS.bundles)
@@ -295,24 +208,14 @@ export const trackingInputSchema = z
295
208
 
296
209
  export type TrackingInput = z.infer<typeof trackingInputSchema>;
297
210
 
298
- /** The shape a caller passes in, before defaults are applied. */
299
211
  export type TrackingInputDraft = z.input<typeof trackingInputSchema>;
300
212
 
301
- /**
302
- * The result of a non-throwing parse. Exported so a consumer can type a
303
- * validation failure without depending on zod directly.
304
- */
305
213
  export type TrackingInputResult = z.ZodSafeParseResult<TrackingInput>;
306
214
 
307
- /**
308
- * Validates a payload, throwing a `ZodError` if it does not satisfy the
309
- * contract. An invalid payload is a caller bug, and it should be loud.
310
- */
311
215
  export function parseTrackingInput(value: unknown): TrackingInput {
312
216
  return trackingInputSchema.parse(value);
313
217
  }
314
218
 
315
- /** Non-throwing variant, for callers that want to inspect the failure. */
316
219
  export function safeParseTrackingInput(value: unknown): TrackingInputResult {
317
220
  return trackingInputSchema.safeParse(value);
318
221
  }