@rudra-js/core 0.5.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 +64 -16
- package/dist/claim-screening.d.ts +10 -0
- package/dist/claim-screening.d.ts.map +1 -0
- package/dist/claim-screening.js +157 -0
- package/dist/claim-screening.js.map +1 -0
- package/dist/component-generator.d.ts +0 -70
- package/dist/component-generator.d.ts.map +1 -1
- package/dist/component-generator.js +27 -135
- package/dist/component-generator.js.map +1 -1
- package/dist/component-spec.d.ts +4 -159
- package/dist/component-spec.d.ts.map +1 -1
- package/dist/component-spec.js +0 -92
- package/dist/component-spec.js.map +1 -1
- package/dist/fallback-component.d.ts +0 -6
- package/dist/fallback-component.d.ts.map +1 -1
- package/dist/fallback-component.js +0 -21
- package/dist/fallback-component.js.map +1 -1
- package/dist/fit-to-shopper.d.ts +5 -1
- package/dist/fit-to-shopper.d.ts.map +1 -1
- package/dist/fit-to-shopper.js +5 -14
- package/dist/fit-to-shopper.js.map +1 -1
- package/dist/index.d.ts +1 -8
- 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 +1 -30
- package/dist/model-prompt.d.ts.map +1 -1
- package/dist/model-prompt.js +19 -78
- package/dist/model-prompt.js.map +1 -1
- package/dist/product-selection.d.ts +0 -29
- package/dist/product-selection.d.ts.map +1 -1
- package/dist/product-selection.js +1 -22
- package/dist/product-selection.js.map +1 -1
- package/dist/provider.d.ts +0 -55
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +0 -12
- package/dist/provider.js.map +1 -1
- package/dist/reconciliation.d.ts +6 -61
- package/dist/reconciliation.d.ts.map +1 -1
- package/dist/reconciliation.js +76 -466
- package/dist/reconciliation.js.map +1 -1
- package/dist/signal-digest.d.ts +1 -18
- package/dist/signal-digest.d.ts.map +1 -1
- package/dist/signal-digest.js +27 -66
- package/dist/signal-digest.js.map +1 -1
- package/dist/spec-cache.d.ts +8 -70
- package/dist/spec-cache.d.ts.map +1 -1
- package/dist/spec-cache.js +5 -68
- 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 +5 -105
- package/dist/tracking-input.js.map +1 -1
- package/package.json +7 -6
- package/src/claim-screening.ts +190 -0
- package/src/component-generator.ts +45 -240
- package/src/component-spec.ts +17 -118
- package/src/fallback-component.ts +0 -22
- package/src/fit-to-shopper.ts +12 -16
- package/src/index.ts +1 -8
- package/src/model-prompt.ts +25 -84
- package/src/product-selection.ts +6 -52
- package/src/provider.ts +4 -60
- package/src/reconciliation.ts +104 -499
- package/src/signal-digest.ts +34 -83
- package/src/spec-cache.ts +10 -97
- package/src/tracking-input.ts +17 -111
package/dist/tracking-input.d.ts
CHANGED
|
@@ -1,34 +1,4 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
/**
|
|
3
|
-
* The tracking-input contract — the boundary between the host application and
|
|
4
|
-
* rudra-js.
|
|
5
|
-
*
|
|
6
|
-
* rudra-js does not collect, store, or aggregate anything. The host owns its
|
|
7
|
-
* tracking pipeline (an event stream, a CDP, a warehouse) and hands the
|
|
8
|
-
* framework one JSON object per render. This module is that contract: one
|
|
9
|
-
* schema, validated at the edge, so a malformed payload fails loudly here
|
|
10
|
-
* rather than quietly producing a bad prompt several layers later.
|
|
11
|
-
*
|
|
12
|
-
* Every fixed-shape object below is a `strictObject`, so an unrecognised field
|
|
13
|
-
* is an error rather than being dropped. That matters more than it looks: with
|
|
14
|
-
* a lenient object, a host that misspells `recentSearches` gets a shopper who
|
|
15
|
-
* silently looks like a first-time visitor, and nothing anywhere reports it.
|
|
16
|
-
* The only dynamic shape is `interaction.meta`, which is a record by design.
|
|
17
|
-
*/
|
|
18
|
-
/**
|
|
19
|
-
* Every free-text field and every array is length-capped.
|
|
20
|
-
*
|
|
21
|
-
* These caps are not cosmetic. Host-supplied strings end up inside the prompt
|
|
22
|
-
* we send to a language model, and a model is billed per token — so an
|
|
23
|
-
* unbounded string is an unbounded bill, and an unbounded array of candidates
|
|
24
|
-
* is the same problem multiplied.
|
|
25
|
-
*
|
|
26
|
-
* What this buys is that no single field is unbounded. It is deliberately not
|
|
27
|
-
* an aggregate budget: the caps multiply out to far more than any context
|
|
28
|
-
* window, because rejecting a large-but-legitimate payload is the wrong
|
|
29
|
-
* response to one. Fitting a payload into a prompt is `digest`'s job, and it
|
|
30
|
-
* trims rather than throws.
|
|
31
|
-
*/
|
|
32
2
|
export declare const FIELD_LIMITS: {
|
|
33
3
|
readonly identifier: 128;
|
|
34
4
|
readonly shortText: 200;
|
|
@@ -44,7 +14,6 @@ export declare const FIELD_LIMITS: {
|
|
|
44
14
|
readonly maxItems: 12;
|
|
45
15
|
readonly reason: 120;
|
|
46
16
|
};
|
|
47
|
-
/** A product the generated component is permitted to place. */
|
|
48
17
|
export declare const productSchema: z.ZodObject<{
|
|
49
18
|
sku: z.ZodString;
|
|
50
19
|
title: z.ZodString;
|
|
@@ -58,7 +27,6 @@ export declare const productSchema: z.ZodObject<{
|
|
|
58
27
|
tags: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
59
28
|
}, z.core.$strict>;
|
|
60
29
|
export type Product = z.infer<typeof productSchema>;
|
|
61
|
-
/** Base shape for any signal that points at a single SKU. */
|
|
62
30
|
export declare const skuSignalSchema: z.ZodObject<{
|
|
63
31
|
sku: z.ZodString;
|
|
64
32
|
category: z.ZodOptional<z.ZodString>;
|
|
@@ -84,11 +52,6 @@ export declare const purchaseSignalSchema: z.ZodObject<{
|
|
|
84
52
|
price: z.ZodOptional<z.ZodNumber>;
|
|
85
53
|
}, z.core.$strict>;
|
|
86
54
|
export type PurchaseSignal = z.infer<typeof purchaseSignalSchema>;
|
|
87
|
-
/**
|
|
88
|
-
* Catch-all for everything else the shopper did. `type` is an open vocabulary
|
|
89
|
-
* on purpose — 'scroll_depth', 'wishlist', 'filter_applied', whatever the host
|
|
90
|
-
* already emits — so hosts do not have to map their events onto ours.
|
|
91
|
-
*/
|
|
92
55
|
export declare const interactionSchema: z.ZodObject<{
|
|
93
56
|
type: z.ZodString;
|
|
94
57
|
sku: z.ZodOptional<z.ZodString>;
|
|
@@ -98,7 +61,6 @@ export declare const interactionSchema: z.ZodObject<{
|
|
|
98
61
|
meta: z.ZodOptional<z.ZodPipe<z.ZodCustom<Record<string, string | number | boolean>, Record<string, string | number | boolean>>, z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>>>;
|
|
99
62
|
}, z.core.$strict>;
|
|
100
63
|
export type Interaction = z.infer<typeof interactionSchema>;
|
|
101
|
-
/** Where on the site this component is being rendered. */
|
|
102
64
|
export declare const renderContextSchema: z.ZodObject<{
|
|
103
65
|
surface: z.ZodString;
|
|
104
66
|
slot: z.ZodDefault<z.ZodString>;
|
|
@@ -155,7 +117,6 @@ export declare const trackingSignalsSchema: z.ZodObject<{
|
|
|
155
117
|
}, z.core.$strict>>>;
|
|
156
118
|
}, z.core.$strict>;
|
|
157
119
|
export type TrackingSignals = z.infer<typeof trackingSignalsSchema>;
|
|
158
|
-
/** A set the shop sells together. The model never picks or invents one. */
|
|
159
120
|
export declare const bundleSchema: z.ZodObject<{
|
|
160
121
|
id: z.ZodString;
|
|
161
122
|
skus: z.ZodArray<z.ZodString>;
|
|
@@ -246,18 +207,8 @@ export declare const trackingInputSchema: z.ZodObject<{
|
|
|
246
207
|
}, z.core.$strict>>>;
|
|
247
208
|
}, z.core.$strict>;
|
|
248
209
|
export type TrackingInput = z.infer<typeof trackingInputSchema>;
|
|
249
|
-
/** The shape a caller passes in, before defaults are applied. */
|
|
250
210
|
export type TrackingInputDraft = z.input<typeof trackingInputSchema>;
|
|
251
|
-
/**
|
|
252
|
-
* The result of a non-throwing parse. Exported so a consumer can type a
|
|
253
|
-
* validation failure without depending on zod directly.
|
|
254
|
-
*/
|
|
255
211
|
export type TrackingInputResult = z.ZodSafeParseResult<TrackingInput>;
|
|
256
|
-
/**
|
|
257
|
-
* Validates a payload, throwing a `ZodError` if it does not satisfy the
|
|
258
|
-
* contract. An invalid payload is a caller bug, and it should be loud.
|
|
259
|
-
*/
|
|
260
212
|
export declare function parseTrackingInput(value: unknown): TrackingInput;
|
|
261
|
-
/** Non-throwing variant, for callers that want to inspect the failure. */
|
|
262
213
|
export declare function safeParseTrackingInput(value: unknown): TrackingInputResult;
|
|
263
214
|
//# sourceMappingURL=tracking-input.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tracking-input.d.ts","sourceRoot":"","sources":["../src/tracking-input.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB
|
|
1
|
+
{"version":3,"file":"tracking-input.d.ts","sourceRoot":"","sources":["../src/tracking-input.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,YAAY;aACvB,UAAU,EAAE,GAAG;aACf,SAAS,EAAE,GAAG;aACd,WAAW,EAAE,GAAG;aAChB,GAAG,EAAE,EAAE;aACP,cAAc,EAAE,EAAE;aAClB,WAAW,EAAE,EAAE;aACf,kBAAkB,EAAE,GAAG;aACvB,UAAU,EAAE,GAAG;aACf,iBAAiB,EAAE,CAAC;aACpB,OAAO,EAAE,EAAE;aACX,SAAS,EAAE,EAAE;aACb,QAAQ,EAAE,EAAE;aAEZ,MAAM,EAAE,GAAG;CACH,CAAC;AA0BX,eAAO,MAAM,aAAa;;;;;;;;;;;kBAmBxB,CAAC;AACH,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAEpD,eAAO,MAAM,eAAe;;;;;kBAM1B,CAAC;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAExD,eAAO,MAAM,gBAAgB;;;;;;;kBAG3B,CAAC;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D,eAAO,MAAM,oBAAoB;;;;;;;kBAG/B,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAmBlE,eAAO,MAAM,iBAAiB;;;;;;;kBAO5B,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAE5D,eAAO,MAAM,mBAAmB;;;;;;;;kBAe9B,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAKhE,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAQhC,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE,eAAO,MAAM,YAAY;;;;;;kBAgBvB,CAAC;AACH,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAElD,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBA0C7B,CAAC;AAEJ,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAErE,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,kBAAkB,CAAC,aAAa,CAAC,CAAC;AAEtE,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,aAAa,CAEhE;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,mBAAmB,CAE1E"}
|
package/dist/tracking-input.js
CHANGED
|
@@ -1,34 +1,4 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
/**
|
|
3
|
-
* The tracking-input contract — the boundary between the host application and
|
|
4
|
-
* rudra-js.
|
|
5
|
-
*
|
|
6
|
-
* rudra-js does not collect, store, or aggregate anything. The host owns its
|
|
7
|
-
* tracking pipeline (an event stream, a CDP, a warehouse) and hands the
|
|
8
|
-
* framework one JSON object per render. This module is that contract: one
|
|
9
|
-
* schema, validated at the edge, so a malformed payload fails loudly here
|
|
10
|
-
* rather than quietly producing a bad prompt several layers later.
|
|
11
|
-
*
|
|
12
|
-
* Every fixed-shape object below is a `strictObject`, so an unrecognised field
|
|
13
|
-
* is an error rather than being dropped. That matters more than it looks: with
|
|
14
|
-
* a lenient object, a host that misspells `recentSearches` gets a shopper who
|
|
15
|
-
* silently looks like a first-time visitor, and nothing anywhere reports it.
|
|
16
|
-
* The only dynamic shape is `interaction.meta`, which is a record by design.
|
|
17
|
-
*/
|
|
18
|
-
/**
|
|
19
|
-
* Every free-text field and every array is length-capped.
|
|
20
|
-
*
|
|
21
|
-
* These caps are not cosmetic. Host-supplied strings end up inside the prompt
|
|
22
|
-
* we send to a language model, and a model is billed per token — so an
|
|
23
|
-
* unbounded string is an unbounded bill, and an unbounded array of candidates
|
|
24
|
-
* is the same problem multiplied.
|
|
25
|
-
*
|
|
26
|
-
* What this buys is that no single field is unbounded. It is deliberately not
|
|
27
|
-
* an aggregate budget: the caps multiply out to far more than any context
|
|
28
|
-
* window, because rejecting a large-but-legitimate payload is the wrong
|
|
29
|
-
* response to one. Fitting a payload into a prompt is `digest`'s job, and it
|
|
30
|
-
* trims rather than throws.
|
|
31
|
-
*/
|
|
32
2
|
export const FIELD_LIMITS = {
|
|
33
3
|
identifier: 128,
|
|
34
4
|
shortText: 200,
|
|
@@ -42,34 +12,21 @@ export const FIELD_LIMITS = {
|
|
|
42
12
|
bundles: 20,
|
|
43
13
|
localeTag: 35,
|
|
44
14
|
maxItems: 12,
|
|
45
|
-
// Matches CLAMP.reason in reconciliation: a host reason is rendered in the
|
|
46
|
-
// same place a model's is, so the same length has to hold.
|
|
47
15
|
reason: 120,
|
|
48
16
|
};
|
|
49
|
-
/** Assigning this as an object key mutates the prototype instead of the object. */
|
|
50
17
|
const RESERVED_META_KEY = '__proto__';
|
|
51
|
-
/** Upper bound on any timestamp: 2100-01-01. */
|
|
52
18
|
const MAX_EPOCH_MS = Date.UTC(2100, 0, 1);
|
|
53
19
|
const identifier = () => z.string().min(1).max(FIELD_LIMITS.identifier);
|
|
54
20
|
const optionalIdentifier = () => z.string().min(1).max(FIELD_LIMITS.identifier).optional();
|
|
55
|
-
/**
|
|
56
|
-
* Epoch milliseconds, used only for recency ordering. Bounded because a
|
|
57
|
-
* negative or year-3000 timestamp does not fail anywhere downstream — it just
|
|
58
|
-
* sorts to one end and silently reorders the shopper's history.
|
|
59
|
-
*/
|
|
60
21
|
const epochMs = () => z.number().int().min(0).max(MAX_EPOCH_MS).optional();
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
*/
|
|
22
|
+
const isRootRelative = (value) => {
|
|
23
|
+
const asParsed = value.replace(/[\t\n\r]/g, '');
|
|
24
|
+
return value.startsWith('/') && /^\/(?![/\\])/.test(asParsed);
|
|
25
|
+
};
|
|
66
26
|
const imageReference = () => z
|
|
67
27
|
.string()
|
|
68
28
|
.max(FIELD_LIMITS.shortText)
|
|
69
|
-
.refine((value) => value.
|
|
70
|
-
? true
|
|
71
|
-
: z.url({ protocol: /^https?$/ }).safeParse(value).success, { message: 'expected an http(s) URL or a root-relative path' });
|
|
72
|
-
/** A product the generated component is permitted to place. */
|
|
29
|
+
.refine((value) => isRootRelative(value) ? true : z.url({ protocol: /^https?$/ }).safeParse(value).success, { message: 'expected an http(s) URL or a root-relative path' });
|
|
73
30
|
export const productSchema = z.strictObject({
|
|
74
31
|
sku: identifier(),
|
|
75
32
|
title: z.string().min(1).max(FIELD_LIMITS.shortText),
|
|
@@ -79,15 +36,8 @@ export const productSchema = z.strictObject({
|
|
|
79
36
|
.string()
|
|
80
37
|
.regex(/^[A-Z]{3}$/, 'expected a three-letter ISO 4217 code')
|
|
81
38
|
.default('USD'),
|
|
82
|
-
// This lands in an `<img src>` the host did not write, so the scheme matters:
|
|
83
|
-
// a bare capped string would accept '' and 'javascript:'. Root-relative paths
|
|
84
|
-
// are allowed because most catalogs store images that way, and they carry no
|
|
85
|
-
// scheme to abuse.
|
|
86
39
|
imageUrl: imageReference().optional(),
|
|
87
40
|
rating: z.number().min(0).max(5).optional(),
|
|
88
|
-
// Your phrase for why this product is here, when your own ranking already has
|
|
89
|
-
// one. It renders as the item's reason and is not screened: these are your
|
|
90
|
-
// words about your product, like the title, so you stand behind them.
|
|
91
41
|
reason: z.string().min(1).max(FIELD_LIMITS.reason).optional(),
|
|
92
42
|
isInStock: z.boolean().default(true),
|
|
93
43
|
tags: z
|
|
@@ -95,12 +45,10 @@ export const productSchema = z.strictObject({
|
|
|
95
45
|
.max(FIELD_LIMITS.tagsPerProduct)
|
|
96
46
|
.default([]),
|
|
97
47
|
});
|
|
98
|
-
/** Base shape for any signal that points at a single SKU. */
|
|
99
48
|
export const skuSignalSchema = z.strictObject({
|
|
100
49
|
sku: identifier(),
|
|
101
50
|
category: optionalIdentifier(),
|
|
102
51
|
at: epochMs(),
|
|
103
|
-
/** Caller-supplied strength, 0..1. Defaults to 1 when absent. */
|
|
104
52
|
weight: z.number().min(0).max(1).optional(),
|
|
105
53
|
});
|
|
106
54
|
export const viewSignalSchema = skuSignalSchema.extend({
|
|
@@ -111,32 +59,12 @@ export const purchaseSignalSchema = skuSignalSchema.extend({
|
|
|
111
59
|
quantity: z.number().int().positive().default(1),
|
|
112
60
|
price: z.number().nonnegative().optional(),
|
|
113
61
|
});
|
|
114
|
-
/**
|
|
115
|
-
* `meta` is the one dynamic shape in the contract, so its key is checked before
|
|
116
|
-
* the record is parsed rather than after. Zod builds its result by assigning
|
|
117
|
-
* each key, and assigning `__proto__` sets the prototype instead of adding a
|
|
118
|
-
* key — so the entry would vanish from the parsed output with no error, which
|
|
119
|
-
* is the silent drop this module exists to prevent.
|
|
120
|
-
*
|
|
121
|
-
* `__proto__` is the only key that behaves this way. `constructor` and
|
|
122
|
-
* `prototype` are ordinary own properties: assigning either shadows it on that
|
|
123
|
-
* one object and leaves the prototype alone, so both survive a parse intact and
|
|
124
|
-
* are accepted. `meta` is a host-defined vocabulary, and a shop with a facet,
|
|
125
|
-
* filter, or CMS field by either name should get a component, not a throw.
|
|
126
|
-
*/
|
|
127
62
|
const metaKeysAreSafe = z.custom((value) => typeof value === 'object' && value !== null && !Object.hasOwn(value, RESERVED_META_KEY), { message: `meta may not use the reserved key ${RESERVED_META_KEY}` });
|
|
128
|
-
// `z.record` bounds key and value shape but not how many entries a record may
|
|
129
|
-
// carry, so the count is checked separately.
|
|
130
63
|
const metaSchema = metaKeysAreSafe.pipe(z
|
|
131
64
|
.record(z.string().min(1).max(FIELD_LIMITS.identifier), z.union([z.string().max(FIELD_LIMITS.shortText), z.number(), z.boolean()]))
|
|
132
65
|
.refine((entries) => Object.keys(entries).length <= FIELD_LIMITS.metaEntries, {
|
|
133
66
|
message: `meta may carry at most ${FIELD_LIMITS.metaEntries} entries`,
|
|
134
67
|
}));
|
|
135
|
-
/**
|
|
136
|
-
* Catch-all for everything else the shopper did. `type` is an open vocabulary
|
|
137
|
-
* on purpose — 'scroll_depth', 'wishlist', 'filter_applied', whatever the host
|
|
138
|
-
* already emits — so hosts do not have to map their events onto ours.
|
|
139
|
-
*/
|
|
140
68
|
export const interactionSchema = z.strictObject({
|
|
141
69
|
type: identifier(),
|
|
142
70
|
sku: optionalIdentifier(),
|
|
@@ -145,23 +73,17 @@ export const interactionSchema = z.strictObject({
|
|
|
145
73
|
value: z.union([z.string().max(FIELD_LIMITS.shortText), z.number(), z.boolean()]).optional(),
|
|
146
74
|
meta: metaSchema.optional(),
|
|
147
75
|
});
|
|
148
|
-
/** Where on the site this component is being rendered. */
|
|
149
76
|
export const renderContextSchema = z.strictObject({
|
|
150
|
-
/** 'pdp', 'home', 'cart', 'search', or any host-defined surface. */
|
|
151
77
|
surface: identifier(),
|
|
152
|
-
/** Named placement, e.g. 'below-fold-recommendations'. */
|
|
153
78
|
slot: z.string().min(1).max(FIELD_LIMITS.identifier).default('recommendations'),
|
|
154
79
|
currentSku: optionalIdentifier(),
|
|
155
80
|
currentCategory: optionalIdentifier(),
|
|
156
81
|
searchQuery: z.string().max(FIELD_LIMITS.searchQuery).optional(),
|
|
157
|
-
// One language tag, not the Accept-Language header it is often copied from:
|
|
158
|
-
// the locale is part of the cohort cache key, so a list makes its own cohort.
|
|
159
82
|
locale: z
|
|
160
83
|
.string()
|
|
161
84
|
.max(FIELD_LIMITS.localeTag)
|
|
162
85
|
.regex(/^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*$/, 'expected one language tag, such as en-US')
|
|
163
86
|
.default('en-US'),
|
|
164
|
-
/** Upper bound on products across the whole generated component. */
|
|
165
87
|
maxItems: z.number().int().min(1).max(FIELD_LIMITS.maxItems).default(4),
|
|
166
88
|
});
|
|
167
89
|
const signalArray = (schema) => z.array(schema).max(FIELD_LIMITS.signalsPerCategory).default([]);
|
|
@@ -174,7 +96,6 @@ export const trackingSignalsSchema = z.strictObject({
|
|
|
174
96
|
recentSearches: signalArray(z.string().min(1).max(FIELD_LIMITS.searchQuery)),
|
|
175
97
|
interactions: signalArray(interactionSchema),
|
|
176
98
|
});
|
|
177
|
-
/** A set the shop sells together. The model never picks or invents one. */
|
|
178
99
|
export const bundleSchema = z.strictObject({
|
|
179
100
|
id: identifier(),
|
|
180
101
|
skus: z
|
|
@@ -184,8 +105,6 @@ export const bundleSchema = z.strictObject({
|
|
|
184
105
|
.refine((skus) => new Set(skus).size === skus.length, {
|
|
185
106
|
message: 'a bundle must not list the same product twice',
|
|
186
107
|
}),
|
|
187
|
-
// The shop's price for the set — not derived from the parts, so the set says
|
|
188
|
-
// which money it is in rather than borrowing it from a member.
|
|
189
108
|
price: z.number().nonnegative(),
|
|
190
109
|
currency: z
|
|
191
110
|
.string()
|
|
@@ -202,19 +121,7 @@ export const trackingInputSchema = z
|
|
|
202
121
|
isReturning: z.boolean().optional(),
|
|
203
122
|
}),
|
|
204
123
|
context: renderContextSchema,
|
|
205
|
-
// A payload with no `signals` block at all is the cold-start case, not an
|
|
206
|
-
// error. Every category defaults to empty, so a first-time visitor needs no
|
|
207
|
-
// special handling from the host.
|
|
208
124
|
signals: trackingSignalsSchema.prefault({}),
|
|
209
|
-
/**
|
|
210
|
-
* The only products the generated component may place. Merchandising rules
|
|
211
|
-
* belong here: whatever the host leaves out cannot be recommended, which is
|
|
212
|
-
* what makes it impossible to surface a product that does not exist or is not
|
|
213
|
-
* merchandised for this shopper.
|
|
214
|
-
*
|
|
215
|
-
* SKUs must be unique — a duplicate is a host bug that spends prompt budget
|
|
216
|
-
* twice and invites the same product in two slots.
|
|
217
|
-
*/
|
|
218
125
|
candidates: z
|
|
219
126
|
.array(productSchema)
|
|
220
127
|
.min(1)
|
|
@@ -222,8 +129,6 @@ export const trackingInputSchema = z
|
|
|
222
129
|
.refine((products) => new Set(products.map((product) => product.sku)).size === products.length, {
|
|
223
130
|
message: 'candidates must have unique SKUs',
|
|
224
131
|
}),
|
|
225
|
-
// Ids must be unique — the renderer looks a bundle up by id alone, so a
|
|
226
|
-
// duplicate would let it draw the wrong set at the wrong price.
|
|
227
132
|
bundles: z
|
|
228
133
|
.array(bundleSchema)
|
|
229
134
|
.max(FIELD_LIMITS.bundles)
|
|
@@ -242,14 +147,9 @@ export const trackingInputSchema = z
|
|
|
242
147
|
}
|
|
243
148
|
return true;
|
|
244
149
|
}, { message: 'every product in a bundle must also be a candidate' });
|
|
245
|
-
/**
|
|
246
|
-
* Validates a payload, throwing a `ZodError` if it does not satisfy the
|
|
247
|
-
* contract. An invalid payload is a caller bug, and it should be loud.
|
|
248
|
-
*/
|
|
249
150
|
export function parseTrackingInput(value) {
|
|
250
151
|
return trackingInputSchema.parse(value);
|
|
251
152
|
}
|
|
252
|
-
/** Non-throwing variant, for callers that want to inspect the failure. */
|
|
253
153
|
export function safeParseTrackingInput(value) {
|
|
254
154
|
return trackingInputSchema.safeParse(value);
|
|
255
155
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tracking-input.js","sourceRoot":"","sources":["../src/tracking-input.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB
|
|
1
|
+
{"version":3,"file":"tracking-input.js","sourceRoot":"","sources":["../src/tracking-input.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rudra-js/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "The contracts and logic that turn one tracking payload into one renderable component specification",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"llm",
|
|
@@ -50,13 +50,14 @@
|
|
|
50
50
|
"publishConfig": {
|
|
51
51
|
"access": "public"
|
|
52
52
|
},
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"zod": "^4.0.0"
|
|
55
|
+
},
|
|
53
56
|
"peerDependencies": {
|
|
54
|
-
"@rudra-js/attested": "^0.
|
|
55
|
-
"zod": "^4.5.0"
|
|
57
|
+
"@rudra-js/attested": "^0.7.0"
|
|
56
58
|
},
|
|
57
59
|
"devDependencies": {
|
|
58
|
-
"@rudra-js/attested": "0.
|
|
59
|
-
"@types/node": "^26.4.1"
|
|
60
|
-
"zod": "^4.5.4"
|
|
60
|
+
"@rudra-js/attested": "0.7.0",
|
|
61
|
+
"@types/node": "^26.4.1"
|
|
61
62
|
}
|
|
62
63
|
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { offeredCandidates } from './model-prompt.js';
|
|
2
|
+
import type { Product, TrackingInput } from './tracking-input.js';
|
|
3
|
+
|
|
4
|
+
const CLAIM_PATTERNS: { kind: string; patterns: RegExp[] }[] = [
|
|
5
|
+
{
|
|
6
|
+
kind: 'rating',
|
|
7
|
+
patterns: [
|
|
8
|
+
/\breviews?\b|\breviewed\b/,
|
|
9
|
+
/\b(?:\d+(?:\.\d+)?|three|four|five)[\s-]?stars?\b/,
|
|
10
|
+
/\bstars?[\s-]?ratings?\b/,
|
|
11
|
+
|
|
12
|
+
/\b(?:customer|shopper|buyer|user|average|overall)[\s-]ratings?\b/,
|
|
13
|
+
|
|
14
|
+
/\bratings?\s+of\s+[0-5]\.\d\b/,
|
|
15
|
+
|
|
16
|
+
/\b[0-5](?:\.\d)?\s+out of\s+(?:5|five)\b/,
|
|
17
|
+
/\b(?:highly|top|best|well|poorly|five|four)[\s-]rated\b/,
|
|
18
|
+
|
|
19
|
+
/\brated\s+(?:[0-5]\.\d|(?:[0-5]|three|four|five)\s+(?:stars?|out of))\b/,
|
|
20
|
+
|
|
21
|
+
/\bbest[\s-]?sell(?:er|ers|ing)\b/,
|
|
22
|
+
|
|
23
|
+
/(?:\bnumber one|\bno\.? ?1|#\s?1)[\s-]?sell(?:er|ers|ing)\b/,
|
|
24
|
+
/\bloved by (?:thousands|hundreds|millions|\d)/,
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
kind: 'price',
|
|
29
|
+
patterns: [
|
|
30
|
+
/[$£€¥₹]/,
|
|
31
|
+
/\b\d+(?:\.\d+)?\s?(?:usd|eur|gbp|dollars?|pounds?|euros?)\b/,
|
|
32
|
+
|
|
33
|
+
/\bdollars?\b|\beuros?\b/,
|
|
34
|
+
|
|
35
|
+
/\b(?:usd|eur|gbp)\s?\d/,
|
|
36
|
+
|
|
37
|
+
/\bkr\s?\d|\d\s?kr\b/,
|
|
38
|
+
/\bpric(?:e|es|ed|ing)\b/,
|
|
39
|
+
|
|
40
|
+
/\bwas\s+[$£€¥]?\s?\d[\d,.]*\s*[,;–—-]?\s*now\s+[$£€¥]?\s?\d/,
|
|
41
|
+
|
|
42
|
+
/\bcheap(?:er|est)\b/,
|
|
43
|
+
/\baffordable\b|\bbargain\b/,
|
|
44
|
+
|
|
45
|
+
/\bcosts?\s+(?:less|more|only|just|about|around|[$£€¥]?\d)/,
|
|
46
|
+
/\blow(?:er)?[\s-]cost\b/,
|
|
47
|
+
|
|
48
|
+
/\bsaves?\s+(?:you\s+)?(?:money|cash|[$£€¥]\s?\d)/,
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
kind: 'discount',
|
|
53
|
+
patterns: [
|
|
54
|
+
/\d+(?:\.\d+)?\s?(?:%|percent)\s*(?:off\b|discount|reduction|less\b)/,
|
|
55
|
+
/\b(?:save|saving|savings|off|discount|extra|up to)\s+(?:up to\s+)?\d+(?:\.\d+)?\s?(?:%|percent)/,
|
|
56
|
+
/\bdiscount(?:s|ed)?\b/,
|
|
57
|
+
/\bsale\b|\bmarked down\b|\bdeal of\b/,
|
|
58
|
+
/\bhalf[\s-]?(?:price|off)\b/,
|
|
59
|
+
|
|
60
|
+
/\bsaves?\s+\d[\d.,]*\s+off\b/,
|
|
61
|
+
|
|
62
|
+
/\bclearance\s+(?:sale|price|event|deal)\b|\bon clearance\b/,
|
|
63
|
+
|
|
64
|
+
/\bprice reduced\b|\breduced price\b|\breduced by \d+\s?(?:%|percent)|\breduced\s+(?:this|next|last)\s+week\b/,
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
kind: 'delivery',
|
|
69
|
+
patterns: [
|
|
70
|
+
/\bdelivery\b|\bshipping\b/,
|
|
71
|
+
/\bnext[\s-]day\b|\bsame[\s-]day\b|\bovernight\b/,
|
|
72
|
+
/\b(?:ships?|arrives?|arriving|delivered|by|before|in time for)\s+(?:on\s+)?(?:today|tomorrow|tonight|this week|next week|the weekend|monday|tuesday|wednesday|thursday|friday|saturday|sunday)\b/,
|
|
73
|
+
/\b(?:ships?|arrives?|arriving)\s+(?:in|within)\s+\d/,
|
|
74
|
+
/\bin time for\b/,
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
kind: 'stock',
|
|
79
|
+
patterns: [
|
|
80
|
+
/\b(?:in|out of|low on) stock\b/,
|
|
81
|
+
/\blimited stock\b/,
|
|
82
|
+
/\brestocked?\b|\bsold out\b/,
|
|
83
|
+
|
|
84
|
+
/\b(?:only\s+)?(?:\d+|a few|a handful|a couple|one|few)\s+(?:left|remain(?:s|ing)?)\b/,
|
|
85
|
+
/\bselling fast\b|\b(?:almost|nearly) gone\b|\bwhile stocks last\b/,
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
const CONFUSABLES: Record<string, string> = {
|
|
91
|
+
а: 'a',
|
|
92
|
+
в: 'b',
|
|
93
|
+
е: 'e',
|
|
94
|
+
к: 'k',
|
|
95
|
+
м: 'm',
|
|
96
|
+
н: 'h',
|
|
97
|
+
о: 'o',
|
|
98
|
+
р: 'p',
|
|
99
|
+
с: 'c',
|
|
100
|
+
т: 't',
|
|
101
|
+
х: 'x',
|
|
102
|
+
у: 'y',
|
|
103
|
+
ѕ: 's',
|
|
104
|
+
і: 'i',
|
|
105
|
+
ј: 'j',
|
|
106
|
+
ԁ: 'd',
|
|
107
|
+
һ: 'h',
|
|
108
|
+
ӏ: 'l',
|
|
109
|
+
α: 'a',
|
|
110
|
+
ε: 'e',
|
|
111
|
+
ι: 'i',
|
|
112
|
+
κ: 'k',
|
|
113
|
+
ν: 'v',
|
|
114
|
+
ο: 'o',
|
|
115
|
+
ρ: 'p',
|
|
116
|
+
τ: 't',
|
|
117
|
+
υ: 'u',
|
|
118
|
+
χ: 'x',
|
|
119
|
+
ϲ: 'c',
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const INVISIBLE = /[\p{Cf}\p{Default_Ignorable_Code_Point}\p{Cc}]/gu;
|
|
123
|
+
|
|
124
|
+
const SPACING = /[\t\n\v\f\r\u0085]/;
|
|
125
|
+
|
|
126
|
+
const MARKS = /[\p{Mn}\p{Me}]/gu;
|
|
127
|
+
|
|
128
|
+
function normaliseForClaims(text: string): string {
|
|
129
|
+
const lowered = text
|
|
130
|
+
.replace(INVISIBLE, (char) => (SPACING.test(char) ? char : ''))
|
|
131
|
+
.normalize('NFKC')
|
|
132
|
+
.toLowerCase()
|
|
133
|
+
.normalize('NFC')
|
|
134
|
+
.replace(MARKS, '');
|
|
135
|
+
|
|
136
|
+
let folded = '';
|
|
137
|
+
for (const char of lowered) folded += CONFUSABLES[char] ?? char;
|
|
138
|
+
return folded;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export const ALLOWED_PHRASES = ['does not feel cheap', "doesn't feel cheap", 'last few miles'];
|
|
142
|
+
|
|
143
|
+
const DIGIT = /\p{Nd}/u;
|
|
144
|
+
|
|
145
|
+
const BARE_EXPONENT = /^[+-]?\d+(?:\.\d+)?[eE]([+-]?\d+)$/;
|
|
146
|
+
|
|
147
|
+
const MAX_EXPONENT = 1000;
|
|
148
|
+
|
|
149
|
+
function canLayOut(fact: string): boolean {
|
|
150
|
+
const match = BARE_EXPONENT.exec(fact.trim());
|
|
151
|
+
if (match === null) return true;
|
|
152
|
+
return Math.abs(Number(match[1] ?? '')) <= MAX_EXPONENT;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function productFacts(product: Product): string[] {
|
|
156
|
+
const facts: string[] = [];
|
|
157
|
+
if (DIGIT.test(product.category) && canLayOut(product.category)) facts.push(product.category);
|
|
158
|
+
for (const tag of product.tags) {
|
|
159
|
+
if (DIGIT.test(tag) && canLayOut(tag)) facts.push(tag);
|
|
160
|
+
}
|
|
161
|
+
return facts;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export interface HostFacts {
|
|
165
|
+
pooled: string[];
|
|
166
|
+
bySku: Map<string, string[]>;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function hostFacts(input: TrackingInput): HostFacts {
|
|
170
|
+
const pooled = new Set<string>();
|
|
171
|
+
const bySku = new Map<string, string[]>();
|
|
172
|
+
|
|
173
|
+
for (const product of offeredCandidates(input)) {
|
|
174
|
+
const own = productFacts(product);
|
|
175
|
+
bySku.set(product.sku, own);
|
|
176
|
+
for (const fact of own) pooled.add(fact);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return { pooled: [...pooled], bySku };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function claimIn(text: string): string | null {
|
|
183
|
+
const normalised = normaliseForClaims(text);
|
|
184
|
+
for (const claim of CLAIM_PATTERNS) {
|
|
185
|
+
for (const pattern of claim.patterns) {
|
|
186
|
+
if (pattern.test(normalised)) return claim.kind;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return null;
|
|
190
|
+
}
|