@rudra-js/core 0.1.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/LICENSE +21 -0
- package/README.md +189 -0
- package/dist/component-generator.d.ts +84 -0
- package/dist/component-generator.d.ts.map +1 -0
- package/dist/component-generator.js +331 -0
- package/dist/component-generator.js.map +1 -0
- package/dist/component-spec.d.ts +426 -0
- package/dist/component-spec.d.ts.map +1 -0
- package/dist/component-spec.js +170 -0
- package/dist/component-spec.js.map +1 -0
- package/dist/fallback-component.d.ts +11 -0
- package/dist/fallback-component.d.ts.map +1 -0
- package/dist/fallback-component.js +69 -0
- package/dist/fallback-component.js.map +1 -0
- package/dist/fit-to-shopper.d.ts +4 -0
- package/dist/fit-to-shopper.d.ts.map +1 -0
- package/dist/fit-to-shopper.js +36 -0
- package/dist/fit-to-shopper.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/model-prompt.d.ts +29 -0
- package/dist/model-prompt.d.ts.map +1 -0
- package/dist/model-prompt.js +227 -0
- package/dist/model-prompt.js.map +1 -0
- package/dist/product-selection.d.ts +33 -0
- package/dist/product-selection.d.ts.map +1 -0
- package/dist/product-selection.js +102 -0
- package/dist/product-selection.js.map +1 -0
- package/dist/provider.d.ts +80 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +23 -0
- package/dist/provider.js.map +1 -0
- package/dist/reconciliation.d.ts +49 -0
- package/dist/reconciliation.d.ts.map +1 -0
- package/dist/reconciliation.js +564 -0
- package/dist/reconciliation.js.map +1 -0
- package/dist/signal-digest.d.ts +66 -0
- package/dist/signal-digest.d.ts.map +1 -0
- package/dist/signal-digest.js +224 -0
- package/dist/signal-digest.js.map +1 -0
- package/dist/spec-cache.d.ts +88 -0
- package/dist/spec-cache.d.ts.map +1 -0
- package/dist/spec-cache.js +152 -0
- package/dist/spec-cache.js.map +1 -0
- package/dist/tracking-input.d.ts +258 -0
- package/dist/tracking-input.d.ts.map +1 -0
- package/dist/tracking-input.js +241 -0
- package/dist/tracking-input.js.map +1 -0
- package/package.json +60 -0
- package/src/component-generator.ts +521 -0
- package/src/component-spec.ts +243 -0
- package/src/fallback-component.ts +77 -0
- package/src/fit-to-shopper.ts +45 -0
- package/src/index.ts +102 -0
- package/src/model-prompt.ts +258 -0
- package/src/product-selection.ts +153 -0
- package/src/provider.ts +98 -0
- package/src/reconciliation.ts +675 -0
- package/src/signal-digest.ts +335 -0
- package/src/spec-cache.ts +223 -0
- package/src/tracking-input.ts +300 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Clive Dsouza
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# @rudra-js/core
|
|
2
|
+
|
|
3
|
+
The contracts and logic that turn one tracking payload into one renderable
|
|
4
|
+
component specification.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
npm install @rudra-js/core zod@^4
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`zod` is a peer dependency: the package's public API _is_ zod schemas, so your
|
|
13
|
+
application and this package must resolve the same zod instance. **zod 4 is
|
|
14
|
+
required** — the schemas use zod 4 APIs, and installing into a zod 3 app fails
|
|
15
|
+
with `ERESOLVE` rather than anything more helpful.
|
|
16
|
+
|
|
17
|
+
## Running without a model
|
|
18
|
+
|
|
19
|
+
`createComponentGenerator` takes a `provider`. Leave it out, or pass `null`, and
|
|
20
|
+
nothing calls a model and nothing is billed:
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
const generator = createComponentGenerator({ provider: null });
|
|
24
|
+
const spec = await generator.generate(input);
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
That is a supported configuration rather than a stub. It is the control arm of
|
|
28
|
+
the benchmark, and the right setting for anyone who has not yet decided on a
|
|
29
|
+
provider. `generate` returns a promise either way, so the shape of your code
|
|
30
|
+
does not change when you add one.
|
|
31
|
+
|
|
32
|
+
The deterministic component emits exactly one **grid** block — or nothing, when
|
|
33
|
+
no candidate is in stock — with a headline from a fixed set of four. Every other
|
|
34
|
+
block kind in the vocabulary (hero, carousel, banner, copy, bundle) only ever
|
|
35
|
+
comes from a model. If you are wiring bundles and none appear, that is why, and
|
|
36
|
+
not your catalog.
|
|
37
|
+
|
|
38
|
+
To render a spec you wrote yourself, without a model, pass
|
|
39
|
+
`createFixedSpecProvider(spec)` as the provider. It answers every request with
|
|
40
|
+
that spec, which is how the tests exercise blocks the deterministic component
|
|
41
|
+
never emits.
|
|
42
|
+
|
|
43
|
+
## `tracking-input`
|
|
44
|
+
|
|
45
|
+
The boundary between a host application and rudra-js. rudra-js collects,
|
|
46
|
+
stores and aggregates nothing — the host owns its tracking pipeline and hands
|
|
47
|
+
the framework one JSON object per render.
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { parseTrackingInput } from '@rudra-js/core';
|
|
51
|
+
|
|
52
|
+
const input = parseTrackingInput({
|
|
53
|
+
user: { id: 'shopper-1' },
|
|
54
|
+
context: { surface: 'pdp', currentSku: 'TR-102' },
|
|
55
|
+
signals: {
|
|
56
|
+
likes: [{ sku: 'TR-104' }],
|
|
57
|
+
recentSearches: ['waterproof trail shoe'],
|
|
58
|
+
},
|
|
59
|
+
candidates: [
|
|
60
|
+
{
|
|
61
|
+
sku: 'TR-102',
|
|
62
|
+
title: 'Switchback Trail Shoe GTX',
|
|
63
|
+
category: 'Trail Running',
|
|
64
|
+
price: 174,
|
|
65
|
+
imageUrl: 'https://cdn.example.com/tr-102.png', // or '/images/tr-102.png'
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`parseTrackingInput` throws a `ZodError`; `safeParseTrackingInput` returns a
|
|
72
|
+
`TrackingInputResult` instead, so a host can inspect `result.error.issues`
|
|
73
|
+
without importing zod itself.
|
|
74
|
+
|
|
75
|
+
### Cold start is not an error
|
|
76
|
+
|
|
77
|
+
A payload with no `signals` block is a first-time visitor, not a malformed
|
|
78
|
+
request. Every category defaults to `[]`, so the host needs no special case.
|
|
79
|
+
|
|
80
|
+
### What the host must supply
|
|
81
|
+
|
|
82
|
+
`user.id`, `context.surface`, and at least one entry in `candidates`.
|
|
83
|
+
`candidates` is the merchandising boundary: whatever the host leaves out cannot
|
|
84
|
+
be recommended, which is what makes it impossible to surface a product that
|
|
85
|
+
does not exist or is not merchandised for this shopper. SKUs must be unique.
|
|
86
|
+
|
|
87
|
+
`bundles` is optional: the sets the shop sells together, each with the shop's
|
|
88
|
+
own price for the set, the currency that price is in, and, if you want one,
|
|
89
|
+
your own name for it. Every product in a set must also be a candidate — that is
|
|
90
|
+
what lets the same checks that pass a single product pass a whole set, and what
|
|
91
|
+
lets the renderer look the members up in the catalog it already has. Ids must be
|
|
92
|
+
unique, and one set must not name the same product twice.
|
|
93
|
+
|
|
94
|
+
The model never picks a set and is never told a price. It only asks for a
|
|
95
|
+
bundle block and writes the words around it; the framework picks which set when
|
|
96
|
+
the page is served, from what the shopper has in their basket, has looked at,
|
|
97
|
+
or is browsing now.
|
|
98
|
+
|
|
99
|
+
Every word the model writes is read for claims: the headline, the subheadline,
|
|
100
|
+
a hero, a banner, a block title, the copy block, the reason under a product,
|
|
101
|
+
and the words around the set. Text you supplied is never read this way — a
|
|
102
|
+
product title, a category and a bundle `label` are your words, not the model's.
|
|
103
|
+
|
|
104
|
+
The framework drops text that makes a claim it cannot check. It looks for
|
|
105
|
+
money, a customer score, a delivery date and a count of what is left, and it
|
|
106
|
+
leaves a specification alone even when the specification has a number in it.
|
|
107
|
+
Spotting one is not a guarantee, the way checking a price against your catalog
|
|
108
|
+
is. A field that cannot be empty — a headline, a banner's text — is emptied
|
|
109
|
+
instead of nulled, so the block drops the way any block with no text drops, and
|
|
110
|
+
an emptied page headline makes the whole generation unusable.
|
|
111
|
+
|
|
112
|
+
For the set the prompt also tells the model to write about the offer, not the
|
|
113
|
+
products in it, and never to say the set saves money or by how much. Pass a
|
|
114
|
+
`label` on the bundle to put your own words on the set: a label is text you
|
|
115
|
+
wrote, not text the model wrote, and it renders ahead of the model's words.
|
|
116
|
+
|
|
117
|
+
### Defaults
|
|
118
|
+
|
|
119
|
+
| Field | Default |
|
|
120
|
+
| -------------------------- | ------------------- |
|
|
121
|
+
| `schemaVersion` | `'1'` |
|
|
122
|
+
| `context.slot` | `'recommendations'` |
|
|
123
|
+
| `context.locale` | `'en-US'` |
|
|
124
|
+
| `context.maxItems` | `4` |
|
|
125
|
+
| `candidates[].currency` | `'USD'` |
|
|
126
|
+
| `candidates[].isInStock` | `true` |
|
|
127
|
+
| `candidates[].tags` | `[]` |
|
|
128
|
+
| `signals.*` | `[]` |
|
|
129
|
+
| `bundles` | `[]` |
|
|
130
|
+
| `bundles[].currency` | `'USD'` |
|
|
131
|
+
| `mostViewed[].views` | `1` |
|
|
132
|
+
| `lastPurchased[].quantity` | `1` |
|
|
133
|
+
|
|
134
|
+
### Cohorts
|
|
135
|
+
|
|
136
|
+
By default one generated component is shared between shoppers who look alike,
|
|
137
|
+
and each shopper's own products are filled in per request. A cohort is the
|
|
138
|
+
shopper's segment, the surface and slot, the locale, the item count, whether
|
|
139
|
+
they are a first-time visitor, and the category they lean towards. Everything
|
|
140
|
+
that makes a person an individual — who they are, what they liked, viewed or
|
|
141
|
+
searched for — is left out, which is what lets many page views reuse one call.
|
|
142
|
+
|
|
143
|
+
The candidate list is part of the cohort too, because the model is shown those
|
|
144
|
+
products and writes about them. In most shops candidates come from the page, so
|
|
145
|
+
everyone looking at it shares them. A shop that picks candidates per shopper
|
|
146
|
+
gets smaller cohorts, which is the honest outcome: its prompt really is
|
|
147
|
+
personal.
|
|
148
|
+
|
|
149
|
+
Pass `generation: 'per-shopper'` to generate for the individual instead. Then
|
|
150
|
+
the model chooses the products too, and every shopper pays for their own call.
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
createComponentGenerator({ provider, generation: 'per-shopper' });
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Limits
|
|
157
|
+
|
|
158
|
+
Every free-text field and every array is capped, because host strings end up
|
|
159
|
+
inside a model prompt and a model is billed per token. The caps live in
|
|
160
|
+
`FIELD_LIMITS` and are exported, so a host can validate against the same
|
|
161
|
+
numbers rather than discovering them from a rejection.
|
|
162
|
+
|
|
163
|
+
| Limit | Value | Applies to |
|
|
164
|
+
| -------------------- | ----- | ------------------------------------------------------------------- |
|
|
165
|
+
| `identifier` | 128 | `sku`, `category`, `surface`, `slot`, `interaction.type`, meta keys |
|
|
166
|
+
| `shortText` | 200 | `title`, `imageUrl`, `interaction.value`, meta values |
|
|
167
|
+
| `searchQuery` | 200 | `context.searchQuery`, `recentSearches[]` |
|
|
168
|
+
| `tag` | 64 | `tags[]` |
|
|
169
|
+
| `tagsPerProduct` | 20 | `tags` |
|
|
170
|
+
| `metaEntries` | 50 | `interaction.meta` |
|
|
171
|
+
| `signalsPerCategory` | 500 | each array under `signals` |
|
|
172
|
+
| `candidates` | 200 | `candidates` |
|
|
173
|
+
| `productsPerBundle` | 5 | `bundles[].skus`, which also needs at least 2 |
|
|
174
|
+
| `bundles` | 20 | `bundles` |
|
|
175
|
+
|
|
176
|
+
These bound each field individually; they are not an aggregate prompt budget.
|
|
177
|
+
Fitting a payload into a context window is `digest`'s job, and it trims rather
|
|
178
|
+
than throws.
|
|
179
|
+
|
|
180
|
+
### Unknown fields are rejected
|
|
181
|
+
|
|
182
|
+
Every fixed-shape object is a `strictObject`. A host that misspells
|
|
183
|
+
`recentSearches` gets an error, not a shopper who silently looks like a
|
|
184
|
+
first-time visitor. `interaction.meta` is the one dynamic shape — an open
|
|
185
|
+
record, minus the keys that would mutate a prototype instead of the object.
|
|
186
|
+
|
|
187
|
+
## Licence
|
|
188
|
+
|
|
189
|
+
[MIT](./LICENSE)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { type ComponentSpec, type DegradedReason, type SpecSource } from './component-spec.js';
|
|
2
|
+
import type { ComponentProvider, TokenUsage } from './provider.js';
|
|
3
|
+
import { type SpecCache } from './spec-cache.js';
|
|
4
|
+
import { type TrackingInputDraft } from './tracking-input.js';
|
|
5
|
+
/**
|
|
6
|
+
* Turns one tracking payload into one renderable component.
|
|
7
|
+
*
|
|
8
|
+
* Everything else in this package is a piece of that sentence; this module is
|
|
9
|
+
* the order they go in. It is deliberately the only place that knows the whole
|
|
10
|
+
* sequence, and it is written as a straight line so the sequence is readable:
|
|
11
|
+
*
|
|
12
|
+
* validate → digest → cache → generate → reconcile → render
|
|
13
|
+
*
|
|
14
|
+
* The single promise it makes to a caller is that `generate` always returns
|
|
15
|
+
* something renderable. A model that is slow, refusing, erroring, rate-limited
|
|
16
|
+
* or simply not configured produces the deterministic component instead. The
|
|
17
|
+
* only way it rejects is a malformed payload, which is a caller bug and should
|
|
18
|
+
* be loud.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Reported exactly once per call to `generate`, whatever happened.
|
|
22
|
+
*
|
|
23
|
+
* One flat shape rather than a variant per outcome, because the numbers the
|
|
24
|
+
* evaluation needs are ratios over all calls — hit rate, fallback share, model
|
|
25
|
+
* calls and cost per thousand views. A variant that some callers do not emit
|
|
26
|
+
* makes every one of those ratios wrong by however many it skipped, which is
|
|
27
|
+
* what happened when requests that joined an in-flight generation reported
|
|
28
|
+
* nothing at all.
|
|
29
|
+
*/
|
|
30
|
+
export interface GenerationEvent {
|
|
31
|
+
/** Null when no key was computed, which means no provider was configured. */
|
|
32
|
+
key: string | null;
|
|
33
|
+
source: SpecSource;
|
|
34
|
+
/** Wall-clock milliseconds for the whole call. */
|
|
35
|
+
elapsedMs: number;
|
|
36
|
+
/**
|
|
37
|
+
* True for the caller that sent the request, on every outcome — including a
|
|
38
|
+
* call that timed out, errored or came back unparseable. Requests that joined
|
|
39
|
+
* an in-flight generation share its answer and its usage figures, so cost
|
|
40
|
+
* must be summed over this flag rather than over every event.
|
|
41
|
+
*
|
|
42
|
+
* It counts requests sent, which is an upper bound on requests billed: an
|
|
43
|
+
* adapter that throws before it reaches the vendor looks the same from here
|
|
44
|
+
* as one that throws after. An upper bound is the useful direction — the
|
|
45
|
+
* calls that produce nothing are the ones worth seeing, and reporting them as
|
|
46
|
+
* no call at all hides them completely.
|
|
47
|
+
*/
|
|
48
|
+
calledModel: boolean;
|
|
49
|
+
/** What reconciliation removed. Absent when no spec was reconciled. */
|
|
50
|
+
violations?: string[];
|
|
51
|
+
usage?: TokenUsage;
|
|
52
|
+
degradedReason?: DegradedReason;
|
|
53
|
+
}
|
|
54
|
+
export interface ComponentGeneratorOptions {
|
|
55
|
+
/**
|
|
56
|
+
* Omit to run without a model. That is a supported configuration rather than
|
|
57
|
+
* a stub: it is the control arm of the benchmark, and the right setting for
|
|
58
|
+
* anyone who has not yet decided on a provider.
|
|
59
|
+
*/
|
|
60
|
+
provider?: ComponentProvider | null;
|
|
61
|
+
/** Defaults to an in-process cache. Pass `createNullSpecCache()` to disable. */
|
|
62
|
+
cache?: SpecCache;
|
|
63
|
+
/**
|
|
64
|
+
* How long the model gets. Past this the deterministic component renders and
|
|
65
|
+
* the request is aborted. Defaults to 1500ms.
|
|
66
|
+
*/
|
|
67
|
+
modelTimeoutMs?: number;
|
|
68
|
+
/**
|
|
69
|
+
* How long the cache gets. The shipped caches cannot exceed it, but the store
|
|
70
|
+
* is a port a host implements — a hung Redis read on the render path would
|
|
71
|
+
* hold the page open, which is exactly what this module exists to prevent.
|
|
72
|
+
*/
|
|
73
|
+
cacheTimeoutMs?: number;
|
|
74
|
+
generation?: 'cohort' | 'per-shopper';
|
|
75
|
+
/** Observability. Never allowed to break a render. */
|
|
76
|
+
onEvent?: (event: GenerationEvent) => void;
|
|
77
|
+
}
|
|
78
|
+
export interface ComponentGenerator {
|
|
79
|
+
generate(input: TrackingInputDraft): Promise<ComponentSpec>;
|
|
80
|
+
/** The deterministic component, without consulting a model or a cache. */
|
|
81
|
+
generateDeterministic(input: TrackingInputDraft): ComponentSpec;
|
|
82
|
+
}
|
|
83
|
+
export declare function createComponentGenerator(options?: ComponentGeneratorOptions): ComponentGenerator;
|
|
84
|
+
//# sourceMappingURL=component-generator.d.ts.map
|
|
@@ -0,0 +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;AAUnE,OAAO,EAKL,KAAK,SAAS,EACf,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAGL,KAAK,kBAAkB,EACxB,MAAM,qBAAqB,CAAC;AAE7B;;;;;;;;;;;;;;GAcG;AAEH;;;;;;;;;GASG;AACH,MAAM,WAAW,eAAe;IAC9B,6EAA6E;IAC7E,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,MAAM,EAAE,UAAU,CAAC;IACnB,kDAAkD;IAClD,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;;;;;;;OAWG;IACH,WAAW,EAAE,OAAO,CAAC;IACrB,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,cAAc,CAAC,EAAE,cAAc,CAAC;CACjC;AAED,MAAM,WAAW,yBAAyB;IACxC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,iBAAiB,GAAG,IAAI,CAAC;IACpC,gFAAgF;IAChF,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAIxB,UAAU,CAAC,EAAE,QAAQ,GAAG,aAAa,CAAC;IACtC,sDAAsD;IACtD,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;IAC5D,0EAA0E;IAC1E,qBAAqB,CAAC,KAAK,EAAE,kBAAkB,GAAG,aAAa,CAAC;CACjE;AAiKD,wBAAgB,wBAAwB,CACtC,OAAO,GAAE,yBAA8B,GACtC,kBAAkB,CA+OpB"}
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { SPEC_VERSION, generatedSpecSchema, } from './component-spec.js';
|
|
3
|
+
import { buildFallbackSpec } from './fallback-component.js';
|
|
4
|
+
import { buildPrompt } from './model-prompt.js';
|
|
5
|
+
import { MAX_BLOCKS, bundleForShopper, placeableHeroSkus, reconcileSpec, } from './reconciliation.js';
|
|
6
|
+
import { selectProducts } from './product-selection.js';
|
|
7
|
+
import { fitToShopper } from './fit-to-shopper.js';
|
|
8
|
+
import { buildDigest, toCohortDigest } from './signal-digest.js';
|
|
9
|
+
import { createMemorySpecCache, cohortCacheKey, specCacheKey, } from './spec-cache.js';
|
|
10
|
+
import { parseTrackingInput, } from './tracking-input.js';
|
|
11
|
+
class TimeoutError extends Error {
|
|
12
|
+
constructor(label, milliseconds) {
|
|
13
|
+
super(`${label} exceeded ${milliseconds}ms`);
|
|
14
|
+
this.name = 'TimeoutError';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Races a promise against a deadline.
|
|
19
|
+
*
|
|
20
|
+
* The deadline is enforced here rather than trusted to the thing being waited
|
|
21
|
+
* on. A provider that ignores its abort signal, or a store that never settles,
|
|
22
|
+
* must still not hold a page open.
|
|
23
|
+
*
|
|
24
|
+
* Once the deadline has fired the caller is told so, whatever the race
|
|
25
|
+
* actually settled with. Aborting is what makes that necessary: a provider
|
|
26
|
+
* honouring its half of the contract rejects from inside the `abort()` below,
|
|
27
|
+
* so its rejection reaches the race first and the deadline's own never wins.
|
|
28
|
+
* Reporting the error that happened to arrive would blame the vendor for the
|
|
29
|
+
* caller's deadline — and blame it most often on the best-behaved adapters.
|
|
30
|
+
*/
|
|
31
|
+
async function withinBudget(label, milliseconds, start) {
|
|
32
|
+
const controller = new AbortController();
|
|
33
|
+
let timer;
|
|
34
|
+
let expired;
|
|
35
|
+
const deadline = new Promise((_resolve, reject) => {
|
|
36
|
+
timer = setTimeout(() => {
|
|
37
|
+
expired = new TimeoutError(label, milliseconds);
|
|
38
|
+
controller.abort();
|
|
39
|
+
reject(expired);
|
|
40
|
+
}, milliseconds);
|
|
41
|
+
});
|
|
42
|
+
try {
|
|
43
|
+
return await Promise.race([start(controller.signal), deadline]);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
throw expired ?? error;
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
clearTimeout(timer);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Collapses concurrent work for the same key into one execution.
|
|
54
|
+
*
|
|
55
|
+
* Without this, a key that is not yet cached fans out into one model call per
|
|
56
|
+
* concurrent request — the same answer, bought many times over. The entry is
|
|
57
|
+
* removed as soon as it settles, so one failure does not poison the next
|
|
58
|
+
* attempt.
|
|
59
|
+
*/
|
|
60
|
+
function createSingleFlight() {
|
|
61
|
+
const inFlight = new Map();
|
|
62
|
+
return {
|
|
63
|
+
isRunning: (key) => inFlight.has(key),
|
|
64
|
+
run(key, task) {
|
|
65
|
+
const existing = inFlight.get(key);
|
|
66
|
+
if (existing)
|
|
67
|
+
return existing;
|
|
68
|
+
const started = task().finally(() => inFlight.delete(key));
|
|
69
|
+
inFlight.set(key, started);
|
|
70
|
+
return started;
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/** A cache entry is no more trustworthy than model output, so it is parsed too. */
|
|
75
|
+
const cachedSpecSchema = z.object({
|
|
76
|
+
spec: generatedSpecSchema,
|
|
77
|
+
generatedAt: z.number(),
|
|
78
|
+
});
|
|
79
|
+
/**
|
|
80
|
+
* Fills a cohort spec with this shopper's products, keeping room for a set.
|
|
81
|
+
*
|
|
82
|
+
* The grid used to take the whole item budget, so a bundle block later in the
|
|
83
|
+
* spec found nothing left and was dropped. The set is chosen first, its
|
|
84
|
+
* products are held back from the grid, and the grid's limit drops by what the
|
|
85
|
+
* set and the heroes have already spoken for.
|
|
86
|
+
*
|
|
87
|
+
* The arithmetic has to hold whatever order the model put the blocks in, so it
|
|
88
|
+
* is written as one sum over the whole spec rather than as a running budget:
|
|
89
|
+
* the grid gets `maxItems` minus every distinct product the set and the heroes
|
|
90
|
+
* will place. Nothing is then dropped for want of budget, and reconciliation
|
|
91
|
+
* reaches the same set this did — it can only ever have more placed than the
|
|
92
|
+
* pre-choice assumed, and never one of the set's own products.
|
|
93
|
+
*/
|
|
94
|
+
function fitCohortSpec(spec, input, digest) {
|
|
95
|
+
const picks = selectProducts(input, digest);
|
|
96
|
+
// Blocks past the cap never render, so a set is not worth reserving for one.
|
|
97
|
+
const blocks = spec.blocks.slice(0, MAX_BLOCKS);
|
|
98
|
+
let hasBundleBlock = false;
|
|
99
|
+
const aboveBundle = [];
|
|
100
|
+
for (const block of blocks) {
|
|
101
|
+
if (block.kind === 'bundle') {
|
|
102
|
+
hasBundleBlock = true;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
aboveBundle.push(block);
|
|
106
|
+
}
|
|
107
|
+
if (!hasBundleBlock)
|
|
108
|
+
return fitToShopper(spec, picks, digest.maxItems);
|
|
109
|
+
// Only the heroes above the bundle block are placed when it is reached, so
|
|
110
|
+
// they are all the choice may account for.
|
|
111
|
+
const chosen = bundleForShopper(input, digest, placeableHeroSkus(aboveBundle, input, digest));
|
|
112
|
+
if (!chosen)
|
|
113
|
+
return fitToShopper(spec, picks, digest.maxItems);
|
|
114
|
+
const spokenFor = new Set(chosen.skus);
|
|
115
|
+
for (const sku of placeableHeroSkus(blocks, input, digest))
|
|
116
|
+
spokenFor.add(sku);
|
|
117
|
+
const roomLeft = digest.maxItems - spokenFor.size;
|
|
118
|
+
// A set is worth showing, but not at the cost of an empty grid.
|
|
119
|
+
if (roomLeft <= 0)
|
|
120
|
+
return fitToShopper(spec, picks, digest.maxItems);
|
|
121
|
+
const forGrid = [];
|
|
122
|
+
for (const pick of picks) {
|
|
123
|
+
if (!spokenFor.has(pick.product.sku))
|
|
124
|
+
forGrid.push(pick);
|
|
125
|
+
}
|
|
126
|
+
return fitToShopper(spec, forGrid, roomLeft);
|
|
127
|
+
}
|
|
128
|
+
/** Attaches the provenance the server owns. The model never supplies any of it. */
|
|
129
|
+
function withProvenance(spec, provenance) {
|
|
130
|
+
return { ...spec, specVersion: SPEC_VERSION, ...provenance };
|
|
131
|
+
}
|
|
132
|
+
export function createComponentGenerator(options = {}) {
|
|
133
|
+
const provider = options.provider ?? null;
|
|
134
|
+
const cache = options.cache ?? createMemorySpecCache();
|
|
135
|
+
const generation = options.generation ?? 'cohort';
|
|
136
|
+
const modelTimeoutMs = options.modelTimeoutMs ?? 1_500;
|
|
137
|
+
const cacheTimeoutMs = options.cacheTimeoutMs ?? 50;
|
|
138
|
+
const singleFlight = createSingleFlight();
|
|
139
|
+
const report = (event) => {
|
|
140
|
+
if (!options.onEvent)
|
|
141
|
+
return;
|
|
142
|
+
try {
|
|
143
|
+
options.onEvent(event);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
// A broken metrics hook must not take down a page.
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
const buildDeterministic = (input, digest, startedAt, key, degradedReason,
|
|
150
|
+
/**
|
|
151
|
+
* What the model call cost, when there was one. A generation that is
|
|
152
|
+
* unusable for this shopper was still asked for and still billed, so
|
|
153
|
+
* omitting it here would hide the calls that produce nothing — exactly the
|
|
154
|
+
* ones worth knowing about.
|
|
155
|
+
*/
|
|
156
|
+
modelCall = {
|
|
157
|
+
calledModel: false,
|
|
158
|
+
}) => {
|
|
159
|
+
const finishedAt = Date.now();
|
|
160
|
+
report({
|
|
161
|
+
key,
|
|
162
|
+
source: 'fallback',
|
|
163
|
+
elapsedMs: finishedAt - startedAt,
|
|
164
|
+
...modelCall,
|
|
165
|
+
degradedReason,
|
|
166
|
+
});
|
|
167
|
+
return withProvenance(buildFallbackSpec(input, digest), {
|
|
168
|
+
slot: digest.slot,
|
|
169
|
+
source: 'fallback',
|
|
170
|
+
generatedAt: finishedAt,
|
|
171
|
+
latencyMs: finishedAt - startedAt,
|
|
172
|
+
provider: null,
|
|
173
|
+
model: null,
|
|
174
|
+
degradedReason,
|
|
175
|
+
});
|
|
176
|
+
};
|
|
177
|
+
/**
|
|
178
|
+
* Reads the cache, treating anything unexpected as a miss.
|
|
179
|
+
*
|
|
180
|
+
* The value is re-validated because a store is a port a host implements, and
|
|
181
|
+
* what comes back is no more trustworthy than what a model returns — a shared
|
|
182
|
+
* store outlives a deploy, so it can hold entries written by an older shape of
|
|
183
|
+
* the spec. Generating again is always safe; handing an unvalidated object to
|
|
184
|
+
* reconciliation is not.
|
|
185
|
+
*/
|
|
186
|
+
const readCache = async (key) => {
|
|
187
|
+
try {
|
|
188
|
+
const stored = await withinBudget('cache read', cacheTimeoutMs, () => cache.get(key));
|
|
189
|
+
const parsed = cachedSpecSchema.safeParse(stored);
|
|
190
|
+
return parsed.success ? parsed.data : undefined;
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
// A store that is down or slow degrades to generating, not to an error
|
|
194
|
+
// page. Nothing here is worth failing a render over.
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
/**
|
|
199
|
+
* Writes to the cache without the render waiting for it.
|
|
200
|
+
*
|
|
201
|
+
* The spec is already in hand; nothing downstream needs the write to finish.
|
|
202
|
+
* Awaiting it put a second unbounded call to a host-implemented store on the
|
|
203
|
+
* render path, which is the failure this module exists to prevent arriving
|
|
204
|
+
* through the other door. The `Promise.resolve` wrapper is what catches a
|
|
205
|
+
* store that throws synchronously rather than rejecting.
|
|
206
|
+
*/
|
|
207
|
+
const storeInBackground = (key, cached) => {
|
|
208
|
+
void Promise.resolve()
|
|
209
|
+
.then(() => cache.set(key, cached))
|
|
210
|
+
.catch(() => {
|
|
211
|
+
// A store that cannot be written is not a reason to fail a render.
|
|
212
|
+
});
|
|
213
|
+
};
|
|
214
|
+
/**
|
|
215
|
+
* Asks the model.
|
|
216
|
+
*
|
|
217
|
+
* Deliberately does not decide whether the answer is usable. That depends on
|
|
218
|
+
* the asking shopper's live facts — stock, dislikes, what is in their basket
|
|
219
|
+
* — and none of those are in the cache key, so a verdict reached here would
|
|
220
|
+
* be handed to every request that joined this one. A null `spec` in the
|
|
221
|
+
* result means the answer did not satisfy the schema, which is a fault of the
|
|
222
|
+
* adapter rather than a judgement about any shopper — and is still a call
|
|
223
|
+
* that happened, so its usage comes back with it.
|
|
224
|
+
*/
|
|
225
|
+
const askModel = async (active, input, promptDigest) => {
|
|
226
|
+
const { system, user } = buildPrompt(input, promptDigest);
|
|
227
|
+
const result = await withinBudget('generation', modelTimeoutMs, (signal) => active.generate({ system, user, schema: generatedSpecSchema, signal }));
|
|
228
|
+
// Providers return parsed objects, but the shape is still model output.
|
|
229
|
+
const parsed = generatedSpecSchema.safeParse(result.spec);
|
|
230
|
+
return {
|
|
231
|
+
spec: parsed.success ? parsed.data : null,
|
|
232
|
+
...(result.usage ? { usage: result.usage } : {}),
|
|
233
|
+
};
|
|
234
|
+
};
|
|
235
|
+
return {
|
|
236
|
+
generateDeterministic(draft) {
|
|
237
|
+
const startedAt = Date.now();
|
|
238
|
+
const input = parseTrackingInput(draft);
|
|
239
|
+
return buildDeterministic(input, buildDigest(input), startedAt, null, 'requested');
|
|
240
|
+
},
|
|
241
|
+
async generate(draft) {
|
|
242
|
+
const startedAt = Date.now();
|
|
243
|
+
// Deliberately unguarded: an invalid payload is a caller bug, not a
|
|
244
|
+
// degraded render.
|
|
245
|
+
const input = parseTrackingInput(draft);
|
|
246
|
+
const digest = buildDigest(input);
|
|
247
|
+
if (!provider) {
|
|
248
|
+
return buildDeterministic(input, digest, startedAt, null, 'no-provider');
|
|
249
|
+
}
|
|
250
|
+
const providerId = `${provider.name}:${provider.model}`;
|
|
251
|
+
const key = generation === 'cohort'
|
|
252
|
+
? cohortCacheKey(digest, input.candidates.map((product) => product.sku), providerId)
|
|
253
|
+
: specCacheKey(digest, input.candidates.map((product) => product.sku), providerId);
|
|
254
|
+
const cached = await readCache(key);
|
|
255
|
+
let calledModel = false;
|
|
256
|
+
let answer;
|
|
257
|
+
// When the model produced this, not when it was served. A cached
|
|
258
|
+
// component is not newly generated, and pretending otherwise makes any
|
|
259
|
+
// measure of how stale a page is showing read as zero.
|
|
260
|
+
let generatedAt;
|
|
261
|
+
if (cached) {
|
|
262
|
+
answer = { spec: cached.spec };
|
|
263
|
+
generatedAt = cached.generatedAt;
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
// Asked before joining, because by the time the shared promise settles
|
|
267
|
+
// the entry is gone and there is no way to tell a leader from a
|
|
268
|
+
// follower — and they must not both be counted as a model call.
|
|
269
|
+
calledModel = !singleFlight.isRunning(key);
|
|
270
|
+
let call;
|
|
271
|
+
try {
|
|
272
|
+
call = await singleFlight.run(key, () => askModel(provider, input, generation === 'cohort' ? toCohortDigest(digest) : digest));
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
const reason = error instanceof TimeoutError ? 'timeout' : 'provider-error';
|
|
276
|
+
// The request went out. Leaving `calledModel` to default here reported
|
|
277
|
+
// every failed call as no call at all, so the calls that cost money
|
|
278
|
+
// and produced nothing were the only ones missing from the count.
|
|
279
|
+
return buildDeterministic(input, digest, startedAt, key, reason, { calledModel });
|
|
280
|
+
}
|
|
281
|
+
if (!call.spec) {
|
|
282
|
+
return buildDeterministic(input, digest, startedAt, key, 'invalid-generation', {
|
|
283
|
+
calledModel,
|
|
284
|
+
...(call.usage ? { usage: call.usage } : {}),
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
answer = { spec: call.spec, ...(call.usage ? { usage: call.usage } : {}) };
|
|
288
|
+
generatedAt = Date.now();
|
|
289
|
+
// Stored unreconciled on purpose, and stored even when it is unusable
|
|
290
|
+
// for this shopper. Reconciliation narrows a spec to one shopper's live
|
|
291
|
+
// facts, and those move independently of the key — a product can sell
|
|
292
|
+
// out and come back without the candidate list changing. Keeping what
|
|
293
|
+
// the model said means the restock is picked up from cache rather than
|
|
294
|
+
// paid for again.
|
|
295
|
+
if (calledModel)
|
|
296
|
+
storeInBackground(key, { spec: answer.spec, generatedAt });
|
|
297
|
+
}
|
|
298
|
+
// One place where anything is served, whichever side of the cache it came
|
|
299
|
+
// from, and always against the facts of the shopper asking now.
|
|
300
|
+
// A cohort spec names products chosen for whoever asked first.
|
|
301
|
+
const served = generation === 'cohort' ? fitCohortSpec(answer.spec, input, digest) : answer.spec;
|
|
302
|
+
const reconciled = reconcileSpec(served, input, digest);
|
|
303
|
+
if (!reconciled.isUsable) {
|
|
304
|
+
return buildDeterministic(input, digest, startedAt, key, 'unusable-on-serve', {
|
|
305
|
+
calledModel,
|
|
306
|
+
violations: reconciled.violations,
|
|
307
|
+
...(answer.usage ? { usage: answer.usage } : {}),
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
const finishedAt = Date.now();
|
|
311
|
+
const source = cached ? 'cache' : 'llm';
|
|
312
|
+
report({
|
|
313
|
+
key,
|
|
314
|
+
source,
|
|
315
|
+
elapsedMs: finishedAt - startedAt,
|
|
316
|
+
calledModel,
|
|
317
|
+
violations: reconciled.violations,
|
|
318
|
+
...(answer.usage ? { usage: answer.usage } : {}),
|
|
319
|
+
});
|
|
320
|
+
return withProvenance(reconciled.spec, {
|
|
321
|
+
slot: digest.slot,
|
|
322
|
+
source,
|
|
323
|
+
generatedAt,
|
|
324
|
+
latencyMs: finishedAt - startedAt,
|
|
325
|
+
provider: provider.name,
|
|
326
|
+
model: provider.model,
|
|
327
|
+
});
|
|
328
|
+
},
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
//# sourceMappingURL=component-generator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"component-generator.js","sourceRoot":"","sources":["../src/component-generator.ts"],"names":[],"mappings":"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"}
|