@rudra-js/core 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +139 -15
- package/dist/claim-screening.d.ts +33 -0
- package/dist/claim-screening.d.ts.map +1 -0
- package/dist/claim-screening.js +226 -0
- package/dist/claim-screening.js.map +1 -0
- package/dist/component-generator.d.ts.map +1 -1
- package/dist/component-generator.js +33 -98
- package/dist/component-generator.js.map +1 -1
- package/dist/component-spec.d.ts +2 -49
- package/dist/component-spec.d.ts.map +1 -1
- package/dist/component-spec.js +3 -17
- package/dist/component-spec.js.map +1 -1
- package/dist/fallback-component.d.ts +3 -0
- package/dist/fallback-component.d.ts.map +1 -1
- package/dist/fallback-component.js +3 -13
- package/dist/fallback-component.js.map +1 -1
- package/dist/fit-to-shopper.d.ts +5 -1
- package/dist/fit-to-shopper.d.ts.map +1 -1
- package/dist/fit-to-shopper.js +6 -4
- package/dist/fit-to-shopper.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/model-prompt.d.ts +13 -1
- package/dist/model-prompt.d.ts.map +1 -1
- package/dist/model-prompt.js +53 -21
- package/dist/model-prompt.js.map +1 -1
- package/dist/product-selection.d.ts +1 -9
- package/dist/product-selection.d.ts.map +1 -1
- package/dist/product-selection.js +1 -4
- package/dist/product-selection.js.map +1 -1
- package/dist/provider.d.ts +1 -8
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +1 -8
- package/dist/provider.js.map +1 -1
- package/dist/reconciliation.d.ts +15 -12
- package/dist/reconciliation.d.ts.map +1 -1
- package/dist/reconciliation.js +150 -340
- package/dist/reconciliation.js.map +1 -1
- package/dist/signal-digest.d.ts +10 -0
- package/dist/signal-digest.d.ts.map +1 -1
- package/dist/signal-digest.js +38 -28
- package/dist/signal-digest.js.map +1 -1
- package/dist/spec-cache.d.ts +14 -2
- package/dist/spec-cache.d.ts.map +1 -1
- package/dist/spec-cache.js +14 -24
- package/dist/spec-cache.js.map +1 -1
- package/dist/tracking-input.d.ts.map +1 -1
- package/dist/tracking-input.js +15 -13
- package/dist/tracking-input.js.map +1 -1
- package/package.json +4 -2
- package/src/claim-screening.ts +242 -0
- package/src/component-generator.ts +38 -123
- package/src/component-spec.ts +5 -18
- package/src/fallback-component.ts +3 -14
- package/src/fit-to-shopper.ts +11 -4
- package/src/index.ts +1 -0
- package/src/model-prompt.ts +58 -23
- package/src/product-selection.ts +2 -13
- package/src/provider.ts +1 -8
- package/src/reconciliation.ts +178 -359
- package/src/signal-digest.ts +41 -31
- package/src/spec-cache.ts +27 -24
- package/src/tracking-input.ts +16 -13
package/README.md
CHANGED
|
@@ -6,16 +6,28 @@ component specification.
|
|
|
6
6
|
## Install
|
|
7
7
|
|
|
8
8
|
```sh
|
|
9
|
-
npm install @rudra-js/core zod@^4
|
|
9
|
+
npm install @rudra-js/core @rudra-js/attested zod@^4
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
Two peer dependencies, for two different reasons.
|
|
13
|
+
|
|
14
|
+
`zod` is a peer because the public API of this package _is_ zod schemas, so your
|
|
15
|
+
app and the package have to resolve the same copy of zod. **zod 4.5 or later is
|
|
14
16
|
required**, which is what the peer range asks for. The schemas use zod 4 APIs,
|
|
15
17
|
and installing into a zod 3 app fails with `ERESOLVE`, which isn't the most
|
|
16
18
|
helpful error you'll ever read. The floor is 4.5 rather than 4.0 because 4.5
|
|
17
19
|
changed how a nullable field is written into the tool schema we send the model.
|
|
18
20
|
|
|
21
|
+
`@rudra-js/attested` is a peer for the opposite reason: none of its types cross
|
|
22
|
+
this package's public surface, and you never have to import it. It's a peer so
|
|
23
|
+
there is exactly one denylist in your tree. As a real dependency, an app that
|
|
24
|
+
pins its own copy gets two — we checked, and npm 10.9.4 installs the app's
|
|
25
|
+
version at the top and quietly nests ours under `@rudra-js/core` — and the two
|
|
26
|
+
would then disagree about what counts as a claim. As a peer that same pin is an
|
|
27
|
+
`ERESOLVE` you can see and fix. Pin nothing and the tree is identical either
|
|
28
|
+
way: one copy, at the top. One tag publishes every `@rudra-js` package at one
|
|
29
|
+
version, so the ranges here always move together.
|
|
30
|
+
|
|
19
31
|
## Running without a model
|
|
20
32
|
|
|
21
33
|
`createComponentGenerator` takes a `provider`. Leave it out, or pass `null`, and
|
|
@@ -108,10 +120,23 @@ before anything renders. A product you left out doesn't reach the page.
|
|
|
108
120
|
`bundles` is optional. These are the sets you sell together, each with your own
|
|
109
121
|
price for the set, the currency that price is in, and, if you want one, your own
|
|
110
122
|
name for it. Every product in a set has to be a candidate as well. That's what
|
|
111
|
-
lets the
|
|
123
|
+
lets the checks that pass a single product run over a whole set, and what lets
|
|
112
124
|
the renderer look the members up in the catalog it already has. Ids must be
|
|
113
125
|
unique, and one set must not name the same product twice.
|
|
114
126
|
|
|
127
|
+
A set is placed whole, so one check runs differently: a set may hold something
|
|
128
|
+
the shopper already bought or has in the basket, where a grid or a carousel
|
|
129
|
+
would drop it. A thumbs-down on a member still blocks the whole set, and so does
|
|
130
|
+
a member that is out of stock, already on the page, or the product being looked
|
|
131
|
+
at right now.
|
|
132
|
+
|
|
133
|
+
That last one has a cost. A "frequently bought together, including this item"
|
|
134
|
+
set names the product whose page it sits on, so it is refused on exactly the
|
|
135
|
+
page you wrote it for. Leave the item out and sell the companions as the set —
|
|
136
|
+
that rejection goes away, and the rest of the checks still apply. And when a
|
|
137
|
+
bundle block is the only block the model asked for, refusing the set leaves
|
|
138
|
+
nothing to render, so the deterministic component takes over.
|
|
139
|
+
|
|
115
140
|
The model _never_ picks a set and is never told a price. All it does is ask for a
|
|
116
141
|
bundle block and write the words around it. We pick which set when the page is
|
|
117
142
|
served, going on what the shopper has in their basket, has looked at, or is
|
|
@@ -122,15 +147,75 @@ hero, a banner, a block title, the copy block, the reason under a product, and
|
|
|
122
147
|
the words around the set. Text you supplied is never read this way. A product
|
|
123
148
|
title, a category and a bundle `label` are your words, not the model's.
|
|
124
149
|
|
|
125
|
-
We drop text that makes a claim we can't check. The
|
|
126
|
-
customer score, a delivery date and a count of what's left
|
|
127
|
-
|
|
128
|
-
|
|
150
|
+
We drop text that makes a claim we can't check, in three passes. The first looks
|
|
151
|
+
for money, a customer score, a delivery date and a count of what's left. The
|
|
152
|
+
second asks whether every numeral in the sentence is one you supplied. The third
|
|
153
|
+
is `@rudra-js/attested`'s phrase list, for claims with no number in them to
|
|
154
|
+
check — "top pick", "customer favourite".
|
|
155
|
+
|
|
156
|
+
That second pass changed what happens to a specification. "a comfort rating of
|
|
157
|
+
-5C" used to be kept on the strength of the words around the number. It is kept
|
|
158
|
+
now only when a `5` turns up in a `tag` or a `category` name you sent us — the
|
|
159
|
+
number, not the string, so a tag reading `5-pocket` keeps it as surely as one
|
|
160
|
+
reading `-5C comfort`. Those are the strings we hand the model and let it repeat.
|
|
161
|
+
A `title` and a `rating` we also show it, and the prompt tells it never to
|
|
162
|
+
restate either, so neither stands behind a number. Put your spec sheet in `tags`
|
|
163
|
+
and the model can quote it; leave it out and a number in that field is one the
|
|
164
|
+
model made up, and it goes.
|
|
165
|
+
|
|
166
|
+
Which strings, exactly:
|
|
167
|
+
|
|
168
|
+
- Only candidates we showed the model. Out of stock, or past the 60 we send,
|
|
169
|
+
means a product the model never saw, and its tags stand behind nothing.
|
|
170
|
+
- The `currentCategory` on the request is not one of them. It's a string from
|
|
171
|
+
this request rather than a row of your catalog, and plenty of sites pass a URL
|
|
172
|
+
segment straight into it.
|
|
173
|
+
- A `reason` or a `badge` sits under a named product, so it's read against that
|
|
174
|
+
product's own tags and category. Every other field reads all the candidates'
|
|
175
|
+
pooled.
|
|
176
|
+
- A tag or category that is nothing but a number in exponent notation, and whose
|
|
177
|
+
exponent runs past a thousand, is dropped here. `1e2000000000` is twelve
|
|
178
|
+
characters that lay out into a run long enough to end the process, and
|
|
179
|
+
`@rudra-js/attested` is a peer dependency, so the copy you have installed may
|
|
180
|
+
be one that still tries. `@rudra-js/attested` draws its own line in the same
|
|
181
|
+
place but measures the laid-out run rather than the exponent, so a handful of
|
|
182
|
+
tags near the margin — `1e1000`, `1.5e1000`, `9999e998` — get past this rule
|
|
183
|
+
and then stand behind nothing on the other side of it. Nothing that wide is a
|
|
184
|
+
product fact either way.
|
|
185
|
+
|
|
186
|
+
Two caveats worth saying out loud, because they're the shape of the check rather
|
|
187
|
+
than bugs in it. Pooled means pooled: one product's `40 litre` tag stands behind
|
|
188
|
+
"take 40 off" written in a headline about another. And nothing in the pass knows
|
|
189
|
+
which quantity a tag was about, so a tag holding a weight stands behind a price
|
|
190
|
+
with the same digits in it. It proves the digits came from you. It doesn't prove
|
|
191
|
+
the sentence is true.
|
|
192
|
+
|
|
193
|
+
The first and third passes are word lists, so spotting a claim isn't a
|
|
194
|
+
guarantee, not the way checking a price against your catalog is. The second is a
|
|
195
|
+
proof of something narrower than it sounds: every digit was one of yours. A
|
|
196
|
+
number written as a word isn't a number to it, so "four and a half stars" is not
|
|
197
|
+
a rating it can see — while a ½ or the K in "10K" is a numeral it can't read,
|
|
198
|
+
which it drops rather than waves through.
|
|
199
|
+
|
|
200
|
+
The model isn't told any of this. The prompt bans prices, ratings, discounts,
|
|
201
|
+
delivery dates and stock levels, and says nothing about digits, so it can't tell
|
|
202
|
+
`3-season` — a tag you sent, and fine — from `2 litres`, which is not. Whatever
|
|
203
|
+
the over-rejection rate is on your catalog, nothing in the prompt is steering it
|
|
204
|
+
down yet.
|
|
129
205
|
|
|
130
206
|
Some fields can't be empty, like a headline or a banner's text. Those get emptied
|
|
131
207
|
instead of nulled, so the block drops the way any block with no text drops. And
|
|
132
208
|
an emptied page headline makes the whole generation unusable.
|
|
133
209
|
|
|
210
|
+
That last one is the expensive edge of the digit check, so it's worth being
|
|
211
|
+
concrete: if the model writes "Our 3 favourites for wet weather" and no candidate
|
|
212
|
+
carries a 3, the headline empties, the generation is unusable, and the
|
|
213
|
+
deterministic component renders — a model call paid for and thrown away. In
|
|
214
|
+
cohort mode the spec is cached before reconciliation, so every cache hit for the
|
|
215
|
+
rest of the TTL runs the same screen and reaches the same fallback. The reason a
|
|
216
|
+
`reason` under a card is exempt is the same idea from the other side: in cohort
|
|
217
|
+
mode we wrote that sentence, so reading it back would only ever cost us.
|
|
218
|
+
|
|
134
219
|
For the set, the prompt also tells the model to write about the offer rather than
|
|
135
220
|
the products in it, and never to say the set saves money or by how much. Pass a
|
|
136
221
|
`label` on the bundle to put your own words on the set. A label is text you
|
|
@@ -161,9 +246,10 @@ list, and not an `Accept-Language` header.
|
|
|
161
246
|
By default one generated component is shared between shoppers who look alike, and
|
|
162
247
|
each shopper's own products are filled in per request. A cohort is the shopper's
|
|
163
248
|
segment, the surface and slot, the locale, the item count, whether they're a
|
|
164
|
-
first-time visitor,
|
|
165
|
-
person an individual stays out of it: who they
|
|
166
|
-
searched for. That's what lets many page views
|
|
249
|
+
first-time visitor, the category being browsed, and the category they lean
|
|
250
|
+
towards. Everything that makes a person an individual stays out of it: who they
|
|
251
|
+
are, what they liked, viewed or searched for. That's what lets many page views
|
|
252
|
+
reuse one call.
|
|
167
253
|
|
|
168
254
|
The candidate list is part of the cohort too, since the model is shown those
|
|
169
255
|
products and writes about them. In most shops candidates come from the page, so
|
|
@@ -171,6 +257,30 @@ everyone looking at it shares them. If you pick candidates per shopper you'll ge
|
|
|
171
257
|
smaller cohorts. That's the honest outcome, because your prompt really is
|
|
172
258
|
personal.
|
|
173
259
|
|
|
260
|
+
What goes in the key is the _set_ of candidate SKUs. Not the order you sent them
|
|
261
|
+
in, not which ones were in stock, and not where the 60-product cut fell. So two
|
|
262
|
+
requests carrying the same SKUs in a different order share one component, and the
|
|
263
|
+
one generated first is the one both get. If your ranking is personal, order your
|
|
264
|
+
candidates and the products in the copy won't be the ones you put at the top.
|
|
265
|
+
Prices, titles and stock are always read from your catalog at render time, so
|
|
266
|
+
nothing stale is ever shown — a product that sold out is dropped as the page is
|
|
267
|
+
served, and any sentence written about it stays.
|
|
268
|
+
|
|
269
|
+
#### Who can create a cohort
|
|
270
|
+
|
|
271
|
+
Every field above is one you supply, and each distinct value is a cohort of its
|
|
272
|
+
own — a fresh model call, and a slot in the cache. If a visitor can choose one of
|
|
273
|
+
them, a visitor can choose how many calls you pay for. A `locale` copied straight
|
|
274
|
+
out of `Accept-Language`, a `currentCategory` read off a URL slug and a `segment`
|
|
275
|
+
built from a query parameter are the three that go wrong. The shipped cache holds
|
|
276
|
+
10,000 entries and drops the oldest, so enough made-up values also evict the
|
|
277
|
+
cohorts your real shoppers were being served from.
|
|
278
|
+
|
|
279
|
+
Draw those fields from sets you control: your own locale list, your own category
|
|
280
|
+
ids, your own segment labels. Nothing leaks either way — a cohort prompt carries
|
|
281
|
+
no shopper text at all — the cost is the bill and the model round trip on a page
|
|
282
|
+
that was being served from cache.
|
|
283
|
+
|
|
174
284
|
Pass `generation: 'per-shopper'` to generate for the individual instead. The
|
|
175
285
|
model then chooses the products too, and every shopper pays for their own call.
|
|
176
286
|
|
|
@@ -238,9 +348,19 @@ const generator = createComponentGenerator({ provider, rank: 'given' });
|
|
|
238
348
|
|
|
239
349
|
You keep the rest either way. We still drop anything the shopper shouldn't be
|
|
240
350
|
shown, whether it's out of stock, already bought, in the basket, disliked, or
|
|
241
|
-
the product they're looking at right now
|
|
242
|
-
|
|
243
|
-
|
|
351
|
+
the product they're looking at right now — read from the payload you sent, not
|
|
352
|
+
from the shortened history the model was shown, so a shopper with a long order
|
|
353
|
+
book is covered too. Every product still carries a basis we check against their
|
|
354
|
+
real signals, and everything the model writes is still screened.
|
|
355
|
+
|
|
356
|
+
How much each basis proves is worth knowing, because two of the six prove less
|
|
357
|
+
than they read. `most_viewed` is checked against the SKUs they viewed,
|
|
358
|
+
`similar_to_current` against the category being browsed, `liked_category`
|
|
359
|
+
against the categories they bought, liked, carted or viewed in — standing on a
|
|
360
|
+
category page is not one of those — and `popular` claims nothing.
|
|
361
|
+
`complements_cart` and `complements_purchase` check only that the basket, or the
|
|
362
|
+
order history, isn't empty: nothing here can know that one product goes with
|
|
363
|
+
another. A basis that fails takes the sentence and the badge stating it with it.
|
|
244
364
|
|
|
245
365
|
Each candidate can carry its own `reason`, the phrase shown under the product.
|
|
246
366
|
Reach for it when your ranking knows something the signals don't:
|
|
@@ -264,7 +384,11 @@ reasons itself, so every one of them is screened, including one that happens to
|
|
|
264
384
|
read the same as yours.
|
|
265
385
|
|
|
266
386
|
Without a reason of your own, the basis is stated for you from the shopper's
|
|
267
|
-
signals.
|
|
387
|
+
signals — "More in Backpacks", "Goes with what is in your cart". Those are our
|
|
388
|
+
words, not the model's, so in `cohort` mode they aren't screened either. They
|
|
389
|
+
used to be, and a shop with a category called Clearance or Last Chance would
|
|
390
|
+
have watched the phrase list delete the reason under every card while the
|
|
391
|
+
deterministic component printed the same sentence untouched.
|
|
268
392
|
|
|
269
393
|
## What the model sees
|
|
270
394
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Product, TrackingInput } from './tracking-input.js';
|
|
2
|
+
/**
|
|
3
|
+
* Wording attested bans that this project has already ruled is not a claim. An allowance
|
|
4
|
+
* forgives a banned phrase sitting inside it, so an entry ending on a preposition — "extra
|
|
5
|
+
* clearance for" — forgives whatever follows it. Exported so a test can hold the list
|
|
6
|
+
* against attested's own.
|
|
7
|
+
*/
|
|
8
|
+
export declare const ALLOWED_PHRASES: string[];
|
|
9
|
+
/**
|
|
10
|
+
* The numbers the shop published about one product: its category and tags, only the strings
|
|
11
|
+
* carrying a digit. Exported so a test can pin what never reaches attested.
|
|
12
|
+
*/
|
|
13
|
+
export declare function productFacts(product: Product): string[];
|
|
14
|
+
export interface HostFacts {
|
|
15
|
+
pooled: string[];
|
|
16
|
+
bySku: Map<string, string[]>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The shop's own numbers for this request, for attested to check numerals against.
|
|
20
|
+
*
|
|
21
|
+
* `offeredCandidates` rather than `input.candidates` — out of stock or past the prompt's cap
|
|
22
|
+
* means a product the model never saw. A title and a rating it is shown but told never to
|
|
23
|
+
* repeat, so a numeral out of one is one it was told not to write. The browsed category comes
|
|
24
|
+
* from the request rather than the catalogue, so a host passing a URL segment through would
|
|
25
|
+
* hand this list to whoever types the URL.
|
|
26
|
+
*
|
|
27
|
+
* `pooled` is the soft part of the check: one candidate's "40 litre" backs "take 40 off"
|
|
28
|
+
* written about another. A `reason` and a `badge` read `bySku`, because they sit under a
|
|
29
|
+
* named product — off the pooled list, a badge reading "Only 2" stood on a tent's tag.
|
|
30
|
+
*/
|
|
31
|
+
export declare function hostFacts(input: TrackingInput): HostFacts;
|
|
32
|
+
export declare function claimIn(text: string): string | null;
|
|
33
|
+
//# sourceMappingURL=claim-screening.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claim-screening.d.ts","sourceRoot":"","sources":["../src/claim-screening.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAmKlE;;;;;GAKG;AACH,eAAO,MAAM,eAAe,UAAkE,CAAC;AAmB/F;;;GAGG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,EAAE,CAOvD;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CAC9B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,aAAa,GAAG,SAAS,CAWzD;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAQnD"}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { offeredCandidates } from './model-prompt.js';
|
|
2
|
+
const CLAIM_PATTERNS = [
|
|
3
|
+
{
|
|
4
|
+
// Customers scoring the product. "rated for winter use", "rated to -10C"
|
|
5
|
+
// and "an IPX7 water rating" are about what the product can take.
|
|
6
|
+
kind: 'rating',
|
|
7
|
+
patterns: [
|
|
8
|
+
/\breviews?\b|\breviewed\b/,
|
|
9
|
+
/\b(?:\d+(?:\.\d+)?|three|four|five)[\s-]?stars?\b/,
|
|
10
|
+
/\bstars?[\s-]?ratings?\b/,
|
|
11
|
+
// A rating somebody gave it, rather than one it was built to.
|
|
12
|
+
/\b(?:customer|shopper|buyer|user|average|overall)[\s-]ratings?\b/,
|
|
13
|
+
// A score out of five is written as a decimal. A spec is "-5C" or "20,000mm".
|
|
14
|
+
/\bratings?\s+of\s+[0-5]\.\d\b/,
|
|
15
|
+
// The same score with the word "rating" nowhere near it: "4.8 out of 5".
|
|
16
|
+
/\b[0-5](?:\.\d)?\s+out of\s+(?:5|five)\b/,
|
|
17
|
+
/\b(?:highly|top|best|well|poorly|five|four)[\s-]rated\b/,
|
|
18
|
+
// "rated 4.8" is a score. "rated 3 season" is what the tent is built for.
|
|
19
|
+
/\brated\s+(?:[0-5]\.\d|(?:[0-5]|three|four|five)\s+(?:stars?|out of))\b/,
|
|
20
|
+
// How many other people bought or liked it is a customer claim too.
|
|
21
|
+
/\bbest[\s-]?sell(?:er|ers|ing)\b/,
|
|
22
|
+
// The words a model reaches for when "best" is banned. Each branch carries its own
|
|
23
|
+
// \b — one in front of the group demands a word character before the #, never there.
|
|
24
|
+
/(?:\bnumber one|\bno\.? ?1|#\s?1)[\s-]?sell(?:er|ers|ing)\b/,
|
|
25
|
+
/\bloved by (?:thousands|hundreds|millions|\d)/,
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
kind: 'price',
|
|
30
|
+
patterns: [
|
|
31
|
+
// No digit beside it: asking for one let "$thirty-nine and it is yours" through.
|
|
32
|
+
/[$£€¥₹]/,
|
|
33
|
+
/\b\d+(?:\.\d+)?\s?(?:usd|eur|gbp|dollars?|pounds?|euros?)\b/,
|
|
34
|
+
// The same spelled out. "pounds" is left off on purpose — a pack weighs two of those.
|
|
35
|
+
/\bdollars?\b|\beuros?\b/,
|
|
36
|
+
// The same codes on the other side of the number: "USD 20", "EUR 5.99".
|
|
37
|
+
/\b(?:usd|eur|gbp)\s?\d/,
|
|
38
|
+
// The krona is spelled out rather than drawn, so it needs a number beside it.
|
|
39
|
+
/\bkr\s?\d|\d\s?kr\b/,
|
|
40
|
+
/\bpric(?:e|es|ed|ing)\b/,
|
|
41
|
+
// A before-and-after is a price claim even with no currency on it.
|
|
42
|
+
/\bwas\s+[$£€¥]?\s?\d[\d,.]*\s*[,;–—-]?\s*now\s+[$£€¥]?\s?\d/,
|
|
43
|
+
// "does not feel cheap" is about quality. Only the comparison is money.
|
|
44
|
+
/\bcheap(?:er|est)\b/,
|
|
45
|
+
/\baffordable\b|\bbargain\b/,
|
|
46
|
+
// "at no cost to comfort" is not money. "costs less" is.
|
|
47
|
+
/\bcosts?\s+(?:less|more|only|just|about|around|[$£€¥]?\d)/,
|
|
48
|
+
/\blow(?:er)?[\s-]cost\b/,
|
|
49
|
+
// "saves weight" and "saves on weight" are both about grams.
|
|
50
|
+
/\bsaves?\s+(?:you\s+)?(?:money|cash|[$£€¥]\s?\d)/,
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
// A percentage in outdoor copy is a specification far more often than it is
|
|
55
|
+
// money off, so it only counts beside a money word: "20% off", not
|
|
56
|
+
// "100% recycled".
|
|
57
|
+
kind: 'discount',
|
|
58
|
+
patterns: [
|
|
59
|
+
/\d+(?:\.\d+)?\s?(?:%|percent)\s*(?:off\b|discount|reduction|less\b)/,
|
|
60
|
+
/\b(?:save|saving|savings|off|discount|extra|up to)\s+(?:up to\s+)?\d+(?:\.\d+)?\s?(?:%|percent)/,
|
|
61
|
+
/\bdiscount(?:s|ed)?\b/,
|
|
62
|
+
/\bsale\b|\bmarked down\b|\bdeal of\b/,
|
|
63
|
+
/\bhalf[\s-]?(?:price|off)\b/,
|
|
64
|
+
// Money off with no percent sign anywhere on it: "save 20 off".
|
|
65
|
+
/\bsaves?\s+\d[\d.,]*\s+off\b/,
|
|
66
|
+
// "extra clearance for thick socks" is room inside the shoe.
|
|
67
|
+
/\bclearance\s+(?:sale|price|event|deal)\b|\bon clearance\b/,
|
|
68
|
+
// "reduced weight" and "reduced to 900g" are specifications. Only a
|
|
69
|
+
// reduced price, or a reduction with a date on it, is money off.
|
|
70
|
+
/\bprice reduced\b|\breduced price\b|\breduced by \d+\s?(?:%|percent)|\breduced\s+(?:this|next|last)\s+week\b/,
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
// When it turns up, not what turns up. "ships in a recycled box" and
|
|
75
|
+
// "arrives flat-packed" describe the thing, so the verb alone is not enough
|
|
76
|
+
// — it needs a day or a date beside it.
|
|
77
|
+
kind: 'delivery',
|
|
78
|
+
patterns: [
|
|
79
|
+
/\bdelivery\b|\bshipping\b/,
|
|
80
|
+
/\bnext[\s-]day\b|\bsame[\s-]day\b|\bovernight\b/,
|
|
81
|
+
/\b(?:ships?|arrives?|arriving|delivered|by|before|in time for)\s+(?:on\s+)?(?:today|tomorrow|tonight|this week|next week|the weekend|monday|tuesday|wednesday|thursday|friday|saturday|sunday)\b/,
|
|
82
|
+
/\b(?:ships?|arrives?|arriving)\s+(?:in|within)\s+\d/,
|
|
83
|
+
/\bin time for\b/,
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
kind: 'stock',
|
|
88
|
+
patterns: [
|
|
89
|
+
/\b(?:in|out of|low on) stock\b/,
|
|
90
|
+
/\blimited stock\b/,
|
|
91
|
+
/\brestocked?\b|\bsold out\b/,
|
|
92
|
+
// "the last few miles" is a distance, so a count needs "left" after it.
|
|
93
|
+
/\b(?:only\s+)?(?:\d+|a few|a handful|a couple|one|few)\s+(?:left|remain(?:s|ing)?)\b/,
|
|
94
|
+
/\bselling fast\b|\b(?:almost|nearly) gone\b|\bwhile stocks last\b/,
|
|
95
|
+
],
|
|
96
|
+
},
|
|
97
|
+
];
|
|
98
|
+
/** Letters that render as a Latin letter but are not one — a Cyrillic о in "in stоck". */
|
|
99
|
+
const CONFUSABLES = {
|
|
100
|
+
а: 'a',
|
|
101
|
+
в: 'b',
|
|
102
|
+
е: 'e',
|
|
103
|
+
к: 'k',
|
|
104
|
+
м: 'm',
|
|
105
|
+
н: 'h',
|
|
106
|
+
о: 'o',
|
|
107
|
+
р: 'p',
|
|
108
|
+
с: 'c',
|
|
109
|
+
т: 't',
|
|
110
|
+
х: 'x',
|
|
111
|
+
у: 'y',
|
|
112
|
+
ѕ: 's',
|
|
113
|
+
і: 'i',
|
|
114
|
+
ј: 'j',
|
|
115
|
+
ԁ: 'd',
|
|
116
|
+
һ: 'h',
|
|
117
|
+
ӏ: 'l',
|
|
118
|
+
α: 'a',
|
|
119
|
+
ε: 'e',
|
|
120
|
+
ι: 'i',
|
|
121
|
+
κ: 'k',
|
|
122
|
+
ν: 'v',
|
|
123
|
+
ο: 'o',
|
|
124
|
+
ρ: 'p',
|
|
125
|
+
τ: 't',
|
|
126
|
+
υ: 'u',
|
|
127
|
+
χ: 'x',
|
|
128
|
+
ϲ: 'c',
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* Characters that render as nothing: format characters, default-ignorable code points and the
|
|
132
|
+
* controls. Cf and Default_Ignorable each hold characters the other does not.
|
|
133
|
+
* A second copy of the class in attested's `hidden.ts`, pinned to it by a test.
|
|
134
|
+
*/
|
|
135
|
+
const INVISIBLE = /[\p{Cf}\p{Default_Ignorable_Code_Point}\p{Cc}]/gu;
|
|
136
|
+
/** The controls that do take room. Deleting these would read `Only 2\n3 left` as 23. */
|
|
137
|
+
const SPACING = /[\t\n\v\f\r\u0085]/;
|
|
138
|
+
/**
|
|
139
|
+
* Marks that hang on the character before them instead of taking a column of their own.
|
|
140
|
+
* Dropped after composition, so an accented e keeps its accent while an overline,
|
|
141
|
+
* which composes with nothing, cannot hide a word from its own rule.
|
|
142
|
+
*/
|
|
143
|
+
const MARKS = /[\p{Mn}\p{Me}]/gu;
|
|
144
|
+
/** Only the reading is normalised. What renders is what the model wrote. */
|
|
145
|
+
function normaliseForClaims(text) {
|
|
146
|
+
const lowered = text
|
|
147
|
+
.replace(INVISIBLE, (char) => (SPACING.test(char) ? char : ''))
|
|
148
|
+
.normalize('NFKC')
|
|
149
|
+
.toLowerCase()
|
|
150
|
+
.normalize('NFC')
|
|
151
|
+
.replace(MARKS, '');
|
|
152
|
+
let folded = '';
|
|
153
|
+
for (const char of lowered)
|
|
154
|
+
folded += CONFUSABLES[char] ?? char;
|
|
155
|
+
return folded;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Wording attested bans that this project has already ruled is not a claim. An allowance
|
|
159
|
+
* forgives a banned phrase sitting inside it, so an entry ending on a preposition — "extra
|
|
160
|
+
* clearance for" — forgives whatever follows it. Exported so a test can hold the list
|
|
161
|
+
* against attested's own.
|
|
162
|
+
*/
|
|
163
|
+
export const ALLOWED_PHRASES = ['does not feel cheap', "doesn't feel cheap", 'last few miles'];
|
|
164
|
+
/** Only a string with one of these in it can stand behind a numeral. */
|
|
165
|
+
const DIGIT = /\p{Nd}/u;
|
|
166
|
+
/** A fact whose whole content is a number in exponent notation. attested lays those out in place. */
|
|
167
|
+
const BARE_EXPONENT = /^[+-]?\d+(?:\.\d+)?[eE]([+-]?\d+)$/;
|
|
168
|
+
/** Wide enough for every finite JavaScript number, and for anything a shop sells. */
|
|
169
|
+
const MAX_EXPONENT = 1000;
|
|
170
|
+
// `1e2000000000` is twelve characters and lays out into a run long enough to end the process.
|
|
171
|
+
// attested caps that itself, but it is a peer dependency and a host's copy of it may be older.
|
|
172
|
+
function canLayOut(fact) {
|
|
173
|
+
const match = BARE_EXPONENT.exec(fact.trim());
|
|
174
|
+
if (match === null)
|
|
175
|
+
return true;
|
|
176
|
+
return Math.abs(Number(match[1] ?? '')) <= MAX_EXPONENT;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* The numbers the shop published about one product: its category and tags, only the strings
|
|
180
|
+
* carrying a digit. Exported so a test can pin what never reaches attested.
|
|
181
|
+
*/
|
|
182
|
+
export function productFacts(product) {
|
|
183
|
+
const facts = [];
|
|
184
|
+
if (DIGIT.test(product.category) && canLayOut(product.category))
|
|
185
|
+
facts.push(product.category);
|
|
186
|
+
for (const tag of product.tags) {
|
|
187
|
+
if (DIGIT.test(tag) && canLayOut(tag))
|
|
188
|
+
facts.push(tag);
|
|
189
|
+
}
|
|
190
|
+
return facts;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* The shop's own numbers for this request, for attested to check numerals against.
|
|
194
|
+
*
|
|
195
|
+
* `offeredCandidates` rather than `input.candidates` — out of stock or past the prompt's cap
|
|
196
|
+
* means a product the model never saw. A title and a rating it is shown but told never to
|
|
197
|
+
* repeat, so a numeral out of one is one it was told not to write. The browsed category comes
|
|
198
|
+
* from the request rather than the catalogue, so a host passing a URL segment through would
|
|
199
|
+
* hand this list to whoever types the URL.
|
|
200
|
+
*
|
|
201
|
+
* `pooled` is the soft part of the check: one candidate's "40 litre" backs "take 40 off"
|
|
202
|
+
* written about another. A `reason` and a `badge` read `bySku`, because they sit under a
|
|
203
|
+
* named product — off the pooled list, a badge reading "Only 2" stood on a tent's tag.
|
|
204
|
+
*/
|
|
205
|
+
export function hostFacts(input) {
|
|
206
|
+
const pooled = new Set();
|
|
207
|
+
const bySku = new Map();
|
|
208
|
+
for (const product of offeredCandidates(input)) {
|
|
209
|
+
const own = productFacts(product);
|
|
210
|
+
bySku.set(product.sku, own);
|
|
211
|
+
for (const fact of own)
|
|
212
|
+
pooled.add(fact);
|
|
213
|
+
}
|
|
214
|
+
return { pooled: [...pooled], bySku };
|
|
215
|
+
}
|
|
216
|
+
export function claimIn(text) {
|
|
217
|
+
const normalised = normaliseForClaims(text);
|
|
218
|
+
for (const claim of CLAIM_PATTERNS) {
|
|
219
|
+
for (const pattern of claim.patterns) {
|
|
220
|
+
if (pattern.test(normalised))
|
|
221
|
+
return claim.kind;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
//# sourceMappingURL=claim-screening.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claim-screening.js","sourceRoot":"","sources":["../src/claim-screening.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"component-generator.d.ts","sourceRoot":"","sources":["../src/component-generator.ts"],"names":[],"mappings":"AACA,OAAO,EAIL,KAAK,aAAa,EAClB,KAAK,cAAc,EAEnB,KAAK,UAAU,EAChB,MAAM,qBAAqB,CAAC;AAG7B,OAAO,KAAK,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"component-generator.d.ts","sourceRoot":"","sources":["../src/component-generator.ts"],"names":[],"mappings":"AACA,OAAO,EAIL,KAAK,aAAa,EAClB,KAAK,cAAc,EAEnB,KAAK,UAAU,EAChB,MAAM,qBAAqB,CAAC;AAG7B,OAAO,KAAK,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEnE,OAAO,EAAkB,KAAK,SAAS,EAAoB,MAAM,wBAAwB,CAAC;AAG1F,OAAO,EAKL,KAAK,SAAS,EACf,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAGL,KAAK,kBAAkB,EACxB,MAAM,qBAAqB,CAAC;AAE7B;;;;;;;;;;;;;;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;IAChC,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC;CAC9C;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;IACxB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,QAAQ,GAAG,aAAa,CAAC;IACtC;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,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;AAED,qBAAa,YAAa,SAAQ,KAAK;IACrC,YAAY,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAG9C;CACF;AA+HD,wBAAgB,wBAAwB,CACtC,OAAO,GAAE,yBAA8B,GACtC,kBAAkB,CA8MpB"}
|