@rudra-js/core 0.7.4 → 0.7.6
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 +14 -4
- package/dist/claim-screening.d.ts +3 -3
- package/dist/claim-screening.d.ts.map +1 -1
- package/dist/claim-screening.js +30 -93
- package/dist/claim-screening.js.map +1 -1
- package/dist/component-generator.d.ts.map +1 -1
- package/dist/component-generator.js +161 -176
- package/dist/component-generator.js.map +1 -1
- package/dist/component-spec.d.ts +1 -0
- package/dist/component-spec.d.ts.map +1 -1
- package/dist/component-spec.js +3 -0
- package/dist/component-spec.js.map +1 -1
- package/dist/fallback-component.d.ts +1 -1
- package/dist/fallback-component.d.ts.map +1 -1
- package/dist/fallback-component.js +9 -17
- package/dist/fallback-component.js.map +1 -1
- package/dist/fit-to-shopper.d.ts.map +1 -1
- package/dist/fit-to-shopper.js +19 -19
- package/dist/fit-to-shopper.js.map +1 -1
- package/dist/model-prompt.d.ts.map +1 -1
- package/dist/model-prompt.js +56 -39
- package/dist/model-prompt.js.map +1 -1
- package/dist/product-selection.d.ts.map +1 -1
- package/dist/product-selection.js +15 -31
- package/dist/product-selection.js.map +1 -1
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js.map +1 -1
- package/dist/reconciliation.d.ts +3 -3
- package/dist/reconciliation.d.ts.map +1 -1
- package/dist/reconciliation.js +133 -171
- package/dist/reconciliation.js.map +1 -1
- package/dist/signal-digest.d.ts +1 -1
- package/dist/signal-digest.d.ts.map +1 -1
- package/dist/signal-digest.js +51 -52
- package/dist/signal-digest.js.map +1 -1
- package/dist/spec-cache.d.ts +0 -4
- package/dist/spec-cache.d.ts.map +1 -1
- package/dist/spec-cache.js +15 -35
- package/dist/spec-cache.js.map +1 -1
- package/dist/tracking-input.d.ts.map +1 -1
- package/dist/tracking-input.js +16 -19
- package/dist/tracking-input.js.map +1 -1
- package/package.json +8 -6
- package/src/claim-screening.ts +34 -114
- package/src/component-generator.ts +223 -220
- package/src/component-spec.ts +4 -15
- package/src/fallback-component.ts +11 -20
- package/src/fit-to-shopper.ts +19 -21
- package/src/model-prompt.ts +55 -54
- package/src/product-selection.ts +21 -50
- package/src/provider.ts +0 -4
- package/src/reconciliation.ts +159 -268
- package/src/signal-digest.ts +50 -79
- package/src/spec-cache.ts +15 -48
- package/src/tracking-input.ts +21 -36
package/README.md
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
# @rudra-js/core
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
This package is part of [rudra-js](https://rudrajs.com).
|
|
4
|
+
|
|
5
|
+
rudra-js is an open-source TypeScript library (MIT) you install from npm. A language model designs one product-recommendation block for a page in your shop, rudra-js checks it against your catalog, and React renders it.
|
|
6
|
+
|
|
7
|
+
It is not a hosted platform, a visual or low-code app builder, or a workspace you sign up for.
|
|
8
|
+
|
|
9
|
+
You give this package one page view: the shopper and what they've been doing,
|
|
10
|
+
the page they're on, and the products you're willing to show. It gives back a
|
|
11
|
+
specification for the recommendations on that page, written by your model or
|
|
12
|
+
built without one, and checked against those products. There's no React in it.
|
|
13
|
+
`@rudra-js/react` turns the specification into markup.
|
|
5
14
|
|
|
6
15
|
Every export of this package is documented at [rudrajs.com](https://rudrajs.com/docs).
|
|
7
16
|
|
|
@@ -55,8 +64,9 @@ appear, that's why. It isn't your catalog.
|
|
|
55
64
|
|
|
56
65
|
Want to render a spec you wrote yourself, still without a model? Pass
|
|
57
66
|
`createFixedSpecProvider(spec)` as the provider and it answers every request with
|
|
58
|
-
that spec.
|
|
59
|
-
|
|
67
|
+
that spec. It still goes through reconciliation, so it needs a headline that
|
|
68
|
+
survives screening and at least one product on show, or you get the
|
|
69
|
+
deterministic component.
|
|
60
70
|
|
|
61
71
|
## Options
|
|
62
72
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { TrackingInput } from './tracking-input.js';
|
|
2
2
|
export declare const ALLOWED_PHRASES: string[];
|
|
3
|
-
export declare
|
|
3
|
+
export declare const NO_FACTS: readonly string[];
|
|
4
4
|
export interface HostFacts {
|
|
5
5
|
pooled: string[];
|
|
6
6
|
bySku: Map<string, string[]>;
|
|
7
7
|
}
|
|
8
8
|
export declare function hostFacts(input: TrackingInput): HostFacts;
|
|
9
|
-
export declare function
|
|
9
|
+
export declare function unbackedClaim(raw: string, clamped: string, facts: readonly string[]): string | null;
|
|
10
10
|
//# sourceMappingURL=claim-screening.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"claim-screening.d.ts","sourceRoot":"","sources":["../src/claim-screening.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"claim-screening.d.ts","sourceRoot":"","sources":["../src/claim-screening.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AA4EzD,eAAO,MAAM,eAAe,UAK3B,CAAC;AAKF,eAAO,MAAM,QAAQ,EAAE,SAAS,MAAM,EAAO,CAAC;AAE9C,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;AAaD,wBAAgB,aAAa,CAC3B,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,SAAS,MAAM,EAAE,GACvB,MAAM,GAAG,IAAI,CASf"}
|
package/dist/claim-screening.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { foldLookalikes, verify } from '@rudra-js/attested';
|
|
1
2
|
import { offeredCandidates } from './model-prompt.js';
|
|
2
3
|
const CLAIM_PATTERNS = [
|
|
3
4
|
{
|
|
@@ -10,7 +11,8 @@ const CLAIM_PATTERNS = [
|
|
|
10
11
|
/\bratings?\s+of\s+[0-5]\.\d\b/,
|
|
11
12
|
/\b[0-5](?:\.\d)?\s+out of\s+(?:5|five)\b/,
|
|
12
13
|
/\b(?:highly|top|best|well|poorly|five|four)[\s-]rated\b/,
|
|
13
|
-
/\brated\s+
|
|
14
|
+
/\brated\s+[0-5]\.\d\b/,
|
|
15
|
+
/\brated\s+(?:[0-5]|three|four|five)\s+(?:stars?|out of)\b/,
|
|
14
16
|
/\bbest[\s-]?sell(?:er|ers|ing)\b/,
|
|
15
17
|
/(?:\bnumber one|\bno\.? ?1|#\s?1)[\s-]?sell(?:er|ers|ing)\b/,
|
|
16
18
|
/\bloved by (?:thousands|hundreds|millions|\d)/,
|
|
@@ -43,7 +45,10 @@ const CLAIM_PATTERNS = [
|
|
|
43
45
|
/\bhalf[\s-]?(?:price|off)\b/,
|
|
44
46
|
/\bsaves?\s+\d[\d.,]*\s+off\b/,
|
|
45
47
|
/\bclearance\s+(?:sale|price|event|deal)\b|\bon clearance\b/,
|
|
46
|
-
/\bprice reduced\b
|
|
48
|
+
/\bprice reduced\b/,
|
|
49
|
+
/\breduced price\b/,
|
|
50
|
+
/\breduced by \d+\s?(?:%|percent)/,
|
|
51
|
+
/\breduced\s+(?:this|next|last)\s+week\b/,
|
|
47
52
|
],
|
|
48
53
|
},
|
|
49
54
|
{
|
|
@@ -68,114 +73,46 @@ const CLAIM_PATTERNS = [
|
|
|
68
73
|
],
|
|
69
74
|
},
|
|
70
75
|
];
|
|
71
|
-
const CONFUSABLES = {
|
|
72
|
-
а: 'a',
|
|
73
|
-
в: 'b',
|
|
74
|
-
е: 'e',
|
|
75
|
-
к: 'k',
|
|
76
|
-
м: 'm',
|
|
77
|
-
н: 'h',
|
|
78
|
-
о: 'o',
|
|
79
|
-
р: 'p',
|
|
80
|
-
с: 'c',
|
|
81
|
-
т: 't',
|
|
82
|
-
х: 'x',
|
|
83
|
-
у: 'y',
|
|
84
|
-
ѕ: 's',
|
|
85
|
-
і: 'i',
|
|
86
|
-
ј: 'j',
|
|
87
|
-
ԁ: 'd',
|
|
88
|
-
һ: 'h',
|
|
89
|
-
ӏ: 'l',
|
|
90
|
-
α: 'a',
|
|
91
|
-
ε: 'e',
|
|
92
|
-
ι: 'i',
|
|
93
|
-
κ: 'k',
|
|
94
|
-
ν: 'v',
|
|
95
|
-
ο: 'o',
|
|
96
|
-
ρ: 'p',
|
|
97
|
-
τ: 't',
|
|
98
|
-
υ: 'u',
|
|
99
|
-
χ: 'x',
|
|
100
|
-
ϳ: 'j',
|
|
101
|
-
ѵ: 'v',
|
|
102
|
-
};
|
|
103
|
-
// Capitals that lowercasing reads wrong: Ν becomes ν, which reads as v.
|
|
104
|
-
const CAPITALS = {
|
|
105
|
-
Β: 'b',
|
|
106
|
-
Ζ: 'z',
|
|
107
|
-
Η: 'h',
|
|
108
|
-
Μ: 'm',
|
|
109
|
-
Ν: 'n',
|
|
110
|
-
Υ: 'y',
|
|
111
|
-
Ү: 'y',
|
|
112
|
-
Ӏ: 'i',
|
|
113
|
-
Ԛ: 'q',
|
|
114
|
-
Ԝ: 'w',
|
|
115
|
-
Ϻ: 'm',
|
|
116
|
-
Ϝ: 'f',
|
|
117
|
-
Ԍ: 'g',
|
|
118
|
-
};
|
|
119
|
-
const INVISIBLE = /[\p{Cf}\p{Default_Ignorable_Code_Point}\p{Cc}]/gu;
|
|
120
|
-
const SPACING = /[\t\n\v\f\r\u0085]/;
|
|
121
|
-
const MARKS = /[\p{Mn}\p{Me}]/gu;
|
|
122
|
-
function normaliseForClaims(text, byLook) {
|
|
123
|
-
// NFKC would turn the lunate sigmas into ς and Σ, which the fold can't read as c.
|
|
124
|
-
const compatible = text
|
|
125
|
-
.replace(INVISIBLE, (char) => (SPACING.test(char) ? char : ''))
|
|
126
|
-
.replace(/[ϲϹ]/g, 'c')
|
|
127
|
-
.normalize('NFKC');
|
|
128
|
-
let cased = compatible;
|
|
129
|
-
if (byLook) {
|
|
130
|
-
cased = '';
|
|
131
|
-
for (const char of compatible)
|
|
132
|
-
cased += CAPITALS[char] ?? char;
|
|
133
|
-
}
|
|
134
|
-
const lowered = cased.toLowerCase().normalize('NFC').replace(MARKS, '');
|
|
135
|
-
let folded = '';
|
|
136
|
-
for (const char of lowered)
|
|
137
|
-
folded += CONFUSABLES[char] ?? char;
|
|
138
|
-
return folded;
|
|
139
|
-
}
|
|
140
76
|
export const ALLOWED_PHRASES = [
|
|
141
77
|
'does not feel cheap',
|
|
142
78
|
"doesn't feel cheap",
|
|
143
79
|
'last few miles',
|
|
144
80
|
'sustainable pace',
|
|
145
81
|
];
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if (DIGIT.test(product.category))
|
|
150
|
-
facts.push(product.category);
|
|
151
|
-
for (const tag of product.tags) {
|
|
152
|
-
if (DIGIT.test(tag))
|
|
153
|
-
facts.push(tag);
|
|
154
|
-
}
|
|
155
|
-
return facts;
|
|
156
|
-
}
|
|
82
|
+
const FACT_DIGIT = /\p{Nd}/u;
|
|
83
|
+
const NUMERAL = /[\p{Nd}\p{No}\p{Nl}]/u;
|
|
84
|
+
export const NO_FACTS = [];
|
|
157
85
|
export function hostFacts(input) {
|
|
158
86
|
const pooled = new Set();
|
|
159
87
|
const bySku = new Map();
|
|
160
88
|
for (const product of offeredCandidates(input)) {
|
|
161
|
-
const own =
|
|
89
|
+
const own = [product.category, ...product.tags].filter((text) => FACT_DIGIT.test(text));
|
|
162
90
|
bySku.set(product.sku, own);
|
|
163
91
|
for (const fact of own)
|
|
164
92
|
pooled.add(fact);
|
|
165
93
|
}
|
|
166
94
|
return { pooled: [...pooled], bySku };
|
|
167
95
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
for (const claim of CLAIM_PATTERNS) {
|
|
173
|
-
for (const pattern of claim.patterns) {
|
|
174
|
-
if (pattern.test(normalised))
|
|
175
|
-
return claim.kind;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
96
|
+
function kindIn(reading) {
|
|
97
|
+
for (const claim of CLAIM_PATTERNS) {
|
|
98
|
+
if (claim.patterns.some((pattern) => pattern.test(reading)))
|
|
99
|
+
return claim.kind;
|
|
178
100
|
}
|
|
179
101
|
return null;
|
|
180
102
|
}
|
|
103
|
+
function claimIn(text) {
|
|
104
|
+
return kindIn(foldLookalikes(text)) ?? kindIn(foldLookalikes(text, true));
|
|
105
|
+
}
|
|
106
|
+
export function unbackedClaim(raw, clamped, facts) {
|
|
107
|
+
const kind = claimIn(raw) ?? claimIn(clamped);
|
|
108
|
+
if (kind !== null)
|
|
109
|
+
return kind;
|
|
110
|
+
const values = NUMERAL.test(clamped) ? facts : NO_FACTS;
|
|
111
|
+
const result = verify(clamped, { values, allowedPhrases: ALLOWED_PHRASES });
|
|
112
|
+
if (!result.quantity.supported)
|
|
113
|
+
return 'quantity';
|
|
114
|
+
if (!result.wording.supported)
|
|
115
|
+
return 'wording';
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
181
118
|
//# sourceMappingURL=claim-screening.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"claim-screening.js","sourceRoot":"","sources":["../src/claim-screening.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAGtD,MAAM,cAAc,GAA2C;IAC7D;QACE,IAAI,EAAE,QAAQ;QACd,QAAQ,EAAE;YACR,2BAA2B;YAC3B,mDAAmD;YACnD,0BAA0B;
|
|
1
|
+
{"version":3,"file":"claim-screening.js","sourceRoot":"","sources":["../src/claim-screening.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAGtD,MAAM,cAAc,GAA2C;IAC7D;QACE,IAAI,EAAE,QAAQ;QACd,QAAQ,EAAE;YACR,2BAA2B;YAC3B,mDAAmD;YACnD,0BAA0B;YAC1B,kEAAkE;YAClE,+BAA+B;YAC/B,0CAA0C;YAC1C,yDAAyD;YACzD,uBAAuB;YACvB,2DAA2D;YAC3D,kCAAkC;YAClC,6DAA6D;YAC7D,+CAA+C;SAChD;KACF;IACD;QACE,IAAI,EAAE,OAAO;QACb,QAAQ,EAAE;YACR,SAAS;YACT,6DAA6D;YAC7D,yBAAyB;YACzB,wBAAwB;YACxB,qBAAqB;YACrB,yBAAyB;YACzB,6DAA6D;YAC7D,qBAAqB;YACrB,4BAA4B;YAC5B,2DAA2D;YAC3D,yBAAyB;YACzB,kDAAkD;SACnD;KACF;IACD;QACE,IAAI,EAAE,UAAU;QAChB,QAAQ,EAAE;YACR,qEAAqE;YACrE,iGAAiG;YACjG,uBAAuB;YACvB,sCAAsC;YACtC,6BAA6B;YAC7B,8BAA8B;YAC9B,4DAA4D;YAC5D,mBAAmB;YACnB,mBAAmB;YACnB,kCAAkC;YAClC,yCAAyC;SAC1C;KACF;IACD;QACE,IAAI,EAAE,UAAU;QAChB,QAAQ,EAAE;YACR,2BAA2B;YAC3B,iDAAiD;YACjD,kMAAkM;YAClM,qDAAqD;YACrD,iBAAiB;SAClB;KACF;IACD;QACE,IAAI,EAAE,OAAO;QACb,QAAQ,EAAE;YACR,gCAAgC;YAChC,mBAAmB;YACnB,6BAA6B;YAC7B,sFAAsF;YACtF,yFAAyF;YACzF,mEAAmE;SACpE;KACF;CACF,CAAC;AAEF,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,qBAAqB;IACrB,oBAAoB;IACpB,gBAAgB;IAChB,kBAAkB;CACnB,CAAC;AAEF,MAAM,UAAU,GAAG,SAAS,CAAC;AAC7B,MAAM,OAAO,GAAG,uBAAuB,CAAC;AAExC,MAAM,CAAC,MAAM,QAAQ,GAAsB,EAAE,CAAC;AAO9C,MAAM,UAAU,SAAS,CAAC,KAAoB;IAC5C,MAAM,MAAM,GAAG,IAAI,GAAG,EAAU,CAAC;IACjC,MAAM,KAAK,GAAG,IAAI,GAAG,EAAoB,CAAC;IAE1C,KAAK,MAAM,OAAO,IAAI,iBAAiB,CAAC,KAAK,CAAC,EAAE,CAAC;QAC/C,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,QAAQ,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QACxF,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QAC5B,KAAK,MAAM,IAAI,IAAI,GAAG;YAAE,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC3C,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,CAAC,GAAG,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC;AACxC,CAAC;AAED,SAAS,MAAM,CAAC,OAAe;IAC7B,KAAK,MAAM,KAAK,IAAI,cAAc,EAAE,CAAC;QACnC,IAAI,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAAE,OAAO,KAAK,CAAC,IAAI,CAAC;IACjF,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,OAAO,CAAC,IAAY;IAC3B,OAAO,MAAM,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,IAAI,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;AAC5E,CAAC;AAED,MAAM,UAAU,aAAa,CAC3B,GAAW,EACX,OAAe,EACf,KAAwB;IAExB,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9C,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAE/B,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC;IACxD,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,EAAE,EAAE,MAAM,EAAE,cAAc,EAAE,eAAe,EAAE,CAAC,CAAC;IAC5E,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS;QAAE,OAAO,UAAU,CAAC;IAClD,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,SAAS;QAAE,OAAO,SAAS,CAAC;IAChD,OAAO,IAAI,CAAC;AACd,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"component-generator.d.ts","sourceRoot":"","sources":["../src/component-generator.ts"],"names":[],"mappings":"AACA,OAAO,
|
|
1
|
+
{"version":3,"file":"component-generator.d.ts","sourceRoot":"","sources":["../src/component-generator.ts"],"names":[],"mappings":"AACA,OAAO,EAGL,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,EAAE,MAAM,wBAAwB,CAAC;AAGxE,OAAO,EAIL,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;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,OAAO,CAAC;IACrB,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;IACpC,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,QAAQ,GAAG,aAAa,CAAC;IACtC,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,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,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;AAuGD,wBAAgB,wBAAwB,CACtC,OAAO,GAAE,yBAA8B,GACtC,kBAAkB,CAgBpB"}
|
|
@@ -6,7 +6,7 @@ import { bundleForShopper, capBlocks, placeableHeroSkus, reconcileSpec } from '.
|
|
|
6
6
|
import { selectProducts } from './product-selection.js';
|
|
7
7
|
import { fitToShopper } from './fit-to-shopper.js';
|
|
8
8
|
import { buildDigest, toCohortDigest } from './signal-digest.js';
|
|
9
|
-
import { createMemorySpecCache,
|
|
9
|
+
import { createMemorySpecCache, specCacheKey, } from './spec-cache.js';
|
|
10
10
|
import { parseTrackingInput, } from './tracking-input.js';
|
|
11
11
|
export class TimeoutError extends Error {
|
|
12
12
|
constructor(label, milliseconds) {
|
|
@@ -35,20 +35,6 @@ async function withinBudget(label, milliseconds, start) {
|
|
|
35
35
|
clearTimeout(timer);
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
-
function createSingleFlight() {
|
|
39
|
-
const inFlight = new Map();
|
|
40
|
-
return {
|
|
41
|
-
isRunning: (key) => inFlight.has(key),
|
|
42
|
-
run(key, task) {
|
|
43
|
-
const existing = inFlight.get(key);
|
|
44
|
-
if (existing)
|
|
45
|
-
return existing;
|
|
46
|
-
const started = task().finally(() => inFlight.delete(key));
|
|
47
|
-
inFlight.set(key, started);
|
|
48
|
-
return started;
|
|
49
|
-
},
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
38
|
const cachedSpecSchema = z.object({
|
|
53
39
|
spec: generatedSpecSchema,
|
|
54
40
|
generatedAt: z.number(),
|
|
@@ -56,183 +42,182 @@ const cachedSpecSchema = z.object({
|
|
|
56
42
|
function fitCohortSpec(spec, input, digest, rank) {
|
|
57
43
|
const picks = selectProducts(input, digest, { rank });
|
|
58
44
|
const blocks = capBlocks(spec.blocks);
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
for (const block of blocks) {
|
|
62
|
-
if (block.kind === 'bundle') {
|
|
63
|
-
hasBundleBlock = true;
|
|
64
|
-
break;
|
|
65
|
-
}
|
|
66
|
-
aboveBundle.push(block);
|
|
67
|
-
}
|
|
68
|
-
if (!hasBundleBlock)
|
|
45
|
+
const bundleAt = blocks.findIndex((block) => block.kind === 'bundle');
|
|
46
|
+
if (bundleAt === -1)
|
|
69
47
|
return fitToShopper(spec, picks, digest.maxItems);
|
|
70
|
-
const
|
|
71
|
-
|
|
48
|
+
const heroesAbove = placeableHeroSkus(blocks.slice(0, bundleAt), input);
|
|
49
|
+
const bundle = bundleForShopper(input, digest, heroesAbove);
|
|
50
|
+
if (!bundle)
|
|
72
51
|
return fitToShopper(spec, picks, digest.maxItems);
|
|
73
|
-
const spokenFor = new Set(
|
|
52
|
+
const spokenFor = new Set(bundle.skus);
|
|
74
53
|
for (const sku of placeableHeroSkus(blocks, input))
|
|
75
54
|
spokenFor.add(sku);
|
|
76
55
|
const roomLeft = digest.maxItems - spokenFor.size;
|
|
77
56
|
if (roomLeft <= 0)
|
|
78
57
|
return fitToShopper(spec, picks, digest.maxItems);
|
|
79
|
-
const forGrid =
|
|
80
|
-
for (const pick of picks) {
|
|
81
|
-
if (!spokenFor.has(pick.product.sku))
|
|
82
|
-
forGrid.push(pick);
|
|
83
|
-
}
|
|
58
|
+
const forGrid = picks.filter((pick) => !spokenFor.has(pick.product.sku));
|
|
84
59
|
return fitToShopper(spec, forGrid, roomLeft);
|
|
85
60
|
}
|
|
86
61
|
export function createComponentGenerator(options = {}) {
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
return;
|
|
97
|
-
try {
|
|
98
|
-
options.onEvent(event);
|
|
99
|
-
}
|
|
100
|
-
catch { }
|
|
62
|
+
const settings = {
|
|
63
|
+
options,
|
|
64
|
+
provider: options.provider ?? null,
|
|
65
|
+
cache: options.cache ?? createMemorySpecCache(),
|
|
66
|
+
generation: options.generation ?? 'cohort',
|
|
67
|
+
rank: options.rank ?? 'signals',
|
|
68
|
+
modelTimeoutMs: options.modelTimeoutMs ?? 1_500,
|
|
69
|
+
cacheTimeoutMs: options.cacheTimeoutMs ?? 50,
|
|
70
|
+
inFlight: new Map(),
|
|
101
71
|
};
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const finishedAt = Date.now();
|
|
106
|
-
report({
|
|
107
|
-
key,
|
|
108
|
-
source: 'fallback',
|
|
109
|
-
elapsedMs: finishedAt - startedAt,
|
|
110
|
-
...modelCall,
|
|
111
|
-
degradedReason,
|
|
112
|
-
});
|
|
113
|
-
return {
|
|
114
|
-
...buildFallbackSpec(input, digest, { rank }),
|
|
115
|
-
specVersion: SPEC_VERSION,
|
|
116
|
-
slot: digest.slot,
|
|
117
|
-
source: 'fallback',
|
|
118
|
-
generatedAt: finishedAt,
|
|
119
|
-
latencyMs: finishedAt - startedAt,
|
|
120
|
-
provider: null,
|
|
121
|
-
model: null,
|
|
122
|
-
degradedReason,
|
|
123
|
-
};
|
|
72
|
+
return {
|
|
73
|
+
generate: (draft) => generate(settings, draft),
|
|
74
|
+
generateDeterministic: (draft) => generateDeterministic(settings, draft),
|
|
124
75
|
};
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
76
|
+
}
|
|
77
|
+
async function generate(settings, draft) {
|
|
78
|
+
const run = startRun(settings, draft);
|
|
79
|
+
const provider = settings.provider;
|
|
80
|
+
if (!provider)
|
|
81
|
+
return fallback(settings, run, null, 'no-provider');
|
|
82
|
+
const key = cacheKeyFor(provider, run);
|
|
83
|
+
const found = await findSpec(settings, provider, run, key);
|
|
84
|
+
if ('failed' in found)
|
|
85
|
+
return fallback(settings, run, key, found.failed, eventDetails(found.trace));
|
|
86
|
+
const reconciled = shapeForShopper(settings, run, found.spec);
|
|
87
|
+
const details = eventDetails(found.trace, reconciled.violations);
|
|
88
|
+
if (!reconciled.isUsable)
|
|
89
|
+
return fallback(settings, run, key, 'unusable-on-serve', details);
|
|
90
|
+
const finishedAt = Date.now();
|
|
91
|
+
report(settings, {
|
|
92
|
+
key,
|
|
93
|
+
source: found.source,
|
|
94
|
+
elapsedMs: finishedAt - run.startedAt,
|
|
95
|
+
...details,
|
|
96
|
+
});
|
|
97
|
+
return {
|
|
98
|
+
...reconciled.spec,
|
|
99
|
+
specVersion: SPEC_VERSION,
|
|
100
|
+
slot: run.digest.slot,
|
|
101
|
+
source: found.source,
|
|
102
|
+
generatedAt: found.generatedAt,
|
|
103
|
+
latencyMs: finishedAt - run.startedAt,
|
|
104
|
+
provider: provider.name,
|
|
105
|
+
model: provider.model,
|
|
134
106
|
};
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
107
|
+
}
|
|
108
|
+
function generateDeterministic(settings, draft) {
|
|
109
|
+
return fallback(settings, startRun(settings, draft), null, 'requested');
|
|
110
|
+
}
|
|
111
|
+
function startRun(settings, draft) {
|
|
112
|
+
const startedAt = Date.now();
|
|
113
|
+
const input = parseTrackingInput(draft);
|
|
114
|
+
const digest = buildDigest(input);
|
|
115
|
+
const promptDigest = settings.generation === 'cohort' ? toCohortDigest(digest) : digest;
|
|
116
|
+
return { input, digest, promptDigest, startedAt };
|
|
117
|
+
}
|
|
118
|
+
function cacheKeyFor(provider, run) {
|
|
119
|
+
const identity = { name: provider.name, model: provider.model };
|
|
120
|
+
const skus = run.input.candidates.map((product) => product.sku);
|
|
121
|
+
return specCacheKey(run.promptDigest, skus, identity);
|
|
122
|
+
}
|
|
123
|
+
async function findSpec(settings, provider, run, key) {
|
|
124
|
+
const read = await readCache(settings, key);
|
|
125
|
+
if (read.entry) {
|
|
126
|
+
const trace = { calledModel: false, cache: read.outcome, usage: undefined };
|
|
127
|
+
return { spec: read.entry.spec, generatedAt: read.entry.generatedAt, source: 'cache', trace };
|
|
128
|
+
}
|
|
129
|
+
const calledModel = !settings.inFlight.has(key);
|
|
130
|
+
let call;
|
|
131
|
+
try {
|
|
132
|
+
call = await sharedCall(settings, key, () => askModel(settings, provider, run));
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
const failed = error instanceof TimeoutError ? 'timeout' : 'provider-error';
|
|
136
|
+
return { failed, trace: { calledModel, cache: read.outcome, usage: undefined, error } };
|
|
137
|
+
}
|
|
138
|
+
const trace = { calledModel, cache: read.outcome, usage: call.usage };
|
|
139
|
+
if (!call.spec)
|
|
140
|
+
return { failed: 'invalid-generation', trace };
|
|
141
|
+
const generatedAt = Date.now();
|
|
142
|
+
if (calledModel)
|
|
143
|
+
storeInBackground(settings, key, { spec: call.spec, generatedAt });
|
|
144
|
+
return { spec: call.spec, generatedAt, source: 'llm', trace };
|
|
145
|
+
}
|
|
146
|
+
async function readCache(settings, key) {
|
|
147
|
+
try {
|
|
148
|
+
const stored = await withinBudget('cache read', settings.cacheTimeoutMs, () => settings.cache.get(key));
|
|
149
|
+
const parsed = cachedSpecSchema.safeParse(stored);
|
|
150
|
+
return parsed.success ? { outcome: 'hit', entry: parsed.data } : { outcome: 'miss' };
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
return { outcome: error instanceof TimeoutError ? 'timeout' : 'error' };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async function askModel(settings, provider, run) {
|
|
157
|
+
const { system, user } = buildPrompt(run.input, run.promptDigest);
|
|
158
|
+
const result = await withinBudget('generation', settings.modelTimeoutMs, (signal) => provider.generate({ system, user, schema: generatedSpecSchema, signal }));
|
|
159
|
+
const parsed = generatedSpecSchema.safeParse(result.spec);
|
|
160
|
+
return {
|
|
161
|
+
spec: parsed.success ? parsed.data : null,
|
|
162
|
+
...(result.usage ? { usage: result.usage } : {}),
|
|
139
163
|
};
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
return
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
164
|
+
}
|
|
165
|
+
function sharedCall(settings, key, ask) {
|
|
166
|
+
const running = settings.inFlight.get(key);
|
|
167
|
+
if (running)
|
|
168
|
+
return running;
|
|
169
|
+
const started = ask().finally(() => settings.inFlight.delete(key));
|
|
170
|
+
settings.inFlight.set(key, started);
|
|
171
|
+
return started;
|
|
172
|
+
}
|
|
173
|
+
function storeInBackground(settings, key, cached) {
|
|
174
|
+
void Promise.resolve()
|
|
175
|
+
.then(() => settings.cache.set(key, cached))
|
|
176
|
+
.catch(() => { });
|
|
177
|
+
}
|
|
178
|
+
function shapeForShopper(settings, run, spec) {
|
|
179
|
+
if (settings.generation !== 'cohort') {
|
|
180
|
+
return reconcileSpec(spec, run.input, run.digest, new Map());
|
|
181
|
+
}
|
|
182
|
+
const fitted = fitCohortSpec(spec, run.input, run.digest, settings.rank);
|
|
183
|
+
return reconcileSpec(fitted.spec, run.input, run.digest, fitted.ourReasons);
|
|
184
|
+
}
|
|
185
|
+
function fallback(settings, run, key, degradedReason, details = { calledModel: false }) {
|
|
186
|
+
const finishedAt = Date.now();
|
|
187
|
+
report(settings, {
|
|
188
|
+
key,
|
|
189
|
+
source: 'fallback',
|
|
190
|
+
elapsedMs: finishedAt - run.startedAt,
|
|
191
|
+
...details,
|
|
192
|
+
degradedReason,
|
|
193
|
+
});
|
|
194
|
+
return {
|
|
195
|
+
...buildFallbackSpec(run.input, run.digest, { rank: settings.rank }),
|
|
196
|
+
specVersion: SPEC_VERSION,
|
|
197
|
+
slot: run.digest.slot,
|
|
198
|
+
source: 'fallback',
|
|
199
|
+
generatedAt: finishedAt,
|
|
200
|
+
latencyMs: finishedAt - run.startedAt,
|
|
201
|
+
provider: null,
|
|
202
|
+
model: null,
|
|
203
|
+
degradedReason,
|
|
148
204
|
};
|
|
205
|
+
}
|
|
206
|
+
function eventDetails(trace, violations) {
|
|
149
207
|
return {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
},
|
|
155
|
-
async generate(draft) {
|
|
156
|
-
const startedAt = Date.now();
|
|
157
|
-
const input = parseTrackingInput(draft);
|
|
158
|
-
const digest = buildDigest(input);
|
|
159
|
-
if (!provider) {
|
|
160
|
-
return buildDeterministic(input, digest, startedAt, null, 'no-provider');
|
|
161
|
-
}
|
|
162
|
-
const identity = { name: provider.name, model: provider.model };
|
|
163
|
-
const candidateSkus = input.candidates.map((product) => product.sku);
|
|
164
|
-
const key = generation === 'cohort'
|
|
165
|
-
? cohortCacheKey(digest, candidateSkus, identity)
|
|
166
|
-
: specCacheKey(digest, candidateSkus, identity);
|
|
167
|
-
const read = await readCache(key);
|
|
168
|
-
const cached = read.entry;
|
|
169
|
-
let calledModel = false;
|
|
170
|
-
let answer;
|
|
171
|
-
let generatedAt;
|
|
172
|
-
if (cached) {
|
|
173
|
-
answer = { spec: cached.spec };
|
|
174
|
-
generatedAt = cached.generatedAt;
|
|
175
|
-
}
|
|
176
|
-
else {
|
|
177
|
-
calledModel = !singleFlight.isRunning(key);
|
|
178
|
-
let call;
|
|
179
|
-
try {
|
|
180
|
-
call = await singleFlight.run(key, () => askModel(provider, input, generation === 'cohort' ? toCohortDigest(digest) : digest));
|
|
181
|
-
}
|
|
182
|
-
catch (error) {
|
|
183
|
-
const reason = error instanceof TimeoutError ? 'timeout' : 'provider-error';
|
|
184
|
-
return buildDeterministic(input, digest, startedAt, key, reason, {
|
|
185
|
-
calledModel,
|
|
186
|
-
cache: read.outcome,
|
|
187
|
-
error,
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
if (!call.spec) {
|
|
191
|
-
return buildDeterministic(input, digest, startedAt, key, 'invalid-generation', {
|
|
192
|
-
calledModel,
|
|
193
|
-
cache: read.outcome,
|
|
194
|
-
...(call.usage ? { usage: call.usage } : {}),
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
answer = { spec: call.spec, ...(call.usage ? { usage: call.usage } : {}) };
|
|
198
|
-
generatedAt = Date.now();
|
|
199
|
-
if (calledModel)
|
|
200
|
-
storeInBackground(key, { spec: answer.spec, generatedAt });
|
|
201
|
-
}
|
|
202
|
-
const { spec: served, ourReasons } = generation === 'cohort'
|
|
203
|
-
? fitCohortSpec(answer.spec, input, digest, rank)
|
|
204
|
-
: { spec: answer.spec, ourReasons: new Map() };
|
|
205
|
-
const reconciled = reconcileSpec(served, input, digest, ourReasons);
|
|
206
|
-
if (!reconciled.isUsable) {
|
|
207
|
-
return buildDeterministic(input, digest, startedAt, key, 'unusable-on-serve', {
|
|
208
|
-
calledModel,
|
|
209
|
-
cache: read.outcome,
|
|
210
|
-
violations: reconciled.violations,
|
|
211
|
-
...(answer.usage ? { usage: answer.usage } : {}),
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
const finishedAt = Date.now();
|
|
215
|
-
const source = cached ? 'cache' : 'llm';
|
|
216
|
-
report({
|
|
217
|
-
key,
|
|
218
|
-
source,
|
|
219
|
-
elapsedMs: finishedAt - startedAt,
|
|
220
|
-
calledModel,
|
|
221
|
-
cache: read.outcome,
|
|
222
|
-
violations: reconciled.violations,
|
|
223
|
-
...(answer.usage ? { usage: answer.usage } : {}),
|
|
224
|
-
});
|
|
225
|
-
return {
|
|
226
|
-
...reconciled.spec,
|
|
227
|
-
specVersion: SPEC_VERSION,
|
|
228
|
-
slot: digest.slot,
|
|
229
|
-
source,
|
|
230
|
-
generatedAt,
|
|
231
|
-
latencyMs: finishedAt - startedAt,
|
|
232
|
-
provider: provider.name,
|
|
233
|
-
model: provider.model,
|
|
234
|
-
};
|
|
235
|
-
},
|
|
208
|
+
calledModel: trace.calledModel,
|
|
209
|
+
cache: trace.cache,
|
|
210
|
+
...(violations ? { violations } : {}),
|
|
211
|
+
...(trace.usage ? { usage: trace.usage } : {}),
|
|
212
|
+
...('error' in trace ? { error: trace.error } : {}),
|
|
236
213
|
};
|
|
237
214
|
}
|
|
215
|
+
function report(settings, event) {
|
|
216
|
+
if (!settings.options.onEvent)
|
|
217
|
+
return;
|
|
218
|
+
try {
|
|
219
|
+
settings.options.onEvent(event);
|
|
220
|
+
}
|
|
221
|
+
catch { }
|
|
222
|
+
}
|
|
238
223
|
//# sourceMappingURL=component-generator.js.map
|