@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.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +189 -0
  3. package/dist/component-generator.d.ts +84 -0
  4. package/dist/component-generator.d.ts.map +1 -0
  5. package/dist/component-generator.js +331 -0
  6. package/dist/component-generator.js.map +1 -0
  7. package/dist/component-spec.d.ts +426 -0
  8. package/dist/component-spec.d.ts.map +1 -0
  9. package/dist/component-spec.js +170 -0
  10. package/dist/component-spec.js.map +1 -0
  11. package/dist/fallback-component.d.ts +11 -0
  12. package/dist/fallback-component.d.ts.map +1 -0
  13. package/dist/fallback-component.js +69 -0
  14. package/dist/fallback-component.js.map +1 -0
  15. package/dist/fit-to-shopper.d.ts +4 -0
  16. package/dist/fit-to-shopper.d.ts.map +1 -0
  17. package/dist/fit-to-shopper.js +36 -0
  18. package/dist/fit-to-shopper.js.map +1 -0
  19. package/dist/index.d.ts +19 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +19 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/model-prompt.d.ts +29 -0
  24. package/dist/model-prompt.d.ts.map +1 -0
  25. package/dist/model-prompt.js +227 -0
  26. package/dist/model-prompt.js.map +1 -0
  27. package/dist/product-selection.d.ts +33 -0
  28. package/dist/product-selection.d.ts.map +1 -0
  29. package/dist/product-selection.js +102 -0
  30. package/dist/product-selection.js.map +1 -0
  31. package/dist/provider.d.ts +80 -0
  32. package/dist/provider.d.ts.map +1 -0
  33. package/dist/provider.js +23 -0
  34. package/dist/provider.js.map +1 -0
  35. package/dist/reconciliation.d.ts +49 -0
  36. package/dist/reconciliation.d.ts.map +1 -0
  37. package/dist/reconciliation.js +564 -0
  38. package/dist/reconciliation.js.map +1 -0
  39. package/dist/signal-digest.d.ts +66 -0
  40. package/dist/signal-digest.d.ts.map +1 -0
  41. package/dist/signal-digest.js +224 -0
  42. package/dist/signal-digest.js.map +1 -0
  43. package/dist/spec-cache.d.ts +88 -0
  44. package/dist/spec-cache.d.ts.map +1 -0
  45. package/dist/spec-cache.js +152 -0
  46. package/dist/spec-cache.js.map +1 -0
  47. package/dist/tracking-input.d.ts +258 -0
  48. package/dist/tracking-input.d.ts.map +1 -0
  49. package/dist/tracking-input.js +241 -0
  50. package/dist/tracking-input.js.map +1 -0
  51. package/package.json +60 -0
  52. package/src/component-generator.ts +521 -0
  53. package/src/component-spec.ts +243 -0
  54. package/src/fallback-component.ts +77 -0
  55. package/src/fit-to-shopper.ts +45 -0
  56. package/src/index.ts +102 -0
  57. package/src/model-prompt.ts +258 -0
  58. package/src/product-selection.ts +153 -0
  59. package/src/provider.ts +98 -0
  60. package/src/reconciliation.ts +675 -0
  61. package/src/signal-digest.ts +335 -0
  62. package/src/spec-cache.ts +223 -0
  63. 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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"}