@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.
- package/README.md +229 -206
- 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
|
-
|
|
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
|
-
|
|
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.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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,
|
|
30
|
-
sentence around a number is true. It says nothing about
|
|
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
|
-
|
|
33
|
-
[What it cannot catch](#what-it-cannot-catch). None of
|
|
34
|
-
|
|
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
|
-
|
|
55
|
-
quantities
|
|
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
|
-
|
|
60
|
-
you supplied.
|
|
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"`
|
|
68
|
-
carrying 2, even when 2 is also the number of colourways. A wrong rejection
|
|
69
|
-
|
|
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.
|
|
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
|
-
-
|
|
86
|
-
decimal point. `1,299` and `1.299` both read as
|
|
87
|
-
|
|
88
|
-
`12.000
|
|
89
|
-
-
|
|
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
|
-
-
|
|
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.**
|
|
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,
|
|
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`.
|
|
100
|
-
digits
|
|
101
|
-
- A run no locale reads as a number, like `24.12.2026` or `3.14.15`.
|
|
102
|
-
written
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
-
|
|
113
|
-
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
122
|
-
sale", "best-selling", "selling fast", "in stock", "free delivery",
|
|
123
|
-
|
|
124
|
-
phrases.
|
|
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
|
-
|
|
132
|
-
neutral" and "sustainable". EU Directive 2024/825
|
|
133
|
-
|
|
134
|
-
unfair unless the trader can show recognised excellent environmental
|
|
135
|
-
performance. A "neutral" claim based on offsetting is unfair outright.
|
|
136
|
-
leaves out the bare word "green", because
|
|
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
|
-
|
|
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
|
|
146
|
-
not on the list. **
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
the `in stock` in `"Only 2 in stock, selling fast"`. `checked` counts the
|
|
150
|
-
list either way, because an allowance
|
|
151
|
-
|
|
152
|
-
Without an allowance, the denylist
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
- **A shorter allowance doesn't clear a longer claim.**
|
|
157
|
-
|
|
158
|
-
- **Allowances don't combine
|
|
159
|
-
`sold out` caught. `['back in', 'stock']` leaves
|
|
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
|
|
162
|
-
the list in a later version.** `['in stock and ready to ship']` forgives
|
|
163
|
-
nothing extra today.
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
|
176
|
-
dropped.
|
|
177
|
-
`free` from its
|
|
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
|
|
183
|
-
words told apart only by their accent read as
|
|
184
|
-
`πρόσφορα`.
|
|
185
|
-
- Spaces are dropped
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
inside a longer word.
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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.
|
|
217
|
-
is 0, the text held no numeral and layer
|
|
218
|
-
|
|
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
|
|
232
|
-
system
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
out
|
|
249
|
-
|
|
250
|
-
characters
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
|
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.**
|
|
266
|
-
`values
|
|
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
|
|
269
|
-
the
|
|
270
|
-
|
|
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
|
-
|
|
288
|
-
to each other
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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.
|
|
299
|
-
`"Save $39 today"` and `"Under $39"` all pass.
|
|
300
|
-
`"Only 5 left"`
|
|
301
|
-
|
|
302
|
-
- **Numbers written as words, in any script.**
|
|
303
|
-
|
|
304
|
-
character
|
|
305
|
-
the noun's dual ending and there is no numeral at
|
|
306
|
-
for all three
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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,
|
|
320
|
-
can
|
|
321
|
-
`"Half of what it was"` for half price
|
|
322
|
-
in.
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
|
|
333
|
-
folds to `no`.
|
|
334
|
-
sign
|
|
335
|
-
way a shop really
|
|
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.
|
|
338
|
-
word-gap rule that stops `cheap` matching `cheapskate`
|
|
339
|
-
thing as a longer word. This holds for
|
|
340
|
-
marker is exactly how a real shop writes
|
|
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
|
-
- **
|
|
345
|
-
|
|
346
|
-
or `2` into a 2.
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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 `½` into ½
|
|
366
|
+
or `2` 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
|
-
|
|
360
|
-
|
|
361
|
-
- **
|
|
362
|
-
size, a care temperature, a number from the product specification,
|
|
363
|
-
phone number, a founding year, the denominator of a rating scale.
|
|
364
|
-
are claims, but all
|
|
365
|
-
supply
|
|
366
|
-
- **
|
|
367
|
-
|
|
368
|
-
|
|
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
|
-
- **
|
|
373
|
-
`4 K display` and `1kg`
|
|
374
|
-
a unit
|
|
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`,
|
|
377
|
-
numerals 1 and 7
|
|
378
|
-
cost of writing facts out in plain
|
|
379
|
-
|
|
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
|
|