@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/README.md
CHANGED
|
@@ -6,20 +6,21 @@ component specification.
|
|
|
6
6
|
## Install
|
|
7
7
|
|
|
8
8
|
```sh
|
|
9
|
-
npm install @rudra-js/core @rudra-js/attested
|
|
9
|
+
npm install @rudra-js/core @rudra-js/attested
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
12
|
+
`zod` is a dependency, so you don't need it yourself. The public API of this
|
|
13
|
+
package _is_ zod schemas, and an app on any zod 4 shares one copy with it. An
|
|
14
|
+
app on zod 3 gets its own copy alongside ours, and the two don't mix: build on
|
|
15
|
+
the exported schemas with their own methods (`productSchema.array()`) rather
|
|
16
|
+
than your zod's (`z.array(productSchema)`), and read `error.issues` rather than
|
|
17
|
+
checking `instanceof ZodError`. Whichever copy core ends up with writes the tool
|
|
18
|
+
schema we send the model, so its wording shifts a little between releases:
|
|
19
|
+
before 4.5, a nullable field is written as `anyOf` rather than a type array. It
|
|
20
|
+
means the same thing either way.
|
|
21
|
+
|
|
22
|
+
`@rudra-js/attested` is a peer dependency, even though none of its types cross
|
|
23
|
+
this package's public surface and you never have to import it. It's a peer so
|
|
23
24
|
there is exactly one denylist in your tree. As a real dependency, an app that
|
|
24
25
|
pins its own copy gets two — we checked, and npm 10.9.4 installs the app's
|
|
25
26
|
version at the top and quietly nests ours under `@rudra-js/core` — and the two
|
|
@@ -120,10 +121,23 @@ before anything renders. A product you left out doesn't reach the page.
|
|
|
120
121
|
`bundles` is optional. These are the sets you sell together, each with your own
|
|
121
122
|
price for the set, the currency that price is in, and, if you want one, your own
|
|
122
123
|
name for it. Every product in a set has to be a candidate as well. That's what
|
|
123
|
-
lets the
|
|
124
|
+
lets the checks that pass a single product run over a whole set, and what lets
|
|
124
125
|
the renderer look the members up in the catalog it already has. Ids must be
|
|
125
126
|
unique, and one set must not name the same product twice.
|
|
126
127
|
|
|
128
|
+
A set is placed whole, so one check runs differently: a set may hold something
|
|
129
|
+
the shopper already bought or has in the basket, where a grid or a carousel
|
|
130
|
+
would drop it. A thumbs-down on a member still blocks the whole set, and so does
|
|
131
|
+
a member that is out of stock, already on the page, or the product being looked
|
|
132
|
+
at right now.
|
|
133
|
+
|
|
134
|
+
That last one has a cost. A "frequently bought together, including this item"
|
|
135
|
+
set names the product whose page it sits on, so it is refused on exactly the
|
|
136
|
+
page you wrote it for. Leave the item out and sell the companions as the set —
|
|
137
|
+
that rejection goes away, and the rest of the checks still apply. And when a
|
|
138
|
+
bundle block is the only block the model asked for, refusing the set leaves
|
|
139
|
+
nothing to render, so the deterministic component takes over.
|
|
140
|
+
|
|
127
141
|
The model _never_ picks a set and is never told a price. All it does is ask for a
|
|
128
142
|
bundle block and write the words around it. We pick which set when the page is
|
|
129
143
|
served, going on what the shopper has in their basket, has looked at, or is
|
|
@@ -244,6 +258,30 @@ everyone looking at it shares them. If you pick candidates per shopper you'll ge
|
|
|
244
258
|
smaller cohorts. That's the honest outcome, because your prompt really is
|
|
245
259
|
personal.
|
|
246
260
|
|
|
261
|
+
What goes in the key is the _set_ of candidate SKUs. Not the order you sent them
|
|
262
|
+
in, not which ones were in stock, and not where the 60-product cut fell. So two
|
|
263
|
+
requests carrying the same SKUs in a different order share one component, and the
|
|
264
|
+
one generated first is the one both get. If your ranking is personal, order your
|
|
265
|
+
candidates and the products in the copy won't be the ones you put at the top.
|
|
266
|
+
Prices, titles and stock are always read from your catalog at render time, so
|
|
267
|
+
nothing stale is ever shown — a product that sold out is dropped as the page is
|
|
268
|
+
served, and any sentence written about it stays.
|
|
269
|
+
|
|
270
|
+
#### Who can create a cohort
|
|
271
|
+
|
|
272
|
+
Every field above is one you supply, and each distinct value is a cohort of its
|
|
273
|
+
own — a fresh model call, and a slot in the cache. If a visitor can choose one of
|
|
274
|
+
them, a visitor can choose how many calls you pay for. A `locale` copied straight
|
|
275
|
+
out of `Accept-Language`, a `currentCategory` read off a URL slug and a `segment`
|
|
276
|
+
built from a query parameter are the three that go wrong. The shipped cache holds
|
|
277
|
+
10,000 entries and drops the oldest, so enough made-up values also evict the
|
|
278
|
+
cohorts your real shoppers were being served from.
|
|
279
|
+
|
|
280
|
+
Draw those fields from sets you control: your own locale list, your own category
|
|
281
|
+
ids, your own segment labels. Nothing leaks either way — a cohort prompt carries
|
|
282
|
+
no shopper text at all — the cost is the bill and the model round trip on a page
|
|
283
|
+
that was being served from cache.
|
|
284
|
+
|
|
247
285
|
Pass `generation: 'per-shopper'` to generate for the individual instead. The
|
|
248
286
|
model then chooses the products too, and every shopper pays for their own call.
|
|
249
287
|
|
|
@@ -311,9 +349,19 @@ const generator = createComponentGenerator({ provider, rank: 'given' });
|
|
|
311
349
|
|
|
312
350
|
You keep the rest either way. We still drop anything the shopper shouldn't be
|
|
313
351
|
shown, whether it's out of stock, already bought, in the basket, disliked, or
|
|
314
|
-
the product they're looking at right now
|
|
315
|
-
|
|
316
|
-
|
|
352
|
+
the product they're looking at right now — read from the payload you sent, not
|
|
353
|
+
from the shortened history the model was shown, so a shopper with a long order
|
|
354
|
+
book is covered too. Every product still carries a basis we check against their
|
|
355
|
+
real signals, and everything the model writes is still screened.
|
|
356
|
+
|
|
357
|
+
How much each basis proves is worth knowing, because two of the six prove less
|
|
358
|
+
than they read. `most_viewed` is checked against the SKUs they viewed,
|
|
359
|
+
`similar_to_current` against the category being browsed, `liked_category`
|
|
360
|
+
against the categories they bought, liked, carted or viewed in — standing on a
|
|
361
|
+
category page is not one of those — and `popular` claims nothing.
|
|
362
|
+
`complements_cart` and `complements_purchase` check only that the basket, or the
|
|
363
|
+
order history, isn't empty: nothing here can know that one product goes with
|
|
364
|
+
another. A basis that fails takes the sentence and the badge stating it with it.
|
|
317
365
|
|
|
318
366
|
Each candidate can carry its own `reason`, the phrase shown under the product.
|
|
319
367
|
Reach for it when your ranking knows something the signals don't:
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Product, TrackingInput } from './tracking-input.js';
|
|
2
|
+
export declare const ALLOWED_PHRASES: string[];
|
|
3
|
+
export declare function productFacts(product: Product): string[];
|
|
4
|
+
export interface HostFacts {
|
|
5
|
+
pooled: string[];
|
|
6
|
+
bySku: Map<string, string[]>;
|
|
7
|
+
}
|
|
8
|
+
export declare function hostFacts(input: TrackingInput): HostFacts;
|
|
9
|
+
export declare function claimIn(text: string): string | null;
|
|
10
|
+
//# sourceMappingURL=claim-screening.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claim-screening.d.ts","sourceRoot":"","sources":["../src/claim-screening.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AA2IlE,eAAO,MAAM,eAAe,UAAkE,CAAC;AAc/F,wBAAgB,YAAY,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,EAAE,CAOvD;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CAC9B;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,aAAa,GAAG,SAAS,CAWzD;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAQnD"}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { offeredCandidates } from './model-prompt.js';
|
|
2
|
+
const CLAIM_PATTERNS = [
|
|
3
|
+
{
|
|
4
|
+
kind: 'rating',
|
|
5
|
+
patterns: [
|
|
6
|
+
/\breviews?\b|\breviewed\b/,
|
|
7
|
+
/\b(?:\d+(?:\.\d+)?|three|four|five)[\s-]?stars?\b/,
|
|
8
|
+
/\bstars?[\s-]?ratings?\b/,
|
|
9
|
+
/\b(?:customer|shopper|buyer|user|average|overall)[\s-]ratings?\b/,
|
|
10
|
+
/\bratings?\s+of\s+[0-5]\.\d\b/,
|
|
11
|
+
/\b[0-5](?:\.\d)?\s+out of\s+(?:5|five)\b/,
|
|
12
|
+
/\b(?:highly|top|best|well|poorly|five|four)[\s-]rated\b/,
|
|
13
|
+
/\brated\s+(?:[0-5]\.\d|(?:[0-5]|three|four|five)\s+(?:stars?|out of))\b/,
|
|
14
|
+
/\bbest[\s-]?sell(?:er|ers|ing)\b/,
|
|
15
|
+
/(?:\bnumber one|\bno\.? ?1|#\s?1)[\s-]?sell(?:er|ers|ing)\b/,
|
|
16
|
+
/\bloved by (?:thousands|hundreds|millions|\d)/,
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
kind: 'price',
|
|
21
|
+
patterns: [
|
|
22
|
+
/[$£€¥₹]/,
|
|
23
|
+
/\b\d+(?:\.\d+)?\s?(?:usd|eur|gbp|dollars?|pounds?|euros?)\b/,
|
|
24
|
+
/\bdollars?\b|\beuros?\b/,
|
|
25
|
+
/\b(?:usd|eur|gbp)\s?\d/,
|
|
26
|
+
/\bkr\s?\d|\d\s?kr\b/,
|
|
27
|
+
/\bpric(?:e|es|ed|ing)\b/,
|
|
28
|
+
/\bwas\s+[$£€¥]?\s?\d[\d,.]*\s*[,;–—-]?\s*now\s+[$£€¥]?\s?\d/,
|
|
29
|
+
/\bcheap(?:er|est)\b/,
|
|
30
|
+
/\baffordable\b|\bbargain\b/,
|
|
31
|
+
/\bcosts?\s+(?:less|more|only|just|about|around|[$£€¥]?\d)/,
|
|
32
|
+
/\blow(?:er)?[\s-]cost\b/,
|
|
33
|
+
/\bsaves?\s+(?:you\s+)?(?:money|cash|[$£€¥]\s?\d)/,
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
kind: 'discount',
|
|
38
|
+
patterns: [
|
|
39
|
+
/\d+(?:\.\d+)?\s?(?:%|percent)\s*(?:off\b|discount|reduction|less\b)/,
|
|
40
|
+
/\b(?:save|saving|savings|off|discount|extra|up to)\s+(?:up to\s+)?\d+(?:\.\d+)?\s?(?:%|percent)/,
|
|
41
|
+
/\bdiscount(?:s|ed)?\b/,
|
|
42
|
+
/\bsale\b|\bmarked down\b|\bdeal of\b/,
|
|
43
|
+
/\bhalf[\s-]?(?:price|off)\b/,
|
|
44
|
+
/\bsaves?\s+\d[\d.,]*\s+off\b/,
|
|
45
|
+
/\bclearance\s+(?:sale|price|event|deal)\b|\bon clearance\b/,
|
|
46
|
+
/\bprice reduced\b|\breduced price\b|\breduced by \d+\s?(?:%|percent)|\breduced\s+(?:this|next|last)\s+week\b/,
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
kind: 'delivery',
|
|
51
|
+
patterns: [
|
|
52
|
+
/\bdelivery\b|\bshipping\b/,
|
|
53
|
+
/\bnext[\s-]day\b|\bsame[\s-]day\b|\bovernight\b/,
|
|
54
|
+
/\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/,
|
|
55
|
+
/\b(?:ships?|arrives?|arriving)\s+(?:in|within)\s+\d/,
|
|
56
|
+
/\bin time for\b/,
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
kind: 'stock',
|
|
61
|
+
patterns: [
|
|
62
|
+
/\b(?:in|out of|low on) stock\b/,
|
|
63
|
+
/\blimited stock\b/,
|
|
64
|
+
/\brestocked?\b|\bsold out\b/,
|
|
65
|
+
/\b(?:only\s+)?(?:\d+|a few|a handful|a couple|one|few)\s+(?:left|remain(?:s|ing)?)\b/,
|
|
66
|
+
/\bselling fast\b|\b(?:almost|nearly) gone\b|\bwhile stocks last\b/,
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
const CONFUSABLES = {
|
|
71
|
+
а: 'a',
|
|
72
|
+
в: 'b',
|
|
73
|
+
е: 'e',
|
|
74
|
+
к: 'k',
|
|
75
|
+
м: 'm',
|
|
76
|
+
н: 'h',
|
|
77
|
+
о: 'o',
|
|
78
|
+
р: 'p',
|
|
79
|
+
с: 'c',
|
|
80
|
+
т: 't',
|
|
81
|
+
х: 'x',
|
|
82
|
+
у: 'y',
|
|
83
|
+
ѕ: 's',
|
|
84
|
+
і: 'i',
|
|
85
|
+
ј: 'j',
|
|
86
|
+
ԁ: 'd',
|
|
87
|
+
һ: 'h',
|
|
88
|
+
ӏ: 'l',
|
|
89
|
+
α: 'a',
|
|
90
|
+
ε: 'e',
|
|
91
|
+
ι: 'i',
|
|
92
|
+
κ: 'k',
|
|
93
|
+
ν: 'v',
|
|
94
|
+
ο: 'o',
|
|
95
|
+
ρ: 'p',
|
|
96
|
+
τ: 't',
|
|
97
|
+
υ: 'u',
|
|
98
|
+
χ: 'x',
|
|
99
|
+
ϲ: 'c',
|
|
100
|
+
};
|
|
101
|
+
const INVISIBLE = /[\p{Cf}\p{Default_Ignorable_Code_Point}\p{Cc}]/gu;
|
|
102
|
+
const SPACING = /[\t\n\v\f\r\u0085]/;
|
|
103
|
+
const MARKS = /[\p{Mn}\p{Me}]/gu;
|
|
104
|
+
function normaliseForClaims(text) {
|
|
105
|
+
const lowered = text
|
|
106
|
+
.replace(INVISIBLE, (char) => (SPACING.test(char) ? char : ''))
|
|
107
|
+
.normalize('NFKC')
|
|
108
|
+
.toLowerCase()
|
|
109
|
+
.normalize('NFC')
|
|
110
|
+
.replace(MARKS, '');
|
|
111
|
+
let folded = '';
|
|
112
|
+
for (const char of lowered)
|
|
113
|
+
folded += CONFUSABLES[char] ?? char;
|
|
114
|
+
return folded;
|
|
115
|
+
}
|
|
116
|
+
export const ALLOWED_PHRASES = ['does not feel cheap', "doesn't feel cheap", 'last few miles'];
|
|
117
|
+
const DIGIT = /\p{Nd}/u;
|
|
118
|
+
const BARE_EXPONENT = /^[+-]?\d+(?:\.\d+)?[eE]([+-]?\d+)$/;
|
|
119
|
+
const MAX_EXPONENT = 1000;
|
|
120
|
+
function canLayOut(fact) {
|
|
121
|
+
const match = BARE_EXPONENT.exec(fact.trim());
|
|
122
|
+
if (match === null)
|
|
123
|
+
return true;
|
|
124
|
+
return Math.abs(Number(match[1] ?? '')) <= MAX_EXPONENT;
|
|
125
|
+
}
|
|
126
|
+
export function productFacts(product) {
|
|
127
|
+
const facts = [];
|
|
128
|
+
if (DIGIT.test(product.category) && canLayOut(product.category))
|
|
129
|
+
facts.push(product.category);
|
|
130
|
+
for (const tag of product.tags) {
|
|
131
|
+
if (DIGIT.test(tag) && canLayOut(tag))
|
|
132
|
+
facts.push(tag);
|
|
133
|
+
}
|
|
134
|
+
return facts;
|
|
135
|
+
}
|
|
136
|
+
export function hostFacts(input) {
|
|
137
|
+
const pooled = new Set();
|
|
138
|
+
const bySku = new Map();
|
|
139
|
+
for (const product of offeredCandidates(input)) {
|
|
140
|
+
const own = productFacts(product);
|
|
141
|
+
bySku.set(product.sku, own);
|
|
142
|
+
for (const fact of own)
|
|
143
|
+
pooled.add(fact);
|
|
144
|
+
}
|
|
145
|
+
return { pooled: [...pooled], bySku };
|
|
146
|
+
}
|
|
147
|
+
export function claimIn(text) {
|
|
148
|
+
const normalised = normaliseForClaims(text);
|
|
149
|
+
for (const claim of CLAIM_PATTERNS) {
|
|
150
|
+
for (const pattern of claim.patterns) {
|
|
151
|
+
if (pattern.test(normalised))
|
|
152
|
+
return claim.kind;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=claim-screening.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claim-screening.js","sourceRoot":"","sources":["../src/claim-screening.ts"],"names":[],"mappings":"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"}
|
|
@@ -3,51 +3,11 @@ import type { ComponentProvider, TokenUsage } from './provider.js';
|
|
|
3
3
|
import { type RankOrder } from './product-selection.js';
|
|
4
4
|
import { type SpecCache } from './spec-cache.js';
|
|
5
5
|
import { type TrackingInputDraft } from './tracking-input.js';
|
|
6
|
-
/**
|
|
7
|
-
* Turns one tracking payload into one renderable component.
|
|
8
|
-
*
|
|
9
|
-
* Everything else in this package is a piece of that sentence; this module is
|
|
10
|
-
* the order they go in. It is deliberately the only place that knows the whole
|
|
11
|
-
* sequence, and it is written as a straight line so the sequence is readable:
|
|
12
|
-
*
|
|
13
|
-
* validate → digest → cache → generate → reconcile → render
|
|
14
|
-
*
|
|
15
|
-
* The single promise it makes to a caller is that `generate` always returns
|
|
16
|
-
* something renderable. A model that is slow, refusing, erroring, rate-limited
|
|
17
|
-
* or simply not configured produces the deterministic component instead. The
|
|
18
|
-
* only way it rejects is a malformed payload, which is a caller bug and should
|
|
19
|
-
* be loud.
|
|
20
|
-
*/
|
|
21
|
-
/**
|
|
22
|
-
* Reported exactly once per call to `generate`, whatever happened.
|
|
23
|
-
*
|
|
24
|
-
* One flat shape rather than a variant per outcome, because the numbers the
|
|
25
|
-
* evaluation needs are ratios over all calls — hit rate, fallback share, model
|
|
26
|
-
* calls and cost per thousand views. A variant that some callers do not emit
|
|
27
|
-
* makes every one of those ratios wrong by however many it skipped, which is
|
|
28
|
-
* what happened when requests that joined an in-flight generation reported
|
|
29
|
-
* nothing at all.
|
|
30
|
-
*/
|
|
31
6
|
export interface GenerationEvent {
|
|
32
|
-
/** Null when no key was computed, which means no provider was configured. */
|
|
33
7
|
key: string | null;
|
|
34
8
|
source: SpecSource;
|
|
35
|
-
/** Wall-clock milliseconds for the whole call. */
|
|
36
9
|
elapsedMs: number;
|
|
37
|
-
/**
|
|
38
|
-
* True for the caller that sent the request, on every outcome — including a
|
|
39
|
-
* call that timed out, errored or came back unparseable. Requests that joined
|
|
40
|
-
* an in-flight generation share its answer and its usage figures, so cost
|
|
41
|
-
* must be summed over this flag rather than over every event.
|
|
42
|
-
*
|
|
43
|
-
* It counts requests sent, which is an upper bound on requests billed: an
|
|
44
|
-
* adapter that throws before it reaches the vendor looks the same from here
|
|
45
|
-
* as one that throws after. An upper bound is the useful direction — the
|
|
46
|
-
* calls that produce nothing are the ones worth seeing, and reporting them as
|
|
47
|
-
* no call at all hides them completely.
|
|
48
|
-
*/
|
|
49
10
|
calledModel: boolean;
|
|
50
|
-
/** What reconciliation removed. Absent when no spec was reconciled. */
|
|
51
11
|
violations?: string[];
|
|
52
12
|
usage?: TokenUsage;
|
|
53
13
|
degradedReason?: DegradedReason;
|
|
@@ -55,46 +15,16 @@ export interface GenerationEvent {
|
|
|
55
15
|
cache?: 'hit' | 'miss' | 'error' | 'timeout';
|
|
56
16
|
}
|
|
57
17
|
export interface ComponentGeneratorOptions {
|
|
58
|
-
/**
|
|
59
|
-
* Omit to run without a model. That is a supported configuration rather than
|
|
60
|
-
* a stub: it is the control arm of the benchmark, and the right setting for
|
|
61
|
-
* anyone who has not yet decided on a provider.
|
|
62
|
-
*/
|
|
63
18
|
provider?: ComponentProvider | null;
|
|
64
|
-
/** Defaults to an in-process cache. Pass `createNullSpecCache()` to disable. */
|
|
65
19
|
cache?: SpecCache;
|
|
66
|
-
/**
|
|
67
|
-
* How long the model gets. Past this the deterministic component renders and
|
|
68
|
-
* the request is aborted. Defaults to 1500ms.
|
|
69
|
-
*/
|
|
70
20
|
modelTimeoutMs?: number;
|
|
71
|
-
/**
|
|
72
|
-
* How long the cache gets. The shipped caches cannot exceed it, but the store
|
|
73
|
-
* is a port a host implements — a hung Redis read on the render path would
|
|
74
|
-
* hold the page open, which is exactly what this module exists to prevent.
|
|
75
|
-
*/
|
|
76
21
|
cacheTimeoutMs?: number;
|
|
77
|
-
/**
|
|
78
|
-
* 'cohort' shares one generated component between shoppers who look alike and
|
|
79
|
-
* fills in each shopper's own products. 'per-shopper' generates for the
|
|
80
|
-
* individual, which is what the benchmark compares against. Defaults to
|
|
81
|
-
* 'cohort'.
|
|
82
|
-
*/
|
|
83
22
|
generation?: 'cohort' | 'per-shopper';
|
|
84
|
-
/**
|
|
85
|
-
* How the products are ordered. 'signals' scores each candidate from this
|
|
86
|
-
* shopper's signals. 'given' keeps the order you sent, for a shop whose own
|
|
87
|
-
* ranking is better than four weights. Either way the exclusions and the
|
|
88
|
-
* stock check still apply, and each product still carries a basis
|
|
89
|
-
* reconciliation can verify. Defaults to 'signals'.
|
|
90
|
-
*/
|
|
91
23
|
rank?: RankOrder;
|
|
92
|
-
/** Observability. Never allowed to break a render. */
|
|
93
24
|
onEvent?: (event: GenerationEvent) => void;
|
|
94
25
|
}
|
|
95
26
|
export interface ComponentGenerator {
|
|
96
27
|
generate(input: TrackingInputDraft): Promise<ComponentSpec>;
|
|
97
|
-
/** The deterministic component, without consulting a model or a cache. */
|
|
98
28
|
generateDeterministic(input: TrackingInputDraft): ComponentSpec;
|
|
99
29
|
}
|
|
100
30
|
export declare class TimeoutError extends Error {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"component-generator.d.ts","sourceRoot":"","sources":["../src/component-generator.ts"],"names":[],"mappings":"AACA,OAAO,EAIL,KAAK,aAAa,EAClB,KAAK,cAAc,EAEnB,KAAK,UAAU,EAChB,MAAM,qBAAqB,CAAC;AAG7B,OAAO,KAAK,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"component-generator.d.ts","sourceRoot":"","sources":["../src/component-generator.ts"],"names":[],"mappings":"AACA,OAAO,EAIL,KAAK,aAAa,EAClB,KAAK,cAAc,EAEnB,KAAK,UAAU,EAChB,MAAM,qBAAqB,CAAC;AAG7B,OAAO,KAAK,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEnE,OAAO,EAAkB,KAAK,SAAS,EAAoB,MAAM,wBAAwB,CAAC;AAG1F,OAAO,EAKL,KAAK,SAAS,EACf,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAGL,KAAK,kBAAkB,EACxB,MAAM,qBAAqB,CAAC;AAE7B,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,MAAM,EAAE,UAAU,CAAC;IAEnB,SAAS,EAAE,MAAM,CAAC;IAElB,WAAW,EAAE,OAAO,CAAC;IAErB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC;CAC9C;AAED,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,EAAE,iBAAiB,GAAG,IAAI,CAAC;IAEpC,KAAK,CAAC,EAAE,SAAS,CAAC;IAElB,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB,UAAU,CAAC,EAAE,QAAQ,GAAG,aAAa,CAAC;IAEtC,IAAI,CAAC,EAAE,SAAS,CAAC;IAEjB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAC;CAC5C;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;IAE5D,qBAAqB,CAAC,KAAK,EAAE,kBAAkB,GAAG,aAAa,CAAC;CACjE;AAED,qBAAa,YAAa,SAAQ,KAAK;IACrC,YAAY,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAG9C;CACF;AAkGD,wBAAgB,wBAAwB,CACtC,OAAO,GAAE,yBAA8B,GACtC,kBAAkB,CA2LpB"}
|