@rudra-js/core 0.6.0 → 0.7.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.
- package/README.md +13 -12
- package/dist/claim-screening.d.ts +0 -23
- package/dist/claim-screening.d.ts.map +1 -1
- package/dist/claim-screening.js +0 -69
- package/dist/claim-screening.js.map +1 -1
- package/dist/component-generator.d.ts +0 -70
- package/dist/component-generator.d.ts.map +1 -1
- package/dist/component-generator.js +10 -52
- package/dist/component-generator.js.map +1 -1
- package/dist/component-spec.d.ts +2 -110
- package/dist/component-spec.d.ts.map +1 -1
- package/dist/component-spec.js +0 -78
- package/dist/component-spec.js.map +1 -1
- package/dist/fallback-component.d.ts +0 -9
- package/dist/fallback-component.d.ts.map +1 -1
- package/dist/fallback-component.js +0 -11
- package/dist/fallback-component.js.map +1 -1
- package/dist/fit-to-shopper.d.ts.map +1 -1
- package/dist/fit-to-shopper.js +1 -11
- package/dist/fit-to-shopper.js.map +1 -1
- package/dist/index.d.ts +0 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -7
- package/dist/index.js.map +1 -1
- package/dist/model-prompt.d.ts +0 -29
- package/dist/model-prompt.d.ts.map +1 -1
- package/dist/model-prompt.js +0 -79
- package/dist/model-prompt.js.map +1 -1
- package/dist/product-selection.d.ts +0 -23
- package/dist/product-selection.d.ts.map +1 -1
- package/dist/product-selection.js +0 -19
- package/dist/product-selection.js.map +1 -1
- package/dist/provider.d.ts +0 -48
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +0 -5
- package/dist/provider.js.map +1 -1
- package/dist/reconciliation.d.ts +1 -44
- package/dist/reconciliation.d.ts.map +1 -1
- package/dist/reconciliation.js +2 -109
- package/dist/reconciliation.js.map +1 -1
- package/dist/signal-digest.d.ts +0 -27
- package/dist/signal-digest.d.ts.map +1 -1
- package/dist/signal-digest.js +1 -50
- package/dist/signal-digest.js.map +1 -1
- package/dist/spec-cache.d.ts +6 -80
- package/dist/spec-cache.d.ts.map +1 -1
- package/dist/spec-cache.js +1 -54
- package/dist/spec-cache.js.map +1 -1
- package/dist/tracking-input.d.ts +0 -49
- package/dist/tracking-input.d.ts.map +1 -1
- package/dist/tracking-input.js +0 -102
- package/dist/tracking-input.js.map +1 -1
- package/package.json +7 -6
- package/src/claim-screening.ts +17 -69
- package/src/component-generator.ts +24 -133
- package/src/component-spec.ts +15 -103
- package/src/fallback-component.ts +0 -11
- package/src/fit-to-shopper.ts +3 -12
- package/src/index.ts +0 -8
- package/src/model-prompt.ts +1 -82
- package/src/product-selection.ts +5 -43
- package/src/provider.ts +4 -53
- package/src/reconciliation.ts +12 -112
- package/src/signal-digest.ts +5 -64
- package/src/spec-cache.ts +8 -98
- package/src/tracking-input.ts +11 -108
package/src/tracking-input.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|