@rudra-js/attested 0.8.1 → 0.8.3

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 CHANGED
@@ -2,34 +2,36 @@
2
2
 
3
3
  This package is part of [rudra-js](https://rudrajs.com).
4
4
 
5
- rudra-js is an open-source TypeScript library (MIT) you install from npm. A language model designs one product-recommendation block for a page in your shop, rudra-js checks it against your catalog, and React renders it.
5
+ rudra-js is an open-source TypeScript library (MIT) you install from npm. A
6
+ language model designs one product-recommendation block for a page in your shop,
7
+ rudra-js checks it against your catalog, and React renders it.
6
8
 
7
- It is not a hosted platform, a visual or low-code app builder, or a workspace you sign up for.
9
+ It is not a hosted platform, a visual or low-code app builder, or a workspace
10
+ you sign up for.
8
11
 
9
- Checks model-written product copy against the facts a shop stands behind.
12
+ This package checks model-written product copy against the facts your shop
13
+ stands behind.
10
14
 
11
- Both entry points are documented at [rudrajs.com](https://rudrajs.com/docs#verify).
15
+ The reference for both functions is on rudrajs.com:
16
+ [`verify`](https://rudrajs.com/reference/attested/verify) and
17
+ [`verifyFields`](https://rudrajs.com/reference/attested/verifyFields).
12
18
 
13
- Zero dependencies, no node builtins, no I/O. It is one pure function over text
14
- and facts, so it runs in Node, in an edge runtime and in a browser, and it drops
15
- in after anything that writes copy — json-render, the Vercel AI SDK, a raw
16
- `fetch` to a model, your own template.
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.
17
23
 
18
24
  ## The guarantee
19
25
 
20
26
  **When `quantity.supported` is true, every numeral in the text is one you
21
- supplied — and a numeral the reader cannot read counts as a failure, not a
22
- pass.**
27
+ supplied. A numeral the package can't read counts as a failure.**
23
28
 
24
- That is the whole promise, and it is a promise about numerals, not about
25
- sentences. It does not say the sentence around the number is true: a price you
26
- supplied can still be written as a sales figure. It says nothing at all about
27
- `wording.supported`, which is a denylist over an open set and carries
28
- `strength: 'best-effort'` on every result for that reason.
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`.
29
31
 
30
- Everything the guarantee leaves open is listed under
31
- [What it cannot catch](#what-it-cannot-catch). Nothing there is a bug queued for
32
- later; it is the shape of what a closed check over digits can promise.
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.
33
35
 
34
36
  ## Install
35
37
 
@@ -49,131 +51,144 @@ result.quantity.findings;
49
51
 
50
52
  ## The two layers
51
53
 
52
- They are checked separately and reported separately, because only one of them is
53
- a proof.
54
+ `verify` checks two layers and reports each one separately. Layer one,
55
+ quantities, is a proof. Layer two, wording, is best-effort.
54
56
 
55
- ### Layer one — quantities. This is the provable half.
57
+ ### Layer one: quantities
56
58
 
57
- Every run of digits in the text has to be a value you supplied. Not "does this
58
- read like a bad phrase" but "is this number one the shop stands behind". The set
59
- of digits in a string is closed and checkable, and it does not care what
60
- language the sentence is in.
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
61
 
62
62
  ```ts
63
63
  verify('Nur noch 2 übrig', { values: [39] }).quantity.supported; // false
64
64
  verify('4,8 von 5', { values: [4.8, 5] }).quantity.supported; // true
65
65
  ```
66
66
 
67
- It is crude on purpose and it errs toward rejection. `"Only 2 left"` is
68
- unsupported unless you supplied a fact carrying 2, even though 2 is also the
69
- number of colourways. A false rejection costs a duller sentence; a false
70
- acceptance costs a fabricated claim.
71
-
72
- **What it reads.** Bare integers and decimals, both decimal separators (`4.8`
73
- and `4,8`), currency symbols attached or detached, percentages, and any Unicode
74
- decimal digit — Eastern Arabic `٤٫٨`, Devanagari `४.८` and mathematical `𝟰.𝟴`
75
- all read as 4.8. Grouping is read in the Western (`12,345,678`), Indian
76
- (`1,29,999`), space (`1 299`, including the no-break and narrow no-break spaces)
77
- and Swiss (`1'299`) shapes.
78
-
79
- **How an ambiguous mark is decided.** One dot or comma with exactly three digits
80
- behind it is grouping, never a decimal point. `1,299` and `1.299` both read as
81
- 1299 and nothing else, which is why a rating of 4.8 cannot stand behind a
82
- written `4.800` and a stock count of 12 cannot stand behind `12.000`. Where
83
- grouping is impossible the decimal reading survives, so `1234,567` is still
84
- 1234.567. Where the two marks differ the last one is the decimal point, so
85
- `1.299,50` is 1299.5.
86
-
87
- **What it refuses to read.** Three things become findings rather than passes:
88
-
89
- - A numeric character that is not a decimal digit — `½`, `²`, `②`, `Ⅲ`. These
90
- render as numbers and carry a value the reader has no way to check.
91
- - A magnitude mark glued to a run — `4.8万`, `12M`, `4.8k`. The digits may be
92
- yours; the multiplier is not.
93
- - A run no locale reads as a number — `24.12.2026`, `3.14.15`. These are
94
- supported only by a fact written exactly the same way.
95
-
96
- Characters that take no room on the page are dropped before any of this, so a
97
- zero-width space, a soft hyphen, a variation selector, a backspace or a
98
- combining overline cannot split `213` into a `2` and a `13` that happen to be
99
- supported separately. That class is every format character, every
100
- default-ignorable code point, every control character and every mark that hangs
101
- on the character before it — a rule over the whole of Unicode rather than a list
102
- someone kept up to date.
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
+
71
+ **What it reads.**
72
+
73
+ - Whole numbers and decimals.
74
+ - Both decimal separators: `4.8` and `4,8`.
75
+ - Currency symbols, attached or detached.
76
+ - Percentages.
77
+ - Any Unicode decimal digit. Eastern Arabic `٤٫٨`, Devanagari `४.८` and
78
+ mathematical `𝟰.𝟴` all read as 4.8.
79
+ - 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.
82
+
83
+ **How it reads a dot or a comma.**
84
+
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
90
+ 1234.567.
91
+ - Where the two marks differ, the last one is the decimal point. `1.299,50` is
92
+ 1299.5.
93
+
94
+ **What it refuses to read.** Layer one reports these as findings:
95
+
96
+ - 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
98
+ 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
103
114
 
104
115
  A character that does take room still separates two numbers. A tab, a line break
105
- and a Devanagari matra all leave a gap the shopper can see, so `"Only 2\n3 left"`
106
- is two numbers and a fact of 23 does not stand behind it.
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.
107
118
 
108
- ### Layer two — wording. This is the best-effort half.
119
+ ### Layer two: wording
109
120
 
110
- Claims with no number in them: "cheap", "bargain", "on sale", "best-selling",
111
- "selling fast", "in stock", "free delivery", "trending". There is no value to
112
- check, so there is nothing to invert. It stays a denylist, it stays open and
113
- English-first, and every result it produces says so:
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'`:
114
125
 
115
126
  ```ts
116
127
  verify('Selling fast', { values: [] }).wording;
117
- // { supported: false, strength: 'best-effort', checked: 122, findings: [...] }
128
+ // { supported: false, strength: 'best-effort', checked: 125, findings: [...] }
118
129
  ```
119
130
 
120
- The list also carries generic environmental claims — "eco-friendly", "carbon
121
- neutral", "sustainable". Under EU Directive 2024/825, applied through each
122
- country's own law from 27 September 2026, a generic environmental claim is
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
123
134
  unfair unless the trader can show recognised excellent environmental
124
- performance, and a "neutral" claim based on offsetting is unfair outright. The
125
- list leaves out the bare word "green", which is too often a colour.
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.
126
137
 
127
- Add your own for your own language, and name the wording you stand behind:
138
+ Add phrases for your own language, and name the wording you stand behind:
128
139
 
129
140
  ```ts
130
141
  verify('Nur noch wenige', { values: [], bannedPhrases: ['nur noch'] });
131
142
  verify('In stock, ships today', { values: [], allowedPhrases: ['in stock'] });
132
143
  ```
133
144
 
134
- `bannedPhrases` is added to the built-in list. `allowedPhrases` works on the
135
- text, not on the list: **where one of your phrases appears, a banned claim
136
- sitting wholly inside it is not reported, and the same words elsewhere in the
137
- copy still are.** So `['back in stock']` clears `"Back in stock"` and still
138
- catches the `in stock` in `"Only 2 in stock, selling fast"`. `checked` is the
139
- whole list either way, because nothing leaves it.
140
-
141
- Without an allowance a denylist bans the truthful disclosure as hard as the
142
- fabricated one, and a shop that genuinely offers free shipping cannot say so.
143
- Three things are worth knowing before you write one:
144
-
145
- - **Containment runs one way.** Allowing `in stock` does not clear
146
- `back in stock`, which claims a restock on top of it.
147
- - **Allowances do not compose, and they do not join.**
148
- `['sold', 'out']` leaves `sold out` caught, and `['back in', 'stock']` leaves
149
- both `in stock` and `back in stock` caught.
150
- - **An allowance forgives whatever nests inside it, including entries added in a
151
- later version.** `['in stock and ready to ship']` forgives nothing extra
152
- today; the day `ready to ship` joins the list, that allowance silences it
153
- wherever your sentence appears. Keep them tight.
154
- - **An allowance does not cross a line break.** The denylist reads straight
155
- through one, so `"Free\n\nshipping"` is still caught, but an allowance will
156
- not bridge it. The model picks where the paragraph breaks fall, and
157
- `"We do not offer"` above a horizontal rule, with the free shipping claim
158
- below it, is two things a shopper reads apart. The two rules pull opposite
159
- ways on purpose: the denylist catches more, the allowance forgives less, and
160
- both err toward reporting.
161
-
162
- It is also the way around negation. The list matches substrings, so
163
- `"there is no sale on this product"` reads as a sale claim; allow `no sale` and
164
- that sentence passes while a `sale` later in the same text still fails.
165
-
166
- Matching is case-insensitive. Hyphens become spaces, accents are composed,
167
- fullwidth forms are folded, characters that take no room are dropped, and a
168
- handful of Cyrillic and Greek letters that render as Latin ones are folded to
169
- Latin. Accents are composed before anything is dropped, so `café` keeps its `é`
170
- while an overline, which composes with nothing, cannot hide `free` from its own
171
- entry. A line break stays a line break: matching reads through it, and only an
172
- allowance refuses to cross it. Spaces are dropped from both sides before
173
- matching, so `送料 無料` still
174
- reads as `送料無料`. A phrase whose first or last character is an ASCII letter or
175
- digit will not match glued inside a longer word, so `cheap` misses `cheapskate`,
176
- but a trailing `s` is allowed, so `discount` catches `discounts`.
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
160
+ `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:
172
+
173
+ - Case is ignored.
174
+ - 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.
178
+ - Fullwidth forms are folded to their normal-width versions.
179
+ - Characters that take no room are dropped.
180
+ - A handful of Cyrillic and Greek letters that look like Latin ones are folded
181
+ 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.
177
192
 
178
193
  ## The result
179
194
 
@@ -198,11 +213,10 @@ interface Finding {
198
213
  }
199
214
  ```
200
215
 
201
- `checked` says how much the layer actually looked at. `quantity.checked === 0`
202
- means the text held no numeral and layer one proved nothing about it — which is
203
- the honest reading of `"Only two left"`, and no assertion you can write on the
204
- result will turn that into a catch. Layer one counts the numerals it refuses to
205
- read, so `"Only ② left"` is a finding rather than a silent zero.
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
219
+ instead of a silent zero.
206
220
 
207
221
  ## Facts
208
222
 
@@ -214,54 +228,46 @@ interface Facts {
214
228
  }
215
229
  ```
216
230
 
217
- `values` is a flat list, not a typed one. Write them however they are written in
218
- your own system — `39`, `'$1,299.00'`, `'4,8'`, `'ships 13 March'` — and every
219
- numeral in each one becomes a supported value.
220
-
221
- A number is laid out in positional notation first, whatever `String` would have
222
- made of it, so a fact of `1e21` stands behind
223
- `'1,000,000,000,000,000,000,000'` and behind neither `1` nor `21`. A string
224
- whose whole content is a number in exponent notation is laid out the same way:
225
- `String`, `JSON.stringify`, a spreadsheet export and an API that writes 64-bit
226
- values as strings all produce one, and none of those is you choosing the digits.
227
- Any other string is read exactly as you typed it, so `'SKU AX-220e5'` still
228
- mints 220 and 5.
229
-
230
- An exponent that would lay out past a thousand digits stands behind no numeral at
231
- all, and the length is weighed before any of it is written. `'1e1000'` is no
232
- product fact under any reading, so it is refused at the limit rather than laid
233
- out. The limit is what stops the far end: `'1e2000000000'` is twelve characters,
234
- and writing its run out would cost more memory than the process has. Every finite
235
- JavaScript number fits well inside the limit — the longest is `5e-324`, at 326
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
236
250
  characters.
237
251
 
238
- Past 2^53 a number has lost its exact digits before it ever reaches here —
239
- `JSON.parse` turns `9007199254740993` into `...92` — so hand a long id over as a
240
- bigint or as a string. A null or undefined field stands behind no numeral rather
241
- than throwing, so one empty field does not take the whole call down. Anything
242
- else is read through `String()` and mints whatever digits that produces, which
243
- for a `Date` is the day, the year and every digit of the clock — so a `listedAt`
244
- handed straight through is the hazard named under **What it cannot catch**, not
245
- an ignored field.
246
-
247
- Flat rather than typed (money, count, rating, date) because typing only helps if
248
- the extractor can classify a token in the text, and it cannot. The `2` in
249
- `"Only 2 left"` and the `2` in `"2 year warranty"` are the same two characters;
250
- telling them apart means understanding the sentence, in whatever language it was
251
- written, which is the problem this package exists to stop relying on. Typed
252
- facts would also need an "unclassified, so skip it" branch, and that branch is a
253
- hole. A flat set has no hole: every numeral is checked against every value.
254
-
255
- The cost is real and it is stated under **What it cannot catch**. Two things
256
- follow from it that are worth doing:
257
-
258
- - **Supply only numbers you are happy to see anywhere.** A SKU of `AT-2199` in
259
- `values` makes `"Was $2,199"` a supported sentence. A listing date makes its
260
- day number a supported sales figure. If a number in your record is not a fact
261
- about the product a shopper would read, leave it out.
262
- - **`values` is a trust boundary.** On a marketplace the product title is
263
- written by the seller, not by you. Putting it in `values` hands the model
264
- every number the seller chose to type. Supply fields you author.
252
+ Past 2^53, a number has lost its exact digits before it ever reaches this
253
+ package. `JSON.parse` turns `9007199254740993` into `...92`. Pass a long id as a
254
+ bigint or a string.
255
+
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).
261
+
262
+ That section also lists what a flat list costs you. Two habits keep the cost
263
+ down:
264
+
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.
265
271
 
266
272
  ## Many fields, one product
267
273
 
@@ -278,103 +284,99 @@ result.fields; // [{ field, result }, ...] in the order the fields were given
278
284
  result.acrossFields; // the wording layer over every field read as one
279
285
  ```
280
286
 
281
- The facts are read once and reused. A card renders its fields next to each
282
- other, and the model picks the field boundaries, so
283
- `{ badge: 'Free', delivery: 'delivery on every order' }` is one sentence to a
284
- shopper while neither field carries it alone. `acrossFields` reads the joined
285
- text and counts toward `supported`.
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`.
286
292
 
287
293
  ## What it cannot catch
288
294
 
289
- This is the honest list.
290
-
291
295
  ### Claims with a number in them
292
296
 
293
- - **A number that is real but means something else.** This is the largest hole
294
- in the provable half. A flat fact set proves the number came from you, not
295
- that the sentence uses it correctly. Supply a price of 39 and `"39 sold in the
296
- last hour"`, `"Save $39 today"` and `"Under $39"` all pass. Supply a rating
297
- scale of 5 and `"Only 5 left"` passes. Supply a rating of 4.8 and a German
298
- `"nur 4,80 €"` passes as a price, because 4.80 and 4.8 are one number.
297
+ - **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.
299
302
  - **Numbers written as words, in any script.** `"Only two left"` has no digit in
300
- it, so layer one never sees it. Neither does `残り二点`, where 二 is a
301
- character rather than a digit, nor Arabic `قطعتين`, where "two" is the noun's
302
- dual ending and there is no numeral at all. `quantity.checked` is 0 for all
303
- three, and that is the only signal you get.
304
- - **Magnitudes written as words.** `4.8万` and `12M` are caught. `4,8 Millionen`,
305
- `4.8 millions`, `4,8 mil`, `4.8 लाख` and `٤٫٨ مليون` are not — a word list is
306
- the denylist this package exists to get away from.
307
- - **The unit on the number.** `¥39.99` against a price of $39.99 passes: the
308
- digits are yours and the currency symbol is not a digit. So does
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
309
311
  `"39,99 Cent"`. Layer one checks the number, never what it is a number of.
310
312
  - **A number your facts happen to contain.** A fact of `'2026-09-13'` makes
311
- 2026, 9 and 13 supported everywhere in the text, including as a stock count.
312
- - **Where in the text a token was.** A finding names the token, not its offset.
313
+ 2026, 9 and 13 supported anywhere in the text, including as a stock count.
314
+ - **Where in the text a token was.** A finding names the token but not its
315
+ position.
313
316
 
314
317
  ### Claims with no number in them
315
318
 
316
- - **Wording, in general.** Layer two is a denylist over an infinite set. A
317
- careful rewording gets past it — `"Half of what it was"` for half price,
318
- `"送料は無料"` with a particle dropped in. Entries are added as they are found,
319
- and the list will never be finished.
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.
320
323
  - **Your language, until you give it one.** Every built-in phrase is English.
321
324
  `"Gratis Versand"`, `"Livraison offerte"` and `"شحن مجاني"` all pass by
322
- default. `bannedPhrases` is the answer, and it is your list to keep.
323
- - **Negation.** The list matches substrings, so `"there is no sale on this
324
- product"` is caught as a sale claim on its own. There is no rule that reads the
325
- `no`; `allowedPhrases: ['no sale']` is the way out, and it is narrow by
326
- design — a `sale` elsewhere in the same text still fails.
327
- - **A character that folds into your allowance.** An allowance is matched
328
- against normalised copy, so anything that folds to those words counts as those
329
- words. Allow `no sale` and `"№ SALE"` passes, because U+2116 folds to `no`;
330
- a superscript `ⁿᵒ` does the same. The shopper reads the numero sign as
331
- decoration and the verifier reads it as a word. Keep allowances tight and
332
- written the way a shop really writes them.
333
- - **A letter glued to the end of a phrase.** `"FREE SHIPPINGᵃ see terms"` is not
334
- reported. The superscript folds to an `a`, which glues to `shipping`, and the
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.
330
+ - **A character that folds into your allowance.** Allowances match the copy
331
+ 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.
336
+ - **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
335
338
  word-gap rule that stops `cheap` matching `cheapskate` then treats the whole
336
- thing as a longer word. It holds for every entry on the list, and a footnote
339
+ thing as a longer word. This holds for every entry on the list. A footnote
337
340
  marker is exactly how a real shop writes one.
338
341
 
339
342
  ### Preconditions the package cannot check for you
340
343
 
341
344
  - **The text you check has to be the text you render.** `verify` sees a string.
342
- If anything downstream decodes it — an HTML sink turning `½` into ½, or
343
- `2` into a 2 — then the string that was checked and the string on the page
344
- are different documents, and the guarantee covers the first one.
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.
345
348
  - **The font has to agree with Unicode about what renders as nothing.** The
346
- layer drops what Unicode marks as taking no room. Where a font gives one of
347
- those a visible glyph the two disagree, and U+3164 HANGUL FILLER is the one
348
- people really paste as a blank: the layer reads `"1<U+3164>3"` as 13, and a
349
- font that widens it shows the shopper `1 3`.
350
- - **Everything that is not a number or a phrase.** A claim about materials,
351
- compatibility, a certification, a warranty condition or a person is entirely
352
- out of scope.
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,
354
+ compatibility, a certification, a warranty condition or a person is out of
355
+ scope.
353
356
 
354
357
  ## What it over-rejects
355
358
 
356
- The guarantee errs toward rejection, and that has a bill:
357
-
358
- - **Every incidental number has to be in `values`.** A VAT rate, a size, a care
359
- temperature, a spec number, a support phone number, a founding year, the
360
- denominator of a rating scale. None of them are claims, all of them are
361
- digits, and copy mentioning one you did not supply is rejected.
362
- - **A three-digit tail is always grouping.** `1.299 kg` reads as 1299, so a
363
- genuine three-decimal number — a weight, or a price in a currency with three
364
- decimal places — cannot be written with a dot or a comma. Supply it, and write
365
- it, the same way.
366
- - **Numeric characters that are not digits are refused outright.** `2 m²` and
367
- `½ inch` fail, along with the `②` and `Ⅲ` the rule is there to stop.
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.
370
+ - **Numeric characters that aren't digits are refused outright.** `2 m²` and
371
+ `½ inch` fail.
368
372
  - **A magnitude letter glued to a digit is refused.** `4K display` fails.
369
- `4 K display` and `1kg` do not, because a letter followed by another letter is
373
+ `4 K display` and `1kg` don't, because a letter followed by another letter is
370
374
  a unit, not a multiplier.
371
- - **A run no locale reads as a number needs its exact spelling.** `24.12.2026`
372
- is supported by a fact of `'24.12.2026'` and by nothing else.
373
- - **Scientific notation in the copy is unsupported by construction.** A fact of
374
- `1e-7` is laid out as `0.0000001`, and the text `"1e-7 g"` reads as the two
375
- numerals 1 and 7, so the two never meet. `"0.0000001 g"` passes. This is the
376
- bill for laying facts out positionally, and a lab or bulk-goods shop that
377
- prints the value the way its own data prints it pays it.
375
+ - **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.
378
380
 
379
381
  ## Licence
380
382
 
@@ -1 +1 @@
1
- {"version":3,"file":"phrases.d.ts","sourceRoot":"","sources":["../src/phrases.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,cAAc,EAAE,SAAS,MAAM,EA2H3C,CAAC;AAgEF,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,UAAQ,GAAG,MAAM,CAKnE;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,UAAQ,GAAG,MAAM,CAMtE;AAQD,MAAM,WAAW,IAAI;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAYnD;AAED,wBAAgB,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,EAAE,CA2BhE"}
1
+ {"version":3,"file":"phrases.d.ts","sourceRoot":"","sources":["../src/phrases.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,cAAc,EAAE,SAAS,MAAM,EA8H3C,CAAC;AAsEF,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,UAAQ,GAAG,MAAM,CAOnE;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,UAAQ,GAAG,MAAM,CAMtE;AAQD,MAAM,WAAW,IAAI;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAYnD;AAED,wBAAgB,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,EAAE,CA2BhE"}
package/dist/phrases.js CHANGED
@@ -74,6 +74,8 @@ export const BANNED_PHRASES = [
74
74
  'most popular',
75
75
  'popular pick',
76
76
  'top pick',
77
+ 'strongest all round pick',
78
+ 'strongest all around pick',
77
79
  'top rated',
78
80
  'highly rated',
79
81
  'highest rated',
@@ -90,6 +92,7 @@ export const BANNED_PHRASES = [
90
92
  'people are viewing',
91
93
  'others are buying',
92
94
  'people are buying',
95
+ 'other shoppers',
93
96
  'in other carts',
94
97
  'in other baskets',
95
98
  'money back',
@@ -169,12 +172,17 @@ const CAPITALS = {
169
172
  Ϻ: 'm',
170
173
  Ϝ: 'f',
171
174
  Ԍ: 'g',
175
+ Ѡ: 'w',
176
+ Ϙ: 'q',
177
+ Ϗ: 'k',
178
+ Ͳ: 't',
172
179
  };
173
180
  const BEFORE_NFKC = {
174
181
  ϲ: 'c',
175
182
  Ϲ: 'c',
176
183
  };
177
184
  const LINE_BREAK = /[\n\v\f\r\u0085\u2028\u2029]/;
185
+ const GREEK_MARKS = /(\p{Script=Greek})[\p{Mn}\p{Me}]+/gu;
178
186
  function foldChars(text, table) {
179
187
  let folded = '';
180
188
  for (const char of text)
@@ -184,7 +192,7 @@ function foldChars(text, table) {
184
192
  export function foldLookalikes(text, byLook = false) {
185
193
  const compatible = foldChars(stripInvisible(text), BEFORE_NFKC).normalize('NFKC');
186
194
  const cased = byLook ? foldChars(compatible, CAPITALS) : compatible;
187
- const lowered = stripMarks(cased.toLowerCase().normalize('NFC'));
195
+ const lowered = stripMarks(cased.toLowerCase().normalize('NFD').replace(GREEK_MARKS, '$1').normalize('NFC'));
188
196
  return foldChars(lowered, CONFUSABLES);
189
197
  }
190
198
  export function normalisePhrasing(text, byLook = false) {
@@ -1 +1 @@
1
- {"version":3,"file":"phrases.js","sourceRoot":"","sources":["../src/phrases.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"phrases.js","sourceRoot":"","sources":["../src/phrases.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,KAAK,GAAG,UAAU,GAAG,SAAS,CAAC;AAE3C,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,KAAK,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,OAAO,GAAG,aAAa,CAAC;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,OAAO,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,WAAW,CAAC;IACtB,OAAO,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,KAAK;IACpB,MAAM,EAAE,SAAS,CAAC,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;IAC9C,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACpC;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,YAAY,CAAC;CACtB;AAED,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,YAAY,EAAE,WAAW,CAAC;CAC3B;AAkJD,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,GAAG,YAAY,CAE/D;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,KAAK,EAAE,KAAK,GAAG,WAAW,CAgBtF"}
1
+ {"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,KAAK,GAAG,UAAU,GAAG,SAAS,CAAC;AAE3C,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,KAAK,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,OAAO,GAAG,aAAa,CAAC;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,OAAO,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,WAAW,CAAC;IACtB,OAAO,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,KAAK;IACpB,MAAM,EAAE,SAAS,CAAC,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;IAC9C,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACpC;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,YAAY,CAAC;CACtB;AAED,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,YAAY,EAAE,WAAW,CAAC;CAC3B;AAmJD,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,GAAG,YAAY,CAE/D;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,KAAK,EAAE,KAAK,GAAG,WAAW,CAgBtF"}
package/dist/verify.js CHANGED
@@ -78,7 +78,7 @@ function isInside(spans, hit) {
78
78
  }
79
79
  return false;
80
80
  }
81
- function forgivenInEitherReading(scans, allowed) {
81
+ function forgivenInAnyReading(scans, allowed) {
82
82
  const forgiven = [];
83
83
  for (const scan of scans) {
84
84
  for (const phrase of allowed) {
@@ -101,10 +101,12 @@ function isCaught(scans, phrase, forgiven) {
101
101
  }
102
102
  function checkWording(text, prepared) {
103
103
  const { banned, allowed } = prepared;
104
- const plain = normalisePhrasing(text);
105
- const byLook = normalisePhrasing(text, true);
106
- const scans = byLook === plain ? [indexPhrasing(plain)] : [indexPhrasing(plain), indexPhrasing(byLook)];
107
- const forgiven = forgivenInEitherReading(scans, allowed);
104
+ const readings = new Set([normalisePhrasing(text), normalisePhrasing(text, true)]);
105
+ if (text.includes('\u04C0')) {
106
+ readings.add(normalisePhrasing(text.replaceAll('\u04C0', '\u04CF'), true));
107
+ }
108
+ const scans = [...readings].map((reading) => indexPhrasing(reading));
109
+ const forgiven = forgivenInAnyReading(scans, allowed);
108
110
  const findings = [];
109
111
  for (const phrase of banned) {
110
112
  if (!isCaught(scans, phrase, forgiven))
@@ -1 +1 @@
1
- {"version":3,"file":"verify.js","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"verify.js","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rudra-js/attested",
3
- "version": "0.8.1",
3
+ "version": "0.8.3",
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",
package/src/phrases.ts CHANGED
@@ -75,6 +75,8 @@ export const BANNED_PHRASES: readonly string[] = [
75
75
  'most popular',
76
76
  'popular pick',
77
77
  'top pick',
78
+ 'strongest all round pick',
79
+ 'strongest all around pick',
78
80
  'top rated',
79
81
  'highly rated',
80
82
  'highest rated',
@@ -91,6 +93,7 @@ export const BANNED_PHRASES: readonly string[] = [
91
93
  'people are viewing',
92
94
  'others are buying',
93
95
  'people are buying',
96
+ 'other shoppers',
94
97
  'in other carts',
95
98
  'in other baskets',
96
99
  'money back',
@@ -172,6 +175,10 @@ const CAPITALS: Record<string, string> = {
172
175
  Ϻ: 'm',
173
176
  Ϝ: 'f',
174
177
  Ԍ: 'g',
178
+ Ѡ: 'w',
179
+ Ϙ: 'q',
180
+ Ϗ: 'k',
181
+ Ͳ: 't',
175
182
  };
176
183
 
177
184
  const BEFORE_NFKC: Record<string, string> = {
@@ -181,6 +188,8 @@ const BEFORE_NFKC: Record<string, string> = {
181
188
 
182
189
  const LINE_BREAK = /[\n\v\f\r\u0085\u2028\u2029]/;
183
190
 
191
+ const GREEK_MARKS = /(\p{Script=Greek})[\p{Mn}\p{Me}]+/gu;
192
+
184
193
  function foldChars(text: string, table: Record<string, string>): string {
185
194
  let folded = '';
186
195
  for (const char of text) folded += table[char] ?? char;
@@ -190,7 +199,9 @@ function foldChars(text: string, table: Record<string, string>): string {
190
199
  export function foldLookalikes(text: string, byLook = false): string {
191
200
  const compatible = foldChars(stripInvisible(text), BEFORE_NFKC).normalize('NFKC');
192
201
  const cased = byLook ? foldChars(compatible, CAPITALS) : compatible;
193
- const lowered = stripMarks(cased.toLowerCase().normalize('NFC'));
202
+ const lowered = stripMarks(
203
+ cased.toLowerCase().normalize('NFD').replace(GREEK_MARKS, '$1').normalize('NFC'),
204
+ );
194
205
  return foldChars(lowered, CONFUSABLES);
195
206
  }
196
207
 
package/src/verify.ts CHANGED
@@ -135,7 +135,7 @@ function isInside(spans: Span[], hit: Span): boolean {
135
135
  return false;
136
136
  }
137
137
 
138
- function forgivenInEitherReading(scans: Indexed[], allowed: readonly string[]): Span[] {
138
+ function forgivenInAnyReading(scans: Indexed[], allowed: readonly string[]): Span[] {
139
139
  const forgiven: Span[] = [];
140
140
  for (const scan of scans) {
141
141
  for (const phrase of allowed) {
@@ -158,12 +158,13 @@ function isCaught(scans: Indexed[], phrase: string, forgiven: Span[]): boolean {
158
158
 
159
159
  function checkWording(text: string, prepared: PreparedFacts): LayerReport {
160
160
  const { banned, allowed } = prepared;
161
- const plain = normalisePhrasing(text);
162
- const byLook = normalisePhrasing(text, true);
163
- const scans =
164
- byLook === plain ? [indexPhrasing(plain)] : [indexPhrasing(plain), indexPhrasing(byLook)];
161
+ const readings = new Set([normalisePhrasing(text), normalisePhrasing(text, true)]);
162
+ if (text.includes('\u04C0')) {
163
+ readings.add(normalisePhrasing(text.replaceAll('\u04C0', '\u04CF'), true));
164
+ }
165
+ const scans = [...readings].map((reading) => indexPhrasing(reading));
165
166
 
166
- const forgiven = forgivenInEitherReading(scans, allowed);
167
+ const forgiven = forgivenInAnyReading(scans, allowed);
167
168
 
168
169
  const findings: Finding[] = [];
169
170
  for (const phrase of banned) {