@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.
Files changed (64) hide show
  1. package/README.md +139 -15
  2. package/dist/claim-screening.d.ts +33 -0
  3. package/dist/claim-screening.d.ts.map +1 -0
  4. package/dist/claim-screening.js +226 -0
  5. package/dist/claim-screening.js.map +1 -0
  6. package/dist/component-generator.d.ts.map +1 -1
  7. package/dist/component-generator.js +33 -98
  8. package/dist/component-generator.js.map +1 -1
  9. package/dist/component-spec.d.ts +2 -49
  10. package/dist/component-spec.d.ts.map +1 -1
  11. package/dist/component-spec.js +3 -17
  12. package/dist/component-spec.js.map +1 -1
  13. package/dist/fallback-component.d.ts +3 -0
  14. package/dist/fallback-component.d.ts.map +1 -1
  15. package/dist/fallback-component.js +3 -13
  16. package/dist/fallback-component.js.map +1 -1
  17. package/dist/fit-to-shopper.d.ts +5 -1
  18. package/dist/fit-to-shopper.d.ts.map +1 -1
  19. package/dist/fit-to-shopper.js +6 -4
  20. package/dist/fit-to-shopper.js.map +1 -1
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js.map +1 -1
  24. package/dist/model-prompt.d.ts +13 -1
  25. package/dist/model-prompt.d.ts.map +1 -1
  26. package/dist/model-prompt.js +53 -21
  27. package/dist/model-prompt.js.map +1 -1
  28. package/dist/product-selection.d.ts +1 -9
  29. package/dist/product-selection.d.ts.map +1 -1
  30. package/dist/product-selection.js +1 -4
  31. package/dist/product-selection.js.map +1 -1
  32. package/dist/provider.d.ts +1 -8
  33. package/dist/provider.d.ts.map +1 -1
  34. package/dist/provider.js +1 -8
  35. package/dist/provider.js.map +1 -1
  36. package/dist/reconciliation.d.ts +15 -12
  37. package/dist/reconciliation.d.ts.map +1 -1
  38. package/dist/reconciliation.js +150 -340
  39. package/dist/reconciliation.js.map +1 -1
  40. package/dist/signal-digest.d.ts +10 -0
  41. package/dist/signal-digest.d.ts.map +1 -1
  42. package/dist/signal-digest.js +38 -28
  43. package/dist/signal-digest.js.map +1 -1
  44. package/dist/spec-cache.d.ts +14 -2
  45. package/dist/spec-cache.d.ts.map +1 -1
  46. package/dist/spec-cache.js +14 -24
  47. package/dist/spec-cache.js.map +1 -1
  48. package/dist/tracking-input.d.ts.map +1 -1
  49. package/dist/tracking-input.js +15 -13
  50. package/dist/tracking-input.js.map +1 -1
  51. package/package.json +4 -2
  52. package/src/claim-screening.ts +242 -0
  53. package/src/component-generator.ts +38 -123
  54. package/src/component-spec.ts +5 -18
  55. package/src/fallback-component.ts +3 -14
  56. package/src/fit-to-shopper.ts +11 -4
  57. package/src/index.ts +1 -0
  58. package/src/model-prompt.ts +58 -23
  59. package/src/product-selection.ts +2 -13
  60. package/src/provider.ts +1 -8
  61. package/src/reconciliation.ts +178 -359
  62. package/src/signal-digest.ts +41 -31
  63. package/src/spec-cache.ts +27 -24
  64. 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
- `zod` is a peer dependency. The public API of this package _is_ zod schemas, so
13
- your app and the package have to resolve the same copy of zod. **zod 4.5 or later is
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 same checks that pass a single product pass a whole set, and what lets
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 check looks for money, a
126
- customer score, a delivery date and a count of what's left, and it leaves a
127
- specification alone even when that specification has a number in it. Spotting one
128
- isn't a guarantee, not the way checking a price against your catalog is.
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, and the category they lean towards. Everything that makes a
165
- person an individual stays out of it: who they are, what they liked, viewed or
166
- searched for. That's what lets many page views reuse one call.
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. Every product still carries a basis
242
- we check against their real signals, and everything the model writes is still
243
- screened.
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;AAOnE,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;AAgKD,wBAAgB,wBAAwB,CACtC,OAAO,GAAE,yBAA8B,GACtC,kBAAkB,CA6PpB"}
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"}