@rudra-js/attested 0.9.0 → 0.9.1

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 (2) hide show
  1. package/README.md +229 -206
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -9,29 +9,31 @@ rudra-js checks it against your catalog, and React renders it.
9
9
  It is not a hosted platform, a visual or low-code app builder, or a workspace
10
10
  you sign up for.
11
11
 
12
- This package checks model-written product copy against the facts your shop
13
- stands behind.
12
+ You can use this package to check model-written product copy against the facts
13
+ your shop stands behind.
14
14
 
15
- The reference for both functions is on rudrajs.com:
15
+ For more, see the reference for
16
16
  [`verify`](https://rudrajs.com/reference/attested/verify) and
17
- [`verifyFields`](https://rudrajs.com/reference/attested/verifyFields).
17
+ [`verifyFields`](https://rudrajs.com/reference/attested/verifyFields) on
18
+ rudrajs.com.
18
19
 
19
- It's a pure function over text and facts. It has no dependencies and does no
20
- I/O. It uses no Node built-ins either, so it runs in a browser or an edge
21
- runtime as well as in Node. Drop it in after anything that writes copy:
22
- json-render, the Vercel AI SDK, a raw `fetch` to a model, or your own template.
20
+ It's a pure function over text and facts. There are no dependencies and no I/O.
21
+ With no Node built-ins either, it will run in browsers and edge runtimes as well
22
+ as in Node. You can run this after anything that writes copy (json-render, the
23
+ Vercel AI SDK, a raw `fetch` to a model, your template, etc.).
23
24
 
24
25
  ## The guarantee
25
26
 
26
27
  **When `quantity.supported` is true, every numeral in the text is one you
27
28
  supplied. A numeral the package can't read counts as a failure.**
28
29
 
29
- That's the whole promise, and it covers numerals only. It doesn't say the
30
- sentence around a number is true. It says nothing about `wording.supported`.
30
+ That's the whole promise, but it's just for numerals. It doesn't tell you if the
31
+ sentence around a number is true. It also says nothing about
32
+ `wording.supported`.
31
33
 
32
- The gaps it leaves are listed under
33
- [What it cannot catch](#what-it-cannot-catch). None of them is a bug waiting for
34
- a fix.
34
+ You can find the gaps it leaves listed under
35
+ [What it cannot catch](#what-it-cannot-catch). None of these gaps is a bug
36
+ waiting to be fixed.
35
37
 
36
38
  ## Install
37
39
 
@@ -51,22 +53,23 @@ result.quantity.findings;
51
53
 
52
54
  ## The two layers
53
55
 
54
- `verify` checks two layers and reports each one separately. Layer one,
55
- quantities, is a proof. Layer two, wording, is best-effort.
56
+ There are two layers to `verify`. It reports on each of these separately. Layer
57
+ one (quantities) is a proof, but layer two (wording) is just best-effort.
56
58
 
57
59
  ### Layer one: quantities
58
60
 
59
- Layer one finds every run of digits in the text and checks it against the values
60
- you supplied. It works the same whatever language the sentence is in.
61
+ This layer will find all the runs of digits in the text and check these runs
62
+ against the values you supplied. This doesn't depend on the language of the
63
+ sentence.
61
64
 
62
65
  ```ts
63
66
  verify('Nur noch 2 übrig', { values: [39] }).quantity.supported; // false
64
67
  verify('4,8 von 5', { values: [4.8, 5] }).quantity.supported; // true
65
68
  ```
66
69
 
67
- It leans toward rejecting. `"Only 2 left"` fails unless you supplied a fact
68
- carrying 2, even when 2 is also the number of colourways. A wrong rejection
69
- costs you a duller sentence. A wrong pass costs you a made-up claim.
70
+ It leans toward rejecting. `"Only 2 left"` will fail unless you have supplied a
71
+ fact carrying 2, even when 2 is also the number of colourways. A wrong rejection
72
+ will cost you a duller sentence, but a wrong pass will cost you a made-up claim.
70
73
 
71
74
  **What it reads.**
72
75
 
@@ -77,118 +80,125 @@ costs you a duller sentence. A wrong pass costs you a made-up claim.
77
80
  - Any Unicode decimal digit. Eastern Arabic `٤٫٨`, Devanagari `४.८` and
78
81
  mathematical `𝟰.𝟴` all read as 4.8.
79
82
  - Digit grouping in the Western (`12,345,678`), Indian (`1,29,999`), space
80
- (`1 299`) and Swiss (`1'299`) shapes. The space shape also accepts the
81
- no-break and narrow no-break spaces.
83
+ (`1 299`) and Swiss (`1'299`) shapes. For the space shape, it will also accept
84
+ the no-break and narrow no-break spaces.
82
85
 
83
86
  **How it reads a dot or a comma.**
84
87
 
85
- - One dot or comma with exactly three digits after it is grouping, never a
86
- decimal point. `1,299` and `1.299` both read as 1299 and nothing else. So a
87
- rating of 4.8 can't back a written `4.800`, and a stock count of 12 can't back
88
- `12.000`.
89
- - Where grouping is impossible, the decimal reading stands. `1234,567` is
88
+ - If there's just a single dot or comma with exactly three digits after it,
89
+ that's grouping and never a decimal point. `1,299` and `1.299` both read as
90
+ 1299 and nothing else. As a result, a rating of 4.8 can't back a written
91
+ `4.800`. A stock count of 12 can't back `12.000` either.
92
+ - If grouping is impossible, the decimal reading will stand. `1234,567` is
90
93
  1234.567.
91
- - Where the two marks differ, the last one is the decimal point. `1.299,50` is
94
+ - With two different marks, the last mark is the decimal point. `1.299,50` is
92
95
  1299.5.
93
96
 
94
- **What it refuses to read.** Layer one reports these as findings:
97
+ **What it refuses to read.** It will report these as findings:
95
98
 
96
99
  - A numeric character that isn't a decimal digit, like `½`, `²`, `②` or `Ⅲ`.
97
- These show up on the page as numbers, and the package has no way to check the
100
+ These show up on the page as numbers, but the package has no way to check the
98
101
  value they carry.
99
- - A magnitude mark glued to a run of digits, like `4.8万`, `12M` or `4.8k`. The
100
- digits may be yours. The multiplier isn't.
101
- - A run no locale reads as a number, like `24.12.2026` or `3.14.15`. Only a fact
102
- written exactly the same way backs it.
103
-
104
- Before any of this, layer one drops characters that take no room on the page. So
105
- a zero-width space, a soft hyphen, a variation selector, a backspace or a
106
- combining overline can't split `213` into a `2` and a `13` that you happen to
107
- have supplied separately. The rule works from Unicode's own categories, so it
108
- covers all of Unicode with no hand-kept list. It drops:
109
-
110
- - every format character
111
- - every default-ignorable code point
112
- - every control character except tabs and line breaks
113
- - every mark that attaches to the character before it
114
-
115
- A character that does take room still separates two numbers. A tab, a line break
116
- and a Devanagari matra all leave a gap the shopper can see. So
117
- `"Only 2\n3 left"` is two numbers, and a fact of 23 doesn't back it.
102
+ - A magnitude mark glued to a run of digits, like `4.8万`, `12M` or `4.8k`. You
103
+ may have supplied the digits, but not the multiplier.
104
+ - A run no locale reads as a number, like `24.12.2026` or `3.14.15`. These can
105
+ only be backed by a fact written the same way, but the digits may be in any
106
+ script.
107
+
108
+ To start with, this layer will drop characters that take no room on the page.
109
+ Because of this, a zero-width space, a soft hyphen, a variation selector, a
110
+ backspace or a combining overline can't split `213` into a `2` and a `13` that
111
+ you happen to have supplied separately. This rule is based on Unicode's
112
+ categories. That way it will cover all of Unicode with no hand-kept list. It
113
+ drops:
114
+
115
+ - all format characters
116
+ - all default-ignorable code points
117
+ - all control characters except tabs and line breaks
118
+ - all marks that attach to the previous character
119
+
120
+ But if a character does take room, it will separate two numbers. A tab, a line
121
+ break and a Devanagari matra all leave a gap the shopper can see. This is why
122
+ `"Only 2\n3 left"` counts as two numbers that a fact of 23 won't back.
118
123
 
119
124
  ### Layer two: wording
120
125
 
121
- Layer two looks for claims with no number in them: "cheap", "bargain", "on
122
- sale", "best-selling", "selling fast", "in stock", "free delivery", "trending".
123
- There's no value to check, so this layer is a denylist, a list of banned
124
- phrases. Every result it gives carries `strength: 'best-effort'`:
126
+ This layer looks for claims that don't have a number, like "cheap", "bargain",
127
+ "on sale", "best-selling", "selling fast", "in stock", "free delivery",
128
+ "trending". With no value to check, it has to be a denylist (a list of banned
129
+ phrases). All of its results carry `strength: 'best-effort'`:
125
130
 
126
131
  ```ts
127
132
  verify('Selling fast', { values: [] }).wording;
128
133
  // { supported: false, strength: 'best-effort', checked: 125, findings: [...] }
129
134
  ```
130
135
 
131
- The list also covers generic environmental claims, like "eco-friendly", "carbon
132
- neutral" and "sustainable". EU Directive 2024/825 applies through each country's
133
- own law from 27 September 2026. Under it, a generic environmental claim is
134
- unfair unless the trader can show recognised excellent environmental
135
- performance. A "neutral" claim based on offsetting is unfair outright. The list
136
- leaves out the bare word "green", because it's too often a colour.
136
+ This list also covers generic environmental claims, like "eco-friendly", "carbon
137
+ neutral" and "sustainable". From 27 September 2026, EU Directive 2024/825
138
+ applies via each country's law. Under this directive, a generic environmental
139
+ claim is unfair unless the trader can show recognised excellent environmental
140
+ performance. A "neutral" claim based on offsetting is unfair outright. But this
141
+ list leaves out the bare word "green", because that's too often a colour.
137
142
 
138
- Add phrases for your own language, and name the wording you stand behind:
143
+ You can add phrases for your language and also name the wording you stand
144
+ behind:
139
145
 
140
146
  ```ts
141
147
  verify('Nur noch wenige', { values: [], bannedPhrases: ['nur noch'] });
142
148
  verify('In stock, ships today', { values: [], allowedPhrases: ['in stock'] });
143
149
  ```
144
150
 
145
- `bannedPhrases` adds to the built-in list. `allowedPhrases` works on the text,
146
- not on the list. **Where one of your allowed phrases appears, a banned claim
147
- sitting wholly inside it isn't reported. The same words elsewhere in the copy
148
- still are.** So `['back in stock']` clears `"Back in stock"` but still catches
149
- the `in stock` in `"Only 2 in stock, selling fast"`. `checked` counts the whole
150
- list either way, because an allowance removes nothing from it.
151
-
152
- Without an allowance, the denylist bans a true statement as hard as a made-up
153
- one. A shop that really offers free shipping couldn't say so. Before you write
154
- an allowance, know this:
155
-
156
- - **A shorter allowance doesn't clear a longer claim.** Allowing `in stock`
157
- doesn't clear `back in stock`, which also claims a restock.
158
- - **Allowances don't combine, and they don't join up.** `['sold', 'out']` leaves
159
- `sold out` caught. `['back in', 'stock']` leaves both `in stock` and
151
+ `bannedPhrases` adds to the built-in list, but `allowedPhrases` works on the
152
+ text and not on the list. **A banned claim that sits wholly inside an allowed
153
+ phrase won't be reported. But if these words appear elsewhere in the copy, they
154
+ will be.** For example, `['back in stock']` clears `"Back in stock"` but won't
155
+ clear the `in stock` in `"Only 2 in stock, selling fast"`. `checked` counts the
156
+ whole list either way, because an allowance won't take anything off the list.
157
+
158
+ Without an allowance, the denylist will ban a true statement as hard as a
159
+ made-up claim. Even a shop that really offers free shipping couldn't say that.
160
+ Here are some things to know if you plan to write an allowance:
161
+
162
+ - **A shorter allowance doesn't clear a longer claim.** If you allow `in stock`,
163
+ this won't clear `back in stock`, which also claims a restock.
164
+ - **Allowances don't combine and don't join up.** `['sold', 'out']` leaves
165
+ `sold out` caught. `['back in', 'stock']` leaves `in stock` and
160
166
  `back in stock` caught.
161
- - **An allowance forgives whatever sits inside it, including phrases added to
162
- the list in a later version.** `['in stock and ready to ship']` forgives
163
- nothing extra today. The day `ready to ship` joins the list, that allowance
164
- silences it wherever your sentence appears. Keep allowances tight.
165
- - **An allowance doesn't cross a line break.** The denylist reads straight
166
- through one, so `"Free\n\nshipping"` is still caught. An allowance won't
167
- bridge it. The model decides where paragraph breaks fall. A shopper reads
168
- `"We do not offer"` above a horizontal rule and a free shipping claim below it
169
- as two separate things. Both rules lean toward reporting a claim.
170
-
171
- Before matching, layer two folds the copy and the phrases into one plain form:
167
+ - **An allowance forgives whatever sits inside it. This includes phrases added
168
+ to the list in a later version.** `['in stock and ready to ship']` forgives
169
+ nothing extra today. But on the day `ready to ship` joins the list, that
170
+ allowance will silence it wherever your sentence appears. Keep allowances as
171
+ tight as you can.
172
+ - **An allowance doesn't cross a line break.** But the denylist will ignore a
173
+ line break: `"Free\n\nshipping"` is still caught. An allowance won't bridge
174
+ it. It's up to the model to place paragraph breaks. If there's
175
+ `"We do not offer"` above a horizontal rule and a free shipping claim below
176
+ it, a shopper will read these as two separate things. Both rules lean toward
177
+ reporting a claim.
178
+
179
+ To match phrases, this layer first folds the copy and the phrases into a single
180
+ plain form:
172
181
 
173
182
  - Case is ignored.
174
183
  - Hyphens become spaces.
175
- - Accents are composed, so `café` keeps its `é`. This happens before anything is
176
- dropped. An overline composes with nothing, so it gets dropped and can't hide
177
- `free` from its own entry.
184
+ - Accents are composed: `café` keeps its `é`. This will be done before anything
185
+ is dropped. Because an overline composes with nothing, it will be dropped and
186
+ can't hide `free` from its entry.
178
187
  - Fullwidth forms are folded to their normal-width versions.
179
188
  - Characters that take no room are dropped.
180
189
  - A handful of Cyrillic and Greek letters that look like Latin ones are folded
181
190
  to Latin.
182
- - Greek letters lose their accents, so `ΕΥΚΑΙΡΙΑ` matches `ευκαιρία`. Greek
183
- words told apart only by their accent read as one, so `προσφορά` also catches
184
- `πρόσφορα`.
185
- - Spaces are dropped, so `送料 無料` still reads as `送料無料`.
186
-
187
- A phrase that starts or ends with an ASCII letter or digit won't match glued
188
- inside a longer word. So `cheap` misses `cheapskate`. A trailing `s` is allowed,
189
- so `discount` catches `discounts`. This word-gap rule reads the folded letters.
190
- So a Greek phrase that starts or ends on a letter folded to Latin is held to it
191
- too: `φθηνό` misses `φθηνότερο`. List each form you want caught.
191
+ - In Greek, letters lose their accents: `ΕΥΚΑΙΡΙΑ` matches `ευκαιρία`. Greek
192
+ words told apart only by their accent will read as a single word: `προσφορά`
193
+ also catches `πρόσφορα`.
194
+ - Spaces are dropped: `送料 無料` reads as `送料無料`.
195
+
196
+ If a phrase starts or ends with an ASCII letter or digit, it won't match glued
197
+ inside a longer word. For example, `cheap` misses `cheapskate`. But a trailing
198
+ `s` will be allowed. That way `discount` will catch `discounts`. This word-gap
199
+ rule reads the folded letters. This means a Greek phrase that starts or ends on
200
+ a letter folded to Latin is also held to this rule: `φθηνό` misses `φθηνότερο`.
201
+ You will have to list all of the forms you want caught.
192
202
 
193
203
  ## The result
194
204
 
@@ -213,9 +223,9 @@ interface Finding {
213
223
  }
214
224
  ```
215
225
 
216
- `checked` tells you how much a layer actually looked at. When `quantity.checked`
217
- is 0, the text held no numeral and layer one proved nothing about it. Layer one
218
- counts the numerals it refuses to read, so `"Only ② left"` gives a finding
226
+ `checked` tells you how much a layer actually looked at. If `quantity.checked`
227
+ is 0, the text held no numeral and that layer proved nothing about it. Numerals
228
+ it refuses to read are also counted. This is why `"Only ② left"` gives a finding
219
229
  instead of a silent zero.
220
230
 
221
231
  ## Facts
@@ -228,46 +238,50 @@ interface Facts {
228
238
  }
229
239
  ```
230
240
 
231
- `values` is one flat list with no types. Write each value the way your own
232
- system writes it, like `39`, `'$1,299.00'`, `'4,8'` or `'ships 13 March'`. Every
233
- numeral in each value becomes a supported value.
234
-
235
- A number is written out in plain digits first, whatever `String` would have made
236
- of it. So a fact of `1e21` backs `'1,000,000,000,000,000,000,000'` and backs
237
- neither `1` nor `21`. A string that holds nothing but a number in exponent
238
- notation is written out the same way. `String`, `JSON.stringify`, spreadsheet
239
- exports and APIs that write 64-bit values as strings all produce such strings.
240
- You didn't pick those digits. Any other string is read exactly as
241
- you typed it, so `'SKU AX-220e5'` still supplies 220 and 5.
242
-
243
- A number in exponent notation that would run past a thousand digits when written
244
- out backs no numeral at all.
245
- The package checks the length before it writes anything out. `'1e1000'` isn't a
246
- product fact under any reading, so it hits the limit and is refused. The limit
247
- stops the extreme cases. `'1e2000000000'` is twelve characters, but writing it
248
- out would take more memory than the process has. Every finite
249
- JavaScript number fits well inside the limit. The longest is `5e-324`, at 326
250
- characters.
251
-
252
- Past 2^53, a number has lost its exact digits before it ever reaches this
241
+ `values` is a single flat list with no types. Write all values the way your
242
+ system does, like `39`, `'$1,299.00'`, `'4,8'` or `'ships 13 March'`. All the
243
+ numerals in a value will become supported values, but not a glued magnitude or
244
+ any numeric character that isn't a digit. `12M`, `4.8万` and `½` will supply
245
+ nothing, not even the 12 or the 4.8.
246
+
247
+ Numbers will be written out in plain digits first, whatever `String` would have
248
+ made of them. For example, a fact of `1e21` backs
249
+ `'1,000,000,000,000,000,000,000'` and backs neither `1` nor `21`. If a string
250
+ holds nothing but a number in exponent notation, it will be written out in the
251
+ same way. `String`, `JSON.stringify`, spreadsheet exports and APIs that write
252
+ 64-bit values as strings all produce such strings. You didn't pick those digits.
253
+ Any other string will be read exactly as you typed it: `'SKU AX-220e5'` still
254
+ supplies 220 and 5.
255
+
256
+ If a number in exponent notation would run past a thousand digits when written
257
+ out, it backs no numeral at all. This package will check the length first and
258
+ only then write anything out. `'1e1000'` isn't a product fact under any reading.
259
+ It will hit the limit and be refused. This limit is there to stop the extreme
260
+ cases: `'1e2000000000'` is twelve characters, but writing it out would take more
261
+ memory than the process has. All finite JavaScript numbers fit well inside the
262
+ limit. Of these, the longest is `5e-324`, at 326 characters.
263
+
264
+ Past 2^53, a number may have lost its exact digits by the time it gets to this
253
265
  package. `JSON.parse` turns `9007199254740993` into `...92`. Pass a long id as a
254
266
  bigint or a string.
255
267
 
256
- A null or undefined field backs no numeral and doesn't throw, so one empty field
257
- doesn't break the whole call. Anything else is read through `String()` and
258
- supplies whatever digits that gives. For a `Date`, that's the day, the year and
259
- every digit of the clock. So a `listedAt` passed straight through isn't ignored.
260
- It's one of the hazards under [What it cannot catch](#what-it-cannot-catch).
268
+ The type of `values` only takes `string | number | bigint`, but plain JavaScript
269
+ or data typed `any` can pass other things. If a field is null or undefined, it
270
+ backs no numeral and doesn't throw. This way an empty field won't break the
271
+ whole call. Anything else is read via `String()` and supplies whatever digits
272
+ that gives. For a `Date`, that's the day, the year, the time zone offset and all
273
+ the digits of the clock. If you pass a raw `listedAt`, it won't be ignored. You
274
+ can find this hazard under [What it cannot catch](#what-it-cannot-catch).
261
275
 
262
- That section also lists what a flat list costs you. Two habits keep the cost
263
- down:
276
+ That section also lists the costs of a flat list. There are two habits to keep
277
+ the cost down:
264
278
 
265
- - **Only supply numbers you're happy to see anywhere.** A SKU of `AT-2199` in
266
- `values` makes `"Was $2,199"` a supported sentence. If a number in your record
267
- isn't a fact about the product that a shopper would read, leave it out.
268
- - **Treat `values` as a trust boundary.** On a marketplace, the seller writes
269
- the product title, not you. Putting it in `values` hands the model every
270
- number the seller chose to type. Supply only fields you write.
279
+ - **Only supply numbers you're happy to see anywhere.** With a SKU of `AT-2199`
280
+ in `values`, `"Was $2,199"` will be a supported sentence. If a number in your
281
+ record isn't a fact about the product that a shopper would read, leave it out.
282
+ - **Treat `values` as a trust boundary.** On a marketplace, the product title is
283
+ written by the seller and not by you. If you put it in `values`, you hand the
284
+ model all the numbers the seller chose to type. Supply only fields you write.
271
285
 
272
286
  ## Many fields, one product
273
287
 
@@ -284,99 +298,108 @@ result.fields; // [{ field, result }, ...] in the order the fields were given
284
298
  result.acrossFields; // the wording layer over every field read as one
285
299
  ```
286
300
 
287
- The facts are read once and reused for every field. A card shows its fields next
288
- to each other, and the model picks where one field ends and the next begins. So
289
- `{ badge: 'Free', delivery: 'delivery on every order' }` reads as one sentence
290
- to a shopper, though neither field carries the claim alone. `acrossFields`
291
- checks the joined text, and it counts toward `supported`.
301
+ These facts are read once and reused for all the fields. On a card the fields
302
+ are shown next to each other and it's up to the model to pick where a field ends
303
+ and the next begins. To a shopper,
304
+ `{ badge: 'Free', delivery: 'delivery on every order' }` reads as a single
305
+ sentence, though neither field carries the claim alone. `acrossFields` checks
306
+ the joined text and counts toward `supported`.
292
307
 
293
308
  ## What it cannot catch
294
309
 
295
310
  ### Claims with a number in them
296
311
 
297
312
  - **A number that is real but means something else.** This is the biggest gap in
298
- layer one. Supply a price of 39, and `"39 sold in the last hour"`,
299
- `"Save $39 today"` and `"Under $39"` all pass. Supply a rating scale of 5, and
300
- `"Only 5 left"` passes. Supply a rating of 4.8, and a German `"nur 4,80 €"`
301
- passes as a price, because 4.80 and 4.8 are the same number.
302
- - **Numbers written as words, in any script.** `"Only two left"` has no digit in
303
- it, so layer one never sees it. Layer one also misses `残り二点`, where 二 is a
304
- character rather than a digit. It misses Arabic `قطعتين` too, where "two" is
305
- the noun's dual ending and there is no numeral at all. `quantity.checked` is 0
306
- for all three, and that's the only signal you get.
307
- - **Magnitudes written as words.** The multiplier in `4,8 Millionen`,
308
- `4.8 millions`, `4,8 mil`, `4.8 लाख` or `٤٫٨ مليون` isn't caught.
309
- - **The unit on the number.** `¥39.99` passes against a price of $39.99, because
310
- the digits are yours and the currency symbol isn't a digit. So does
311
- `"39,99 Cent"`. Layer one checks the number, never what it is a number of.
312
- - **A number your facts happen to contain.** A fact of `'2026-09-13'` makes
313
- 2026, 9 and 13 supported anywhere in the text, including as a stock count.
313
+ layer one. If you supply a price of 39, then `"39 sold in the last hour"`,
314
+ `"Save $39 today"` and `"Under $39"` will all pass. If you supply a rating
315
+ scale of 5, `"Only 5 left"` will pass. With a rating of 4.8, a German
316
+ `"nur 4,80 €"` will pass as a price, because 4.80 and 4.8 are equal.
317
+ - **Numbers written as words, in any script.** This layer will never see
318
+ `"Only two left"`, because there's no digit in it. It will also miss
319
+ `残り二点`, because 二 is a character and not a digit. And it will miss Arabic
320
+ `قطعتين`, where "two" is the noun's dual ending and there is no numeral at
321
+ all. `quantity.checked` is 0 for all three of these. That's the only signal
322
+ you get.
323
+ - **Magnitudes written as words.** This layer won't catch the multiplier in
324
+ `4,8 Millionen`, `4.8 millions`, `4,8 mil`, `4.8 लाख` or `٤٫٨ مليون`.
325
+ - **Units on numbers.** `¥39.99` will pass against a price of $39.99, because
326
+ the digits are yours and a currency symbol isn't a digit. `"39,99 Cent"` will
327
+ also pass. This layer can only check the number and not the thing it counts.
328
+ - **A number your facts happen to contain.** With a fact of `'2026-09-13'`, all
329
+ of 2026, 9 and 13 will be supported anywhere in the text, including as a stock
330
+ count.
314
331
  - **Where in the text a token was.** A finding names the token but not its
315
332
  position.
316
333
 
317
334
  ### Claims with no number in them
318
335
 
319
- - **Wording, in general.** There are endless ways to word a claim, and a list
320
- can't hold them all. A careful rewording gets past layer two, like
321
- `"Half of what it was"` for half price, or `"送料は無料"` with a particle dropped
322
- in. Entries are added as they're found, and the list will never be finished.
323
- - **Your language, until you give it one.** Every built-in phrase is English.
324
- `"Gratis Versand"`, `"Livraison offerte"` and `"شحن مجاني"` all pass by
325
- default. Add them with `bannedPhrases`. That list is yours to keep.
326
- - **Negation.** The list matches substrings, so
327
- `"there is no sale on this product"` is caught as a sale claim. No rule reads
328
- the `no`. `allowedPhrases: ['no sale']` lets that sentence pass. That
329
- allowance is narrow, so a `sale` elsewhere in the same text still fails.
336
+ - **Wording, in general.** There are endless ways to word a claim, more than a
337
+ list can hold. A careful rewording will get past layer two, like
338
+ `"Half of what it was"` for half price or `"送料は無料"` with a particle
339
+ dropped in. More entries will be added as they're found, but the list will
340
+ never be finished.
341
+ - **Your language, until you give it one.** All of the built-in phrases are
342
+ English. `"Gratis Versand"`, `"Livraison offerte"` and `"شحن مجاني"` all pass
343
+ by default. You'll have to add them with `bannedPhrases`. That list is yours
344
+ to keep.
345
+ - **Negation.** Because the list matches substrings,
346
+ `"there is no sale on this product"` is caught as a sale claim. There's no
347
+ rule that reads the `no`. You can use `allowedPhrases: ['no sale']` to let
348
+ that sentence pass. But this allowance is narrow: a `sale` elsewhere in the
349
+ text will still fail.
330
350
  - **A character that folds into your allowance.** Allowances match the copy
331
351
  after the folding described under layer two. Anything that folds to your words
332
- counts as your words. Allow `no sale` and `"№ SALE"` passes, because U+2116
333
- folds to `no`. A superscript `ⁿᵒ` does the same. The shopper reads the numero
334
- sign as decoration, but the package reads it as a word. Write allowances the
335
- way a shop really writes them.
352
+ will count as your words. If you allow `no sale`, then `"№ SALE"` passes,
353
+ because U+2116 folds to `no`. This is also the case for a superscript `ⁿᵒ`. To
354
+ the shopper the numero sign is decoration, but to the package it's a word.
355
+ Write allowances the way a shop really would.
336
356
  - **A letter glued to the end of a phrase.** `"FREE SHIPPINGᵃ see terms"` isn't
337
- reported. The superscript folds to an `a`, which glues onto `shipping`. The
338
- word-gap rule that stops `cheap` matching `cheapskate` then treats the whole
339
- thing as a longer word. This holds for every entry on the list. A footnote
340
- marker is exactly how a real shop writes one.
357
+ reported. This superscript folds to an `a` that glues onto `shipping`. Then
358
+ the word-gap rule that stops `cheap` matching `cheapskate` will treat the
359
+ whole thing as a longer word. This holds for all entries on the list. A
360
+ footnote marker is exactly how a real shop writes a claim like this.
341
361
 
342
362
  ### Preconditions the package cannot check for you
343
363
 
344
- - **The text you check has to be the text you render.** `verify` sees a string.
345
- Something downstream may decode it, like an HTML sink turning `½` into ½
346
- or `2` into a 2. Then the string you checked and the string on the page
347
- are different documents. The guarantee covers only the one you checked.
348
- - **The font has to agree with Unicode about what renders as nothing.** The
349
- package drops what Unicode marks as taking no room. Where a font gives one of
350
- those characters a visible glyph, the two disagree. U+3164 HANGUL FILLER is
351
- the one people really paste as a blank. Layer one reads `"1<U+3164>3"` as 13,
352
- while a font that widens it shows the shopper `1 3`.
353
- - **Everything that isn't a number or a phrase.** A claim about materials,
364
+ - **You have to render the text you checked.** `verify` sees a string, but
365
+ something downstream may decode it, like an HTML sink turning `&half;` into ½
366
+ or `&#50;` into a 2. If this happens, the string you checked and the string on
367
+ the page will be different documents. This guarantee only covers the string
368
+ you checked.
369
+ - **Your font has to agree with Unicode about what renders as nothing.** This
370
+ package will drop anything that Unicode marks as taking no room. If a font
371
+ gives any of these characters a visible glyph, the two disagree. U+3164 HANGUL
372
+ FILLER is the character people really paste as a blank. Layer one reads
373
+ `"1<U+3164>3"` as 13, but with a font that widens it the shopper will see
374
+ `1 3`.
375
+ - **Everything that isn't a number or a phrase.** Any claim about materials,
354
376
  compatibility, a certification, a warranty condition or a person is out of
355
377
  scope.
356
378
 
357
379
  ## What it over-rejects
358
380
 
359
- Leaning toward rejection has a cost:
360
-
361
- - **Every incidental number has to be in `values`.** That means a VAT rate, a
362
- size, a care temperature, a number from the product specification, a support
363
- phone number, a founding year, the denominator of a rating scale. None of them
364
- are claims, but all of them are digits. Copy that mentions one you didn't
365
- supply is rejected.
366
- - **Three digits after a dot or comma are always grouping.** A real
367
- three-decimal number, like a weight or a price in a currency with three
368
- decimal places, can't be written with a dot or a comma. `1.299 kg` reads
369
- as 1299. Supply it and write it the same way.
381
+ There's a cost to leaning toward rejection:
382
+
383
+ - **All incidental numbers have to be in `values`.** That means things like a
384
+ VAT rate, a size, a care temperature, a number from the product specification,
385
+ a support phone number, a founding year, the denominator of a rating scale.
386
+ None of these are claims, but they're all digits. If copy mentions a number
387
+ you didn't supply, it will be rejected.
388
+ - **With up to three digits in front, three digits after a single dot or comma
389
+ are always grouping.** Neither mark can be used to write a real three-decimal
390
+ number that way (like a weight or a price in a currency with three decimal
391
+ places). `1.299 kg` reads as 1299. Supply it and write it the same way.
370
392
  - **Numeric characters that aren't digits are refused outright.** `2 m²` and
371
- `½ inch` fail.
372
- - **A magnitude letter glued to a digit is refused.** `4K display` fails.
373
- `4 K display` and `1kg` don't, because a letter followed by another letter is
374
- a unit, not a multiplier.
393
+ `½ inch` will fail.
394
+ - **Magnitude letters glued to a digit are refused.** `4K display` will fail.
395
+ But `4 K display` and `1kg` won't, because a letter followed by another letter
396
+ is just a unit and not a multiplier.
375
397
  - **Scientific notation in the copy can never be supported.** A fact of `1e-7`
376
- is written out as `0.0000001`, and the text `"1e-7 g"` reads as the two
377
- numerals 1 and 7, so the two never meet. `"0.0000001 g"` passes. That's the
378
- cost of writing facts out in plain digits. It falls on a lab or bulk-goods
379
- shop that prints a value the way its own data prints it.
398
+ is written out as `0.0000001`, but the text `"1e-7 g"` will be read as the two
399
+ numerals 1 and 7. This means the fact won't ever match the text.
400
+ `"0.0000001 g"` will pass. That's the cost of writing facts out in plain
401
+ digits. It falls on a lab or bulk-goods shop printing a value the way its data
402
+ does.
380
403
 
381
404
  ## Licence
382
405
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rudra-js/attested",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "From rudra-js: checks model-written product copy against the facts a shop stands behind",
5
5
  "keywords": [
6
6
  "rudra-js",