@rudra-js/attested 0.6.0 → 0.7.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/dist/hidden.d.ts.map +1 -1
- package/dist/hidden.js +0 -12
- package/dist/hidden.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/numerals.d.ts +0 -4
- package/dist/numerals.d.ts.map +1 -1
- package/dist/numerals.js +0 -41
- package/dist/numerals.js.map +1 -1
- package/dist/phrases.d.ts +0 -14
- package/dist/phrases.d.ts.map +1 -1
- package/dist/phrases.js +0 -24
- package/dist/phrases.js.map +1 -1
- package/dist/verify.d.ts +0 -16
- package/dist/verify.d.ts.map +1 -1
- package/dist/verify.js +0 -15
- package/dist/verify.js.map +1 -1
- package/package.json +2 -2
- package/src/hidden.ts +0 -13
- package/src/index.ts +0 -2
- package/src/numerals.ts +8 -45
- package/src/phrases.ts +5 -28
- package/src/verify.ts +9 -31
package/dist/hidden.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hidden.d.ts","sourceRoot":"","sources":["../src/hidden.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"hidden.d.ts","sourceRoot":"","sources":["../src/hidden.ts"],"names":[],"mappings":"AAMA,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE/C"}
|
package/dist/hidden.js
CHANGED
|
@@ -1,17 +1,5 @@
|
|
|
1
|
-
// One definition for both layers. It lived in two files, and that is why widening it
|
|
2
|
-
// for the quantity layer left the wording layer open twice running.
|
|
3
|
-
/**
|
|
4
|
-
* Characters that render as nothing: format characters, default-ignorable code points
|
|
5
|
-
* and the controls. Cf and Default_Ignorable each hold characters the other does not.
|
|
6
|
-
*/
|
|
7
1
|
const INVISIBLE = /[\p{Cf}\p{Default_Ignorable_Code_Point}\p{Cc}]/gu;
|
|
8
|
-
/** The controls that do take room. Deleting these would read `Only 2\n3 left` as 23. */
|
|
9
2
|
const SPACING = /[\t\n\v\f\r\u0085]/;
|
|
10
|
-
/**
|
|
11
|
-
* Marks that hang on the character before them instead of taking a column of their own,
|
|
12
|
-
* so `4<U+0305>9` reads as 49. Spacing marks (Mc) are not here: a Devanagari matra is
|
|
13
|
-
* a visible gap between two digits and separates them for a reader.
|
|
14
|
-
*/
|
|
15
3
|
const MARKS = /[\p{Mn}\p{Me}]/gu;
|
|
16
4
|
export function stripInvisible(text) {
|
|
17
5
|
return text.replace(INVISIBLE, (char) => (SPACING.test(char) ? char : ''));
|
package/dist/hidden.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hidden.js","sourceRoot":"","sources":["../src/hidden.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"hidden.js","sourceRoot":"","sources":["../src/hidden.ts"],"names":[],"mappings":"AAAA,MAAM,SAAS,GAAG,kDAAkD,CAAC;AAErE,MAAM,OAAO,GAAG,oBAAoB,CAAC;AAErC,MAAM,KAAK,GAAG,kBAAkB,CAAC;AAEjC,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,OAAO,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AAC7E,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,OAAO,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;AACjC,CAAC"}
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,MAAM,EACN,YAAY,EACZ,KAAK,WAAW,EAChB,KAAK,KAAK,EACV,KAAK,WAAW,EAChB,KAAK,OAAO,EACZ,KAAK,KAAK,EACV,KAAK,WAAW,EAChB,KAAK,YAAY,GAClB,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC"}
|
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,MAAM,EACN,YAAY,GAQb,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC"}
|
package/dist/numerals.d.ts
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
export interface Numeral {
|
|
2
|
-
/** The run exactly as it appears in the text. */
|
|
3
2
|
token: string;
|
|
4
|
-
/** Every reading of that run, canonical. Empty when the layer cannot read the run. */
|
|
5
3
|
forms: string[];
|
|
6
|
-
/** `digits` is a run this layer reads. The other two it cannot read at all. */
|
|
7
4
|
kind: 'digits' | 'magnitude' | 'other-numeral';
|
|
8
5
|
}
|
|
9
6
|
export declare function numeralsIn(text: string): Numeral[];
|
|
10
|
-
/** Every reading of every numeral the host stands behind. */
|
|
11
7
|
export declare function supportedValues(values: readonly (string | number | bigint)[]): Set<string>;
|
|
12
8
|
//# sourceMappingURL=numerals.d.ts.map
|
package/dist/numerals.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"numerals.d.ts","sourceRoot":"","sources":["../src/numerals.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"numerals.d.ts","sourceRoot":"","sources":["../src/numerals.ts"],"names":[],"mappings":"AAuBA,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;IAEd,KAAK,EAAE,MAAM,EAAE,CAAC;IAEhB,IAAI,EAAE,QAAQ,GAAG,WAAW,GAAG,eAAe,CAAC;CAChD;AAoID,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,EAAE,CAIlD;AAoCD,wBAAgB,eAAe,CAAC,MAAM,EAAE,SAAS,CAAC,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC,EAAE,GAAG,GAAG,CAAC,MAAM,CAAC,CAQ1F"}
|
package/dist/numerals.js
CHANGED
|
@@ -1,17 +1,10 @@
|
|
|
1
|
-
// Everything here is string work. No float ever holds a value, so a twenty-digit
|
|
2
|
-
// order number compares exactly.
|
|
3
1
|
import { stripInvisible, stripMarks } from './hidden.js';
|
|
4
|
-
/** A dot or a comma. Either mark is a decimal point in one locale and grouping in another. */
|
|
5
2
|
const AMBIGUOUS = '.,';
|
|
6
|
-
/** The Arabic decimal separator, which is never grouping. */
|
|
7
3
|
const DECIMAL_ONLY = '\u066b';
|
|
8
|
-
/** Marks that are only ever grouping: Arabic thousands, the space family, the apostrophe. */
|
|
9
4
|
const GROUPING_ONLY = '\u066c\u0020\u00a0\u2007\u2008\u2009\u202f\u0027\u2019';
|
|
10
5
|
const SEPARATORS = AMBIGUOUS + DECIMAL_ONLY + GROUPING_ONLY;
|
|
11
6
|
const RUN = `\\p{Nd}+(?:[${SEPARATORS}]\\p{Nd}+)*`;
|
|
12
|
-
/** Marks that multiply the digits in front of them: 万 億 兆, and the k/M/B of English copy. */
|
|
13
7
|
const MAGNITUDE = '(?:[十百千万萬億亿兆]|[kKMB](?!\\p{L}))';
|
|
14
|
-
/** Numeric characters that are not decimal digits: ½ ² ② Ⅲ. */
|
|
15
8
|
const OTHER_NUMERAL = '[\\p{No}\\p{Nl}]+';
|
|
16
9
|
const SCAN = new RegExp(`${RUN}${MAGNITUDE}|${RUN}|${OTHER_NUMERAL}`, 'gu');
|
|
17
10
|
const LOOSE = new RegExp(`[${GROUPING_ONLY}]`, 'u');
|
|
@@ -19,12 +12,6 @@ const IS_DIGIT = /^\p{Nd}$/u;
|
|
|
19
12
|
const STARTS_DIGIT = /^\p{Nd}/u;
|
|
20
13
|
const ENDS_DIGIT = /\p{Nd}$/u;
|
|
21
14
|
const zeros = new Map();
|
|
22
|
-
/**
|
|
23
|
-
* Unicode writes decimal digits in complete sets of ten, and adjacent sets abut, so
|
|
24
|
-
* the start of a maximal run of digit code points is a zero and the offset into that
|
|
25
|
-
* run, modulo ten, is the value. Walking back without that modulo reads every digit
|
|
26
|
-
* of the second and later sets in the maths block as a nine.
|
|
27
|
-
*/
|
|
28
15
|
function digitValue(char) {
|
|
29
16
|
const code = char.codePointAt(0) ?? 0;
|
|
30
17
|
let zero = zeros.get(code);
|
|
@@ -43,7 +30,6 @@ function toAscii(token) {
|
|
|
43
30
|
}
|
|
44
31
|
return out;
|
|
45
32
|
}
|
|
46
|
-
/** Strips the zeros that carry no value, so 39.00 and 39 are one string. */
|
|
47
33
|
function canonical(whole, fraction) {
|
|
48
34
|
let start = 0;
|
|
49
35
|
while (start < whole.length - 1 && whole[start] === '0')
|
|
@@ -55,7 +41,6 @@ function canonical(whole, fraction) {
|
|
|
55
41
|
const tail = fraction.slice(0, end);
|
|
56
42
|
return tail.length === 0 ? head : `${head}.${tail}`;
|
|
57
43
|
}
|
|
58
|
-
/** Whether these runs read as one grouped whole number: 12,345,678 or 1,29,999. */
|
|
59
44
|
function isGrouping(groups) {
|
|
60
45
|
if (groups.length === 1)
|
|
61
46
|
return true;
|
|
@@ -73,12 +58,10 @@ function isGrouping(groups) {
|
|
|
73
58
|
const lead = (groups[0] ?? '').length;
|
|
74
59
|
if (three)
|
|
75
60
|
return lead >= 1 && lead <= 3;
|
|
76
|
-
// 1,29,999 — the Indian grouping, two digits at a time above the last three.
|
|
77
61
|
if (two)
|
|
78
62
|
return lead >= 1 && lead <= 2;
|
|
79
63
|
return false;
|
|
80
64
|
}
|
|
81
|
-
/** Whether every mark is the same one, and every one of them can be grouping. */
|
|
82
65
|
function allGrouping(separators) {
|
|
83
66
|
for (const separator of separators) {
|
|
84
67
|
if (separator !== separators[0])
|
|
@@ -88,7 +71,6 @@ function allGrouping(separators) {
|
|
|
88
71
|
}
|
|
89
72
|
return true;
|
|
90
73
|
}
|
|
91
|
-
/** `1,299` is 1299 in London and in Berlin. `1234,567` is a decimal in both. */
|
|
92
74
|
function formsOf(token) {
|
|
93
75
|
const ascii = toAscii(token);
|
|
94
76
|
const groups = [];
|
|
@@ -114,8 +96,6 @@ function formsOf(token) {
|
|
|
114
96
|
const leading = separators.slice(0, -1);
|
|
115
97
|
const point = separators[separators.length - 1] ?? '';
|
|
116
98
|
const fraction = groups[groups.length - 1] ?? '';
|
|
117
|
-
// One dot or comma with exactly three digits behind it is grouping wherever it is
|
|
118
|
-
// written, so `4.800` is four thousand eight hundred and never the rating 4.8.
|
|
119
99
|
const groupingWins = forms.length > 0 && leading.length === 0 && fraction.length === 3;
|
|
120
100
|
if (!groupingWins &&
|
|
121
101
|
!GROUPING_ONLY.includes(point) &&
|
|
@@ -140,7 +120,6 @@ function collect(found, token) {
|
|
|
140
120
|
found.push({ token, forms, kind: 'digits' });
|
|
141
121
|
return;
|
|
142
122
|
}
|
|
143
|
-
// `8 10 12` is three numbers, not one badly grouped one.
|
|
144
123
|
if (LOOSE.test(token)) {
|
|
145
124
|
for (const part of token.split(LOOSE)) {
|
|
146
125
|
if (part.length > 0)
|
|
@@ -148,9 +127,6 @@ function collect(found, token) {
|
|
|
148
127
|
}
|
|
149
128
|
return;
|
|
150
129
|
}
|
|
151
|
-
// No locale reads `24.12.2026` as a number, so only a fact written the same way
|
|
152
|
-
// stands behind it. The raw run carries a mark, and a reading never does, so it
|
|
153
|
-
// can never be mistaken for one.
|
|
154
130
|
found.push({ token, forms: [toAscii(token)], kind: 'digits' });
|
|
155
131
|
}
|
|
156
132
|
export function numeralsIn(text) {
|
|
@@ -159,24 +135,13 @@ export function numeralsIn(text) {
|
|
|
159
135
|
collect(found, match[0]);
|
|
160
136
|
return found;
|
|
161
137
|
}
|
|
162
|
-
/** A whole fact in exponent notation and nothing else, so expanding it swallows no prose. */
|
|
163
138
|
const BARE_EXPONENT = /^[+-]?\d+(?:\.\d+)?[eE][+-]?\d+$/;
|
|
164
|
-
/**
|
|
165
|
-
* The widest digit run a fact may lay out to. Every finite JS number fits — 5e-324 is
|
|
166
|
-
* the longest, at 326 characters — and nothing a shop sells is wider than that.
|
|
167
|
-
*/
|
|
168
139
|
const DIGIT_LIMIT = 1000;
|
|
169
|
-
/**
|
|
170
|
-
* `String(1e21)` is `1e+21`, which reads as the two numerals 1 and 21. Lay the same digits
|
|
171
|
-
* out in place. Empty past the limit: `1e2000000000` is twelve characters and no product
|
|
172
|
-
* fact, and laying it out builds a run long enough to take the process down with it.
|
|
173
|
-
*/
|
|
174
140
|
function positional(written) {
|
|
175
141
|
const marker = written.search(/[eE]/);
|
|
176
142
|
if (marker < 0)
|
|
177
143
|
return written;
|
|
178
144
|
const power = Number(written.slice(marker + 1));
|
|
179
|
-
// The sign never reaches a numeral, so dropping it here mints no digit the host lacks.
|
|
180
145
|
const body = written.slice(0, marker).replace(/^[+-]/, '');
|
|
181
146
|
const point = body.indexOf('.');
|
|
182
147
|
const digits = point < 0 ? body : body.slice(0, point) + body.slice(point + 1);
|
|
@@ -193,11 +158,6 @@ function positional(written) {
|
|
|
193
158
|
}
|
|
194
159
|
return `${digits.slice(0, place)}.${digits.slice(place)}`;
|
|
195
160
|
}
|
|
196
|
-
/**
|
|
197
|
-
* A string fact is read as the host typed it, except when the whole of it is a number
|
|
198
|
-
* in exponent notation: `String`, `JSON.stringify`, a CSV export and an API that writes
|
|
199
|
-
* 64-bit values as strings all produce that, and none of them is the host choosing digits.
|
|
200
|
-
*/
|
|
201
161
|
function digitsOf(value) {
|
|
202
162
|
if (typeof value === 'number')
|
|
203
163
|
return positional(String(value));
|
|
@@ -206,7 +166,6 @@ function digitsOf(value) {
|
|
|
206
166
|
const trimmed = value.trim();
|
|
207
167
|
return BARE_EXPONENT.test(trimmed) ? positional(trimmed) : value;
|
|
208
168
|
}
|
|
209
|
-
/** Every reading of every numeral the host stands behind. */
|
|
210
169
|
export function supportedValues(values) {
|
|
211
170
|
const supported = new Set();
|
|
212
171
|
for (const value of values) {
|
package/dist/numerals.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"numerals.js","sourceRoot":"","sources":["../src/numerals.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"numerals.js","sourceRoot":"","sources":["../src/numerals.ts"],"names":[],"mappings":"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"}
|
package/dist/phrases.d.ts
CHANGED
|
@@ -1,28 +1,14 @@
|
|
|
1
|
-
/** Claims with no value behind them. Stored in the form `normalisePhrasing` produces. */
|
|
2
1
|
export declare const BANNED_PHRASES: readonly string[];
|
|
3
|
-
/** One shape for text and phrases, so a hyphen or a stray mark cannot hide a claim. */
|
|
4
2
|
export declare function normalisePhrasing(text: string): string;
|
|
5
3
|
export interface Span {
|
|
6
4
|
start: number;
|
|
7
|
-
/** The last character of the match, not one past it. */
|
|
8
5
|
end: number;
|
|
9
6
|
}
|
|
10
7
|
export interface Indexed {
|
|
11
|
-
/** The text as it was handed in, which is what spans index and the word gap reads. */
|
|
12
8
|
text: string;
|
|
13
|
-
/** The same characters with the gaps dropped, which is what matching runs against. */
|
|
14
9
|
haystack: string;
|
|
15
|
-
/** Where each character of `haystack` really sat in `text`. */
|
|
16
10
|
spots: number[];
|
|
17
11
|
}
|
|
18
|
-
/**
|
|
19
|
-
* Build this once and read every phrase off it. The text must already be normalised.
|
|
20
|
-
*
|
|
21
|
-
* Matching runs with every space dropped, because 送料 無料 is 送料無料 to anyone and a
|
|
22
|
-
* space is free. `spots` carries each character back to where it really sat, so the
|
|
23
|
-
* word-gap guard still reads the neighbours the shopper sees.
|
|
24
|
-
*/
|
|
25
12
|
export declare function indexPhrasing(text: string): Indexed;
|
|
26
|
-
/** Spans index the text the index was built from. The phrase must already be normalised. */
|
|
27
13
|
export declare function spansIn(indexed: Indexed, phrase: string): Span[];
|
|
28
14
|
//# sourceMappingURL=phrases.d.ts.map
|
package/dist/phrases.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"phrases.d.ts","sourceRoot":"","sources":["../src/phrases.ts"],"names":[],"mappings":"AAEA,
|
|
1
|
+
{"version":3,"file":"phrases.d.ts","sourceRoot":"","sources":["../src/phrases.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,cAAc,EAAE,SAAS,MAAM,EAkF3C,CAAC;AAoCF,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAWtD;AAQD,MAAM,WAAW,IAAI;IACnB,KAAK,EAAE,MAAM,CAAC;IAEd,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IAEb,QAAQ,EAAE,MAAM,CAAC;IAEjB,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;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
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { stripInvisible, stripMarks } from './hidden.js';
|
|
2
|
-
/** Claims with no value behind them. Stored in the form `normalisePhrasing` produces. */
|
|
3
2
|
export const BANNED_PHRASES = [
|
|
4
3
|
'cheap',
|
|
5
4
|
'cheaper',
|
|
@@ -83,11 +82,6 @@ export const BANNED_PHRASES = [
|
|
|
83
82
|
'customer favorite',
|
|
84
83
|
'money back',
|
|
85
84
|
];
|
|
86
|
-
/**
|
|
87
|
-
* Letters that render as a Latin letter but are not one. Folded so a Cyrillic о in
|
|
88
|
-
* "in stоck" cannot walk a claim past its own entry on the list. Both the text and
|
|
89
|
-
* the phrase are folded, so a genuine Cyrillic phrase still matches itself.
|
|
90
|
-
*/
|
|
91
85
|
const CONFUSABLES = {
|
|
92
86
|
а: 'a',
|
|
93
87
|
в: 'b',
|
|
@@ -119,18 +113,12 @@ const CONFUSABLES = {
|
|
|
119
113
|
χ: 'x',
|
|
120
114
|
ϲ: 'c',
|
|
121
115
|
};
|
|
122
|
-
/** A run of whitespace holding one of these is a break the shopper sees. */
|
|
123
116
|
const LINE_BREAK = /[\n\v\f\r\u0085\u2028\u2029]/;
|
|
124
|
-
/** One shape for text and phrases, so a hyphen or a stray mark cannot hide a claim. */
|
|
125
117
|
export function normalisePhrasing(text) {
|
|
126
|
-
// Marks come off after composition, so an accented e keeps its accent while an
|
|
127
|
-
// overline, which composes with nothing, does not hide `free` from its own entry.
|
|
128
118
|
const lowered = stripMarks(stripInvisible(text).normalize('NFKC').toLowerCase().normalize('NFC'));
|
|
129
119
|
let folded = '';
|
|
130
120
|
for (const char of lowered)
|
|
131
121
|
folded += CONFUSABLES[char] ?? char;
|
|
132
|
-
// A line break survives as itself. Matching reads straight through it, and only
|
|
133
|
-
// the allowance in verify.ts cares, because it will not forgive across a break.
|
|
134
122
|
return folded
|
|
135
123
|
.replace(/[\u2018\u2019]/g, "'")
|
|
136
124
|
.replace(/[-_\u2010-\u2015]+/g, ' ')
|
|
@@ -141,19 +129,11 @@ const ASCII_WORD = /[a-z0-9]/;
|
|
|
141
129
|
function isGlued(char) {
|
|
142
130
|
return char !== undefined && ASCII_WORD.test(char);
|
|
143
131
|
}
|
|
144
|
-
/**
|
|
145
|
-
* Build this once and read every phrase off it. The text must already be normalised.
|
|
146
|
-
*
|
|
147
|
-
* Matching runs with every space dropped, because 送料 無料 is 送料無料 to anyone and a
|
|
148
|
-
* space is free. `spots` carries each character back to where it really sat, so the
|
|
149
|
-
* word-gap guard still reads the neighbours the shopper sees.
|
|
150
|
-
*/
|
|
151
132
|
export function indexPhrasing(text) {
|
|
152
133
|
const tight = [];
|
|
153
134
|
const spots = [];
|
|
154
135
|
for (let index = 0; index < text.length; index += 1) {
|
|
155
136
|
const char = text[index] ?? '';
|
|
156
|
-
// A line break reads like a space here, so a claim split over two lines is caught.
|
|
157
137
|
if (char === ' ' || char === '\n')
|
|
158
138
|
continue;
|
|
159
139
|
tight.push(char);
|
|
@@ -161,9 +141,6 @@ export function indexPhrasing(text) {
|
|
|
161
141
|
}
|
|
162
142
|
return { text, haystack: tight.join(''), spots };
|
|
163
143
|
}
|
|
164
|
-
/** Spans index the text the index was built from. The phrase must already be normalised. */
|
|
165
|
-
// The word-gap rule only guards an ASCII edge, so `cheap` misses `cheapskate`
|
|
166
|
-
// while a Japanese phrase — which has no word gaps to find — still matches.
|
|
167
144
|
export function spansIn(indexed, phrase) {
|
|
168
145
|
const { text, haystack, spots } = indexed;
|
|
169
146
|
const spans = [];
|
|
@@ -180,7 +157,6 @@ export function spansIn(indexed, phrase) {
|
|
|
180
157
|
const start = spots[at] ?? 0;
|
|
181
158
|
const end = spots[at + needle.length - 1] ?? 0;
|
|
182
159
|
const next = text[end + 1];
|
|
183
|
-
// A trailing s is a plural, not a different word: `discount` catches `discounts`.
|
|
184
160
|
const plural = next === 's' && !isGlued(text[end + 2]);
|
|
185
161
|
const before = guardStart && isGlued(text[start - 1]);
|
|
186
162
|
const after = guardEnd && isGlued(next) && !plural;
|
package/dist/phrases.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"phrases.js","sourceRoot":"","sources":["../src/phrases.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzD,
|
|
1
|
+
{"version":3,"file":"phrases.js","sourceRoot":"","sources":["../src/phrases.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzD,MAAM,CAAC,MAAM,cAAc,GAAsB;IAC/C,OAAO;IACP,SAAS;IACT,UAAU;IACV,YAAY;IACZ,SAAS;IACT,aAAa;IACb,YAAY;IACZ,WAAW;IACX,aAAa;IACb,cAAc;IACd,YAAY;IACZ,YAAY;IACZ,eAAe;IACf,cAAc;IACd,WAAW;IACX,eAAe;IACf,MAAM;IACN,aAAa;IACb,YAAY;IACZ,UAAU;IACV,UAAU;IACV,YAAY;IACZ,WAAW;IACX,eAAe;IACf,eAAe;IACf,cAAc;IACd,cAAc;IACd,YAAY;IACZ,eAAe;IACf,UAAU;IACV,UAAU;IACV,oBAAoB;IACpB,kBAAkB;IAClB,oBAAoB;IACpB,aAAa;IACb,UAAU;IACV,cAAc;IACd,WAAW;IACX,eAAe;IACf,aAAa;IACb,aAAa;IACb,eAAe;IACf,WAAW;IACX,UAAU;IACV,aAAa;IACb,iBAAiB;IACjB,iBAAiB;IACjB,cAAc;IACd,YAAY;IACZ,aAAa;IACb,YAAY;IACZ,mBAAmB;IACnB,aAAa;IACb,aAAa;IACb,UAAU;IACV,UAAU;IACV,aAAa;IACb,cAAc;IACd,YAAY;IACZ,WAAW;IACX,OAAO;IACP,UAAU;IACV,aAAa;IACb,YAAY;IACZ,aAAa;IACb,cAAc;IACd,YAAY;IACZ,aAAa;IACb,cAAc;IACd,cAAc;IACd,UAAU;IACV,WAAW;IACX,cAAc;IACd,eAAe;IACf,eAAe;IACf,YAAY;IACZ,WAAW;IACX,aAAa;IACb,oBAAoB;IACpB,mBAAmB;IACnB,YAAY;CACb,CAAC;AAEF,MAAM,WAAW,GAA2B;IAC1C,CAAC,EAAE,GAAG;IACN,CAAC,EAAE,GAAG;IACN,CAAC,EAAE,GAAG;IACN,CAAC,EAAE,GAAG;IACN,CAAC,EAAE,GAAG;IACN,CAAC,EAAE,GAAG;IACN,CAAC,EAAE,GAAG;IACN,CAAC,EAAE,GAAG;IACN,CAAC,EAAE,GAAG;IACN,CAAC,EAAE,GAAG;IACN,CAAC,EAAE,GAAG;IACN,CAAC,EAAE,GAAG;IACN,CAAC,EAAE,GAAG;IACN,CAAC,EAAE,GAAG;IACN,CAAC,EAAE,GAAG;IACN,CAAC,EAAE,GAAG;IACN,CAAC,EAAE,GAAG;IACN,CAAC,EAAE,GAAG;IACN,CAAC,EAAE,GAAG;IACN,CAAC,EAAE,GAAG;IACN,CAAC,EAAE,GAAG;IACN,CAAC,EAAE,GAAG;IACN,CAAC,EAAE,GAAG;IACN,CAAC,EAAE,GAAG;IACN,CAAC,EAAE,GAAG;IACN,CAAC,EAAE,GAAG;IACN,CAAC,EAAE,GAAG;IACN,CAAC,EAAE,GAAG;IACN,CAAC,EAAE,GAAG;CACP,CAAC;AAEF,MAAM,UAAU,GAAG,8BAA8B,CAAC;AAElD,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC5C,MAAM,OAAO,GAAG,UAAU,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,WAAW,EAAE,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;IAElG,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,KAAK,MAAM,IAAI,IAAI,OAAO;QAAE,MAAM,IAAI,WAAW,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC;IAEhE,OAAO,MAAM;SACV,OAAO,CAAC,iBAAiB,EAAE,GAAG,CAAC;SAC/B,OAAO,CAAC,qBAAqB,EAAE,GAAG,CAAC;SACnC,OAAO,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;SAC7D,IAAI,EAAE,CAAC;AACZ,CAAC;AAED,MAAM,UAAU,GAAG,UAAU,CAAC;AAE9B,SAAS,OAAO,CAAC,IAAwB;IACvC,OAAO,IAAI,KAAK,SAAS,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACrD,CAAC;AAgBD,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC,MAAM,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;QACpD,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;QAE/B,IAAI,IAAI,KAAK,GAAG,IAAI,IAAI,KAAK,IAAI;YAAE,SAAS;QAC5C,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACjB,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACpB,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC;AACnD,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,OAAgB,EAAE,MAAc;IACtD,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC;IAE1C,MAAM,KAAK,GAAW,EAAE,CAAC;IACzB,MAAM,MAAM,GAAG,MAAM,CAAC,UAAU,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,UAAU,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IAC/D,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAEtC,MAAM,UAAU,GAAG,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IACpD,MAAM,QAAQ,GAAG,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IAElE,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,SAAS,CAAC;QACR,MAAM,EAAE,GAAG,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QAC1C,IAAI,EAAE,KAAK,CAAC,CAAC;YAAE,OAAO,KAAK,CAAC;QAE5B,MAAM,KAAK,GAAG,KAAK,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;QAC7B,MAAM,GAAG,GAAG,KAAK,CAAC,EAAE,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC;QAC/C,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC;QAE3B,MAAM,MAAM,GAAG,IAAI,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;QAEvD,MAAM,MAAM,GAAG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC;QACtD,MAAM,KAAK,GAAG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC;QACnD,IAAI,CAAC,MAAM,IAAI,CAAC,KAAK;YAAE,KAAK,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;QAElD,IAAI,GAAG,EAAE,GAAG,CAAC,CAAC;IAChB,CAAC;AACH,CAAC"}
|
package/dist/verify.d.ts
CHANGED
|
@@ -1,35 +1,23 @@
|
|
|
1
1
|
export type Layer = 'quantity' | 'wording';
|
|
2
2
|
export interface Finding {
|
|
3
3
|
layer: Layer;
|
|
4
|
-
/** The numeral as the text wrote it, or the phrase as the denylist holds it. */
|
|
5
4
|
token: string;
|
|
6
|
-
/** Audit evidence. Names the layer and the token. */
|
|
7
5
|
reason: string;
|
|
8
6
|
}
|
|
9
7
|
export interface LayerReport {
|
|
10
8
|
supported: boolean;
|
|
11
|
-
/**
|
|
12
|
-
* What this layer's verdict is worth. `proof` means every numeral in the text is
|
|
13
|
-
* a value the host supplied — not that the sentence around it is true.
|
|
14
|
-
*/
|
|
15
9
|
strength: 'proof' | 'best-effort';
|
|
16
|
-
/** Numerals found, or phrases screened against. Zero means nothing was checked. */
|
|
17
10
|
checked: number;
|
|
18
|
-
/** One per distinct token that failed. */
|
|
19
11
|
findings: Finding[];
|
|
20
12
|
}
|
|
21
13
|
export interface VerifyResult {
|
|
22
|
-
/** True when both layers are. */
|
|
23
14
|
supported: boolean;
|
|
24
15
|
quantity: LayerReport;
|
|
25
16
|
wording: LayerReport;
|
|
26
17
|
}
|
|
27
18
|
export interface Facts {
|
|
28
|
-
/** In any shape they are written: `39`, `'$1,299.00'`, `'4,8'`. Every numeral in one counts. */
|
|
29
19
|
values: readonly (string | number | bigint)[];
|
|
30
|
-
/** Claims to ban on top of the built-in English list, for the host's own language. */
|
|
31
20
|
bannedPhrases?: readonly string[];
|
|
32
|
-
/** Wording the shop stands behind. A banned claim inside one of these is not reported. */
|
|
33
21
|
allowedPhrases?: readonly string[];
|
|
34
22
|
}
|
|
35
23
|
export interface FieldResult {
|
|
@@ -37,14 +25,10 @@ export interface FieldResult {
|
|
|
37
25
|
result: VerifyResult;
|
|
38
26
|
}
|
|
39
27
|
export interface BatchResult {
|
|
40
|
-
/** True when every field is, and when the fields read as one carry no banned claim. */
|
|
41
28
|
supported: boolean;
|
|
42
29
|
fields: FieldResult[];
|
|
43
|
-
/** The wording layer over every field joined, so a phrase split across two fields still reads. */
|
|
44
30
|
acrossFields: LayerReport;
|
|
45
31
|
}
|
|
46
|
-
/** Whether one piece of model-written text stands on the facts the host supplied. */
|
|
47
32
|
export declare function verify(text: string, facts: Facts): VerifyResult;
|
|
48
|
-
/** The same check over every field of one product, without repeating the facts. */
|
|
49
33
|
export declare function verifyFields(fields: Record<string, string>, facts: Facts): BatchResult;
|
|
50
34
|
//# sourceMappingURL=verify.d.ts.map
|
package/dist/verify.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,KAAK,GAAG,UAAU,GAAG,SAAS,CAAC;AAE3C,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,KAAK,CAAC;IAEb,KAAK,EAAE,MAAM,CAAC;IAEd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,OAAO,CAAC;IAEnB,QAAQ,EAAE,OAAO,GAAG,aAAa,CAAC;IAElC,OAAO,EAAE,MAAM,CAAC;IAEhB,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;IAE9C,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAElC,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;IAEtB,YAAY,EAAE,WAAW,CAAC;CAC3B;AAkID,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,CAmBtF"}
|
package/dist/verify.js
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
import { numeralsIn, supportedValues } from './numerals.js';
|
|
2
2
|
import { BANNED_PHRASES, indexPhrasing, normalisePhrasing, spansIn } from './phrases.js';
|
|
3
|
-
/**
|
|
4
|
-
* One reading per fact list, keyed on the list itself, because core hands `verify` the
|
|
5
|
-
* same array again for every field it screens. Held against a copy of what the list
|
|
6
|
-
* carried: `readonly` on `Facts.values` is a compile-time view of one reference, and a
|
|
7
|
-
* host holding the array can write to it between two calls.
|
|
8
|
-
*/
|
|
9
3
|
const readings = new WeakMap();
|
|
10
4
|
function unchanged(held, values) {
|
|
11
5
|
if (held.length !== values.length)
|
|
@@ -18,7 +12,6 @@ function unchanged(held, values) {
|
|
|
18
12
|
}
|
|
19
13
|
function valuesOf(facts) {
|
|
20
14
|
const values = facts.values;
|
|
21
|
-
// A caller outside TypeScript can pass something with no identity to key on.
|
|
22
15
|
if (!Array.isArray(values))
|
|
23
16
|
return supportedValues(values);
|
|
24
17
|
const read = readings.get(values);
|
|
@@ -90,16 +83,12 @@ function checkWording(text, phrases, allowed) {
|
|
|
90
83
|
const spans = [];
|
|
91
84
|
for (const phrase of allowed) {
|
|
92
85
|
for (const span of spansIn(indexed, phrase)) {
|
|
93
|
-
// The denylist reads through a break and the allowance does not, so both rules
|
|
94
|
-
// err toward reporting. A model that puts the negation in one block and the
|
|
95
|
-
// claim in another has written two things the shopper reads apart.
|
|
96
86
|
if (!normalised.slice(span.start, span.end + 1).includes('\n'))
|
|
97
87
|
spans.push(span);
|
|
98
88
|
}
|
|
99
89
|
}
|
|
100
90
|
const findings = [];
|
|
101
91
|
for (const phrase of phrases) {
|
|
102
|
-
// One hit inside allowed wording is forgiven; the same phrase elsewhere is not.
|
|
103
92
|
let caught = false;
|
|
104
93
|
for (const hit of spansIn(indexed, phrase)) {
|
|
105
94
|
if (!standsBehind(spans, hit))
|
|
@@ -125,11 +114,9 @@ function check(text, supported, phrases, allowed) {
|
|
|
125
114
|
const wording = checkWording(text, phrases, allowed);
|
|
126
115
|
return { supported: quantity.supported && wording.supported, quantity, wording };
|
|
127
116
|
}
|
|
128
|
-
/** Whether one piece of model-written text stands on the facts the host supplied. */
|
|
129
117
|
export function verify(text, facts) {
|
|
130
118
|
return check(text, valuesOf(facts), phrasesOf(facts), allowedOf(facts));
|
|
131
119
|
}
|
|
132
|
-
/** The same check over every field of one product, without repeating the facts. */
|
|
133
120
|
export function verifyFields(fields, facts) {
|
|
134
121
|
const supported = valuesOf(facts);
|
|
135
122
|
const phrases = phrasesOf(facts);
|
|
@@ -145,8 +132,6 @@ export function verifyFields(fields, facts) {
|
|
|
145
132
|
results.push({ field, result });
|
|
146
133
|
written.push(text);
|
|
147
134
|
}
|
|
148
|
-
// A card renders its fields next to each other, so `Free` and `delivery on every
|
|
149
|
-
// order` are one sentence to a shopper even though neither field carries one.
|
|
150
135
|
const acrossFields = checkWording(written.join(' '), phrases, allowed);
|
|
151
136
|
return { supported: allSupported && acrossFields.supported, fields: results, acrossFields };
|
|
152
137
|
}
|
package/dist/verify.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"verify.js","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,eAAe,EAAgB,MAAM,eAAe,CAAC;AAC1E,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,iBAAiB,EAAE,OAAO,EAAa,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"verify.js","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,eAAe,EAAgB,MAAM,eAAe,CAAC;AAC1E,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,iBAAiB,EAAE,OAAO,EAAa,MAAM,cAAc,CAAC;AAgDpG,MAAM,QAAQ,GAAG,IAAI,OAAO,EAAgE,CAAC;AAE7F,SAAS,SAAS,CAAC,IAAwB,EAAE,MAA0B;IACrE,IAAI,IAAI,CAAC,MAAM,KAAK,MAAM,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IAChD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QACxC,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,CAAC,CAAC;YAAE,OAAO,KAAK,CAAC;IAC1C,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,QAAQ,CAAC,KAAY;IAC5B,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;IAE5B,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;QAAE,OAAO,eAAe,CAAC,MAAM,CAAC,CAAC;IAE3D,MAAM,IAAI,GAAG,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAClC,IAAI,IAAI,KAAK,SAAS,IAAI,SAAS,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC;QAAE,OAAO,IAAI,CAAC,SAAS,CAAC;IAE9E,MAAM,SAAS,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;IAC1C,QAAQ,CAAC,GAAG,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,CAAC,GAAG,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC;IACvD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,SAAS,SAAS,CAAC,KAAY;IAC7B,MAAM,OAAO,GAAG,IAAI,GAAG,CAAS,cAAc,CAAC,CAAC;IAEhD,KAAK,MAAM,MAAM,IAAI,KAAK,CAAC,aAAa,IAAI,EAAE,EAAE,CAAC;QAC/C,MAAM,UAAU,GAAG,iBAAiB,CAAC,MAAM,CAAC,CAAC;QAC7C,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;IACrD,CAAC;IACD,OAAO,CAAC,GAAG,OAAO,CAAC,CAAC;AACtB,CAAC;AAED,SAAS,SAAS,CAAC,KAAY;IAC7B,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,KAAK,MAAM,MAAM,IAAI,KAAK,CAAC,cAAc,IAAI,EAAE,EAAE,CAAC;QAChD,MAAM,UAAU,GAAG,iBAAiB,CAAC,MAAM,CAAC,CAAC;QAC7C,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACtD,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,SAAS,SAAS,CAAC,OAAgB;IACjC,IAAI,OAAO,CAAC,IAAI,KAAK,eAAe,EAAE,CAAC;QACrC,OAAO,cAAc,OAAO,CAAC,KAAK,yEAAyE,CAAC;IAC9G,CAAC;IACD,IAAI,OAAO,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;QACjC,OAAO,cAAc,OAAO,CAAC,KAAK,uFAAuF,CAAC;IAC5H,CAAC;IACD,OAAO,cAAc,OAAO,CAAC,KAAK,2CAA2C,CAAC;AAChF,CAAC;AAED,SAAS,eAAe,CAAC,IAAY,EAAE,SAAsB;IAC3D,MAAM,QAAQ,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;IAClC,MAAM,QAAQ,GAAc,EAAE,CAAC;IAC/B,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAU,CAAC;IAEnC,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,IAAI,MAAM,GAAG,KAAK,CAAC;QACnB,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;YACjC,IAAI,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC;gBAAE,MAAM,GAAG,IAAI,CAAC;QACzC,CAAC;QACD,IAAI,MAAM,IAAI,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC;YAAE,SAAS;QAEpD,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QAC5B,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IACzF,CAAC;IAED,OAAO;QACL,SAAS,EAAE,QAAQ,CAAC,MAAM,KAAK,CAAC;QAChC,QAAQ,EAAE,OAAO;QACjB,OAAO,EAAE,QAAQ,CAAC,MAAM;QACxB,QAAQ;KACT,CAAC;AACJ,CAAC;AAED,SAAS,YAAY,CAAC,OAAe,EAAE,GAAS;IAC9C,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;QAC3B,IAAI,IAAI,CAAC,KAAK,IAAI,GAAG,CAAC,KAAK,IAAI,GAAG,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG;YAAE,OAAO,IAAI,CAAC;IAClE,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,YAAY,CAAC,IAAY,EAAE,OAAiB,EAAE,OAAiB;IACtE,MAAM,UAAU,GAAG,iBAAiB,CAAC,IAAI,CAAC,CAAC;IAC3C,MAAM,OAAO,GAAG,aAAa,CAAC,UAAU,CAAC,CAAC;IAE1C,MAAM,KAAK,GAAW,EAAE,CAAC;IACzB,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,OAAO,EAAE,MAAM,CAAC,EAAE,CAAC;YAC5C,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC;gBAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnF,CAAC;IACH,CAAC;IAED,MAAM,QAAQ,GAAc,EAAE,CAAC;IAC/B,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,IAAI,MAAM,GAAG,KAAK,CAAC;QACnB,KAAK,MAAM,GAAG,IAAI,OAAO,CAAC,OAAO,EAAE,MAAM,CAAC,EAAE,CAAC;YAC3C,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,GAAG,CAAC;gBAAE,MAAM,GAAG,IAAI,CAAC;QAC/C,CAAC;QACD,IAAI,CAAC,MAAM;YAAE,SAAS;QAEtB,QAAQ,CAAC,IAAI,CAAC;YACZ,KAAK,EAAE,SAAS;YAChB,KAAK,EAAE,MAAM;YACb,MAAM,EAAE,aAAa,MAAM,uEAAuE;SACnG,CAAC,CAAC;IACL,CAAC;IAED,OAAO;QACL,SAAS,EAAE,QAAQ,CAAC,MAAM,KAAK,CAAC;QAChC,QAAQ,EAAE,aAAa;QACvB,OAAO,EAAE,OAAO,CAAC,MAAM;QACvB,QAAQ;KACT,CAAC;AACJ,CAAC;AAED,SAAS,KAAK,CACZ,IAAY,EACZ,SAAsB,EACtB,OAAiB,EACjB,OAAiB;IAEjB,MAAM,QAAQ,GAAG,eAAe,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IAClD,MAAM,OAAO,GAAG,YAAY,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;IACrD,OAAO,EAAE,SAAS,EAAE,QAAQ,CAAC,SAAS,IAAI,OAAO,CAAC,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;AACnF,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,IAAY,EAAE,KAAY;IAC/C,OAAO,KAAK,CAAC,IAAI,EAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,CAAC,KAAK,CAAC,EAAE,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;AAC1E,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,MAA8B,EAAE,KAAY;IACvE,MAAM,SAAS,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAClC,MAAM,OAAO,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC;IACjC,MAAM,OAAO,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC;IAEjC,MAAM,OAAO,GAAkB,EAAE,CAAC;IAClC,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,IAAI,YAAY,GAAG,IAAI,CAAC;IACxB,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QACxC,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;QACjC,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;QACxD,IAAI,CAAC,MAAM,CAAC,SAAS;YAAE,YAAY,GAAG,KAAK,CAAC;QAC5C,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;QAChC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACrB,CAAC;IAED,MAAM,YAAY,GAAG,YAAY,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;IAEvE,OAAO,EAAE,SAAS,EAAE,YAAY,IAAI,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,CAAC;AAC9F,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rudra-js/attested",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Checks model-written product copy against the facts a shop stands behind",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"llm",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
],
|
|
13
13
|
"license": "MIT",
|
|
14
14
|
"author": "Clive Dsouza",
|
|
15
|
-
"homepage": "https://
|
|
15
|
+
"homepage": "https://rudrajs.com",
|
|
16
16
|
"bugs": {
|
|
17
17
|
"url": "https://github.com/clivedsouza1010/rudra-js/issues"
|
|
18
18
|
},
|
package/src/hidden.ts
CHANGED
|
@@ -1,20 +1,7 @@
|
|
|
1
|
-
// One definition for both layers. It lived in two files, and that is why widening it
|
|
2
|
-
// for the quantity layer left the wording layer open twice running.
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Characters that render as nothing: format characters, default-ignorable code points
|
|
6
|
-
* and the controls. Cf and Default_Ignorable each hold characters the other does not.
|
|
7
|
-
*/
|
|
8
1
|
const INVISIBLE = /[\p{Cf}\p{Default_Ignorable_Code_Point}\p{Cc}]/gu;
|
|
9
2
|
|
|
10
|
-
/** The controls that do take room. Deleting these would read `Only 2\n3 left` as 23. */
|
|
11
3
|
const SPACING = /[\t\n\v\f\r\u0085]/;
|
|
12
4
|
|
|
13
|
-
/**
|
|
14
|
-
* Marks that hang on the character before them instead of taking a column of their own,
|
|
15
|
-
* so `4<U+0305>9` reads as 49. Spacing marks (Mc) are not here: a Devanagari matra is
|
|
16
|
-
* a visible gap between two digits and separates them for a reader.
|
|
17
|
-
*/
|
|
18
5
|
const MARKS = /[\p{Mn}\p{Me}]/gu;
|
|
19
6
|
|
|
20
7
|
export function stripInvisible(text: string): string {
|
package/src/index.ts
CHANGED
package/src/numerals.ts
CHANGED
|
@@ -1,21 +1,17 @@
|
|
|
1
|
-
// Everything here is string work. No float ever holds a value, so a twenty-digit
|
|
2
|
-
// order number compares exactly.
|
|
3
|
-
|
|
4
1
|
import { stripInvisible, stripMarks } from './hidden.js';
|
|
5
2
|
|
|
6
|
-
/** A dot or a comma. Either mark is a decimal point in one locale and grouping in another. */
|
|
7
3
|
const AMBIGUOUS = '.,';
|
|
8
|
-
|
|
4
|
+
|
|
9
5
|
const DECIMAL_ONLY = '\u066b';
|
|
10
|
-
|
|
6
|
+
|
|
11
7
|
const GROUPING_ONLY = '\u066c\u0020\u00a0\u2007\u2008\u2009\u202f\u0027\u2019';
|
|
12
8
|
|
|
13
9
|
const SEPARATORS = AMBIGUOUS + DECIMAL_ONLY + GROUPING_ONLY;
|
|
14
10
|
|
|
15
11
|
const RUN = `\\p{Nd}+(?:[${SEPARATORS}]\\p{Nd}+)*`;
|
|
16
|
-
|
|
12
|
+
|
|
17
13
|
const MAGNITUDE = '(?:[十百千万萬億亿兆]|[kKMB](?!\\p{L}))';
|
|
18
|
-
|
|
14
|
+
|
|
19
15
|
const OTHER_NUMERAL = '[\\p{No}\\p{Nl}]+';
|
|
20
16
|
|
|
21
17
|
const SCAN = new RegExp(`${RUN}${MAGNITUDE}|${RUN}|${OTHER_NUMERAL}`, 'gu');
|
|
@@ -26,22 +22,15 @@ const STARTS_DIGIT = /^\p{Nd}/u;
|
|
|
26
22
|
const ENDS_DIGIT = /\p{Nd}$/u;
|
|
27
23
|
|
|
28
24
|
export interface Numeral {
|
|
29
|
-
/** The run exactly as it appears in the text. */
|
|
30
25
|
token: string;
|
|
31
|
-
|
|
26
|
+
|
|
32
27
|
forms: string[];
|
|
33
|
-
|
|
28
|
+
|
|
34
29
|
kind: 'digits' | 'magnitude' | 'other-numeral';
|
|
35
30
|
}
|
|
36
31
|
|
|
37
32
|
const zeros = new Map<number, number>();
|
|
38
33
|
|
|
39
|
-
/**
|
|
40
|
-
* Unicode writes decimal digits in complete sets of ten, and adjacent sets abut, so
|
|
41
|
-
* the start of a maximal run of digit code points is a zero and the offset into that
|
|
42
|
-
* run, modulo ten, is the value. Walking back without that modulo reads every digit
|
|
43
|
-
* of the second and later sets in the maths block as a nine.
|
|
44
|
-
*/
|
|
45
34
|
function digitValue(char: string): number {
|
|
46
35
|
const code = char.codePointAt(0) ?? 0;
|
|
47
36
|
|
|
@@ -63,7 +52,6 @@ function toAscii(token: string): string {
|
|
|
63
52
|
return out;
|
|
64
53
|
}
|
|
65
54
|
|
|
66
|
-
/** Strips the zeros that carry no value, so 39.00 and 39 are one string. */
|
|
67
55
|
function canonical(whole: string, fraction: string): string {
|
|
68
56
|
let start = 0;
|
|
69
57
|
while (start < whole.length - 1 && whole[start] === '0') start += 1;
|
|
@@ -76,7 +64,6 @@ function canonical(whole: string, fraction: string): string {
|
|
|
76
64
|
return tail.length === 0 ? head : `${head}.${tail}`;
|
|
77
65
|
}
|
|
78
66
|
|
|
79
|
-
/** Whether these runs read as one grouped whole number: 12,345,678 or 1,29,999. */
|
|
80
67
|
function isGrouping(groups: string[]): boolean {
|
|
81
68
|
if (groups.length === 1) return true;
|
|
82
69
|
if ((groups[groups.length - 1] ?? '').length !== 3) return false;
|
|
@@ -91,12 +78,11 @@ function isGrouping(groups: string[]): boolean {
|
|
|
91
78
|
|
|
92
79
|
const lead = (groups[0] ?? '').length;
|
|
93
80
|
if (three) return lead >= 1 && lead <= 3;
|
|
94
|
-
|
|
81
|
+
|
|
95
82
|
if (two) return lead >= 1 && lead <= 2;
|
|
96
83
|
return false;
|
|
97
84
|
}
|
|
98
85
|
|
|
99
|
-
/** Whether every mark is the same one, and every one of them can be grouping. */
|
|
100
86
|
function allGrouping(separators: string[]): boolean {
|
|
101
87
|
for (const separator of separators) {
|
|
102
88
|
if (separator !== separators[0]) return false;
|
|
@@ -105,7 +91,6 @@ function allGrouping(separators: string[]): boolean {
|
|
|
105
91
|
return true;
|
|
106
92
|
}
|
|
107
93
|
|
|
108
|
-
/** `1,299` is 1299 in London and in Berlin. `1234,567` is a decimal in both. */
|
|
109
94
|
function formsOf(token: string): string[] {
|
|
110
95
|
const ascii = toAscii(token);
|
|
111
96
|
|
|
@@ -133,8 +118,6 @@ function formsOf(token: string): string[] {
|
|
|
133
118
|
const point = separators[separators.length - 1] ?? '';
|
|
134
119
|
const fraction = groups[groups.length - 1] ?? '';
|
|
135
120
|
|
|
136
|
-
// One dot or comma with exactly three digits behind it is grouping wherever it is
|
|
137
|
-
// written, so `4.800` is four thousand eight hundred and never the rating 4.8.
|
|
138
121
|
const groupingWins = forms.length > 0 && leading.length === 0 && fraction.length === 3;
|
|
139
122
|
|
|
140
123
|
if (
|
|
@@ -166,7 +149,6 @@ function collect(found: Numeral[], token: string): void {
|
|
|
166
149
|
return;
|
|
167
150
|
}
|
|
168
151
|
|
|
169
|
-
// `8 10 12` is three numbers, not one badly grouped one.
|
|
170
152
|
if (LOOSE.test(token)) {
|
|
171
153
|
for (const part of token.split(LOOSE)) {
|
|
172
154
|
if (part.length > 0) collect(found, part);
|
|
@@ -174,9 +156,6 @@ function collect(found: Numeral[], token: string): void {
|
|
|
174
156
|
return;
|
|
175
157
|
}
|
|
176
158
|
|
|
177
|
-
// No locale reads `24.12.2026` as a number, so only a fact written the same way
|
|
178
|
-
// stands behind it. The raw run carries a mark, and a reading never does, so it
|
|
179
|
-
// can never be mistaken for one.
|
|
180
159
|
found.push({ token, forms: [toAscii(token)], kind: 'digits' });
|
|
181
160
|
}
|
|
182
161
|
|
|
@@ -186,26 +165,16 @@ export function numeralsIn(text: string): Numeral[] {
|
|
|
186
165
|
return found;
|
|
187
166
|
}
|
|
188
167
|
|
|
189
|
-
/** A whole fact in exponent notation and nothing else, so expanding it swallows no prose. */
|
|
190
168
|
const BARE_EXPONENT = /^[+-]?\d+(?:\.\d+)?[eE][+-]?\d+$/;
|
|
191
169
|
|
|
192
|
-
/**
|
|
193
|
-
* The widest digit run a fact may lay out to. Every finite JS number fits — 5e-324 is
|
|
194
|
-
* the longest, at 326 characters — and nothing a shop sells is wider than that.
|
|
195
|
-
*/
|
|
196
170
|
const DIGIT_LIMIT = 1000;
|
|
197
171
|
|
|
198
|
-
/**
|
|
199
|
-
* `String(1e21)` is `1e+21`, which reads as the two numerals 1 and 21. Lay the same digits
|
|
200
|
-
* out in place. Empty past the limit: `1e2000000000` is twelve characters and no product
|
|
201
|
-
* fact, and laying it out builds a run long enough to take the process down with it.
|
|
202
|
-
*/
|
|
203
172
|
function positional(written: string): string {
|
|
204
173
|
const marker = written.search(/[eE]/);
|
|
205
174
|
if (marker < 0) return written;
|
|
206
175
|
|
|
207
176
|
const power = Number(written.slice(marker + 1));
|
|
208
|
-
|
|
177
|
+
|
|
209
178
|
const body = written.slice(0, marker).replace(/^[+-]/, '');
|
|
210
179
|
const point = body.indexOf('.');
|
|
211
180
|
const digits = point < 0 ? body : body.slice(0, point) + body.slice(point + 1);
|
|
@@ -222,11 +191,6 @@ function positional(written: string): string {
|
|
|
222
191
|
return `${digits.slice(0, place)}.${digits.slice(place)}`;
|
|
223
192
|
}
|
|
224
193
|
|
|
225
|
-
/**
|
|
226
|
-
* A string fact is read as the host typed it, except when the whole of it is a number
|
|
227
|
-
* in exponent notation: `String`, `JSON.stringify`, a CSV export and an API that writes
|
|
228
|
-
* 64-bit values as strings all produce that, and none of them is the host choosing digits.
|
|
229
|
-
*/
|
|
230
194
|
function digitsOf(value: string | number | bigint): string {
|
|
231
195
|
if (typeof value === 'number') return positional(String(value));
|
|
232
196
|
if (typeof value !== 'string') return String(value);
|
|
@@ -235,7 +199,6 @@ function digitsOf(value: string | number | bigint): string {
|
|
|
235
199
|
return BARE_EXPONENT.test(trimmed) ? positional(trimmed) : value;
|
|
236
200
|
}
|
|
237
201
|
|
|
238
|
-
/** Every reading of every numeral the host stands behind. */
|
|
239
202
|
export function supportedValues(values: readonly (string | number | bigint)[]): Set<string> {
|
|
240
203
|
const supported = new Set<string>();
|
|
241
204
|
for (const value of values) {
|
package/src/phrases.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { stripInvisible, stripMarks } from './hidden.js';
|
|
2
2
|
|
|
3
|
-
/** Claims with no value behind them. Stored in the form `normalisePhrasing` produces. */
|
|
4
3
|
export const BANNED_PHRASES: readonly string[] = [
|
|
5
4
|
'cheap',
|
|
6
5
|
'cheaper',
|
|
@@ -85,11 +84,6 @@ export const BANNED_PHRASES: readonly string[] = [
|
|
|
85
84
|
'money back',
|
|
86
85
|
];
|
|
87
86
|
|
|
88
|
-
/**
|
|
89
|
-
* Letters that render as a Latin letter but are not one. Folded so a Cyrillic о in
|
|
90
|
-
* "in stоck" cannot walk a claim past its own entry on the list. Both the text and
|
|
91
|
-
* the phrase are folded, so a genuine Cyrillic phrase still matches itself.
|
|
92
|
-
*/
|
|
93
87
|
const CONFUSABLES: Record<string, string> = {
|
|
94
88
|
а: 'a',
|
|
95
89
|
в: 'b',
|
|
@@ -122,20 +116,14 @@ const CONFUSABLES: Record<string, string> = {
|
|
|
122
116
|
ϲ: 'c',
|
|
123
117
|
};
|
|
124
118
|
|
|
125
|
-
/** A run of whitespace holding one of these is a break the shopper sees. */
|
|
126
119
|
const LINE_BREAK = /[\n\v\f\r\u0085\u2028\u2029]/;
|
|
127
120
|
|
|
128
|
-
/** One shape for text and phrases, so a hyphen or a stray mark cannot hide a claim. */
|
|
129
121
|
export function normalisePhrasing(text: string): string {
|
|
130
|
-
// Marks come off after composition, so an accented e keeps its accent while an
|
|
131
|
-
// overline, which composes with nothing, does not hide `free` from its own entry.
|
|
132
122
|
const lowered = stripMarks(stripInvisible(text).normalize('NFKC').toLowerCase().normalize('NFC'));
|
|
133
123
|
|
|
134
124
|
let folded = '';
|
|
135
125
|
for (const char of lowered) folded += CONFUSABLES[char] ?? char;
|
|
136
126
|
|
|
137
|
-
// A line break survives as itself. Matching reads straight through it, and only
|
|
138
|
-
// the allowance in verify.ts cares, because it will not forgive across a break.
|
|
139
127
|
return folded
|
|
140
128
|
.replace(/[\u2018\u2019]/g, "'")
|
|
141
129
|
.replace(/[-_\u2010-\u2015]+/g, ' ')
|
|
@@ -151,32 +139,24 @@ function isGlued(char: string | undefined): boolean {
|
|
|
151
139
|
|
|
152
140
|
export interface Span {
|
|
153
141
|
start: number;
|
|
154
|
-
|
|
142
|
+
|
|
155
143
|
end: number;
|
|
156
144
|
}
|
|
157
145
|
|
|
158
146
|
export interface Indexed {
|
|
159
|
-
/** The text as it was handed in, which is what spans index and the word gap reads. */
|
|
160
147
|
text: string;
|
|
161
|
-
|
|
148
|
+
|
|
162
149
|
haystack: string;
|
|
163
|
-
|
|
150
|
+
|
|
164
151
|
spots: number[];
|
|
165
152
|
}
|
|
166
153
|
|
|
167
|
-
/**
|
|
168
|
-
* Build this once and read every phrase off it. The text must already be normalised.
|
|
169
|
-
*
|
|
170
|
-
* Matching runs with every space dropped, because 送料 無料 is 送料無料 to anyone and a
|
|
171
|
-
* space is free. `spots` carries each character back to where it really sat, so the
|
|
172
|
-
* word-gap guard still reads the neighbours the shopper sees.
|
|
173
|
-
*/
|
|
174
154
|
export function indexPhrasing(text: string): Indexed {
|
|
175
155
|
const tight: string[] = [];
|
|
176
156
|
const spots: number[] = [];
|
|
177
157
|
for (let index = 0; index < text.length; index += 1) {
|
|
178
158
|
const char = text[index] ?? '';
|
|
179
|
-
|
|
159
|
+
|
|
180
160
|
if (char === ' ' || char === '\n') continue;
|
|
181
161
|
tight.push(char);
|
|
182
162
|
spots.push(index);
|
|
@@ -185,9 +165,6 @@ export function indexPhrasing(text: string): Indexed {
|
|
|
185
165
|
return { text, haystack: tight.join(''), spots };
|
|
186
166
|
}
|
|
187
167
|
|
|
188
|
-
/** Spans index the text the index was built from. The phrase must already be normalised. */
|
|
189
|
-
// The word-gap rule only guards an ASCII edge, so `cheap` misses `cheapskate`
|
|
190
|
-
// while a Japanese phrase — which has no word gaps to find — still matches.
|
|
191
168
|
export function spansIn(indexed: Indexed, phrase: string): Span[] {
|
|
192
169
|
const { text, haystack, spots } = indexed;
|
|
193
170
|
|
|
@@ -206,7 +183,7 @@ export function spansIn(indexed: Indexed, phrase: string): Span[] {
|
|
|
206
183
|
const start = spots[at] ?? 0;
|
|
207
184
|
const end = spots[at + needle.length - 1] ?? 0;
|
|
208
185
|
const next = text[end + 1];
|
|
209
|
-
|
|
186
|
+
|
|
210
187
|
const plural = next === 's' && !isGlued(text[end + 2]);
|
|
211
188
|
|
|
212
189
|
const before = guardStart && isGlued(text[start - 1]);
|
package/src/verify.ts
CHANGED
|
@@ -1,44 +1,37 @@
|
|
|
1
1
|
import { numeralsIn, supportedValues, type Numeral } from './numerals.js';
|
|
2
2
|
import { BANNED_PHRASES, indexPhrasing, normalisePhrasing, spansIn, type Span } from './phrases.js';
|
|
3
3
|
|
|
4
|
-
// Two layers, reported apart, because only one of them is a proof.
|
|
5
|
-
|
|
6
4
|
export type Layer = 'quantity' | 'wording';
|
|
7
5
|
|
|
8
6
|
export interface Finding {
|
|
9
7
|
layer: Layer;
|
|
10
|
-
|
|
8
|
+
|
|
11
9
|
token: string;
|
|
12
|
-
|
|
10
|
+
|
|
13
11
|
reason: string;
|
|
14
12
|
}
|
|
15
13
|
|
|
16
14
|
export interface LayerReport {
|
|
17
15
|
supported: boolean;
|
|
18
|
-
|
|
19
|
-
* What this layer's verdict is worth. `proof` means every numeral in the text is
|
|
20
|
-
* a value the host supplied — not that the sentence around it is true.
|
|
21
|
-
*/
|
|
16
|
+
|
|
22
17
|
strength: 'proof' | 'best-effort';
|
|
23
|
-
|
|
18
|
+
|
|
24
19
|
checked: number;
|
|
25
|
-
|
|
20
|
+
|
|
26
21
|
findings: Finding[];
|
|
27
22
|
}
|
|
28
23
|
|
|
29
24
|
export interface VerifyResult {
|
|
30
|
-
/** True when both layers are. */
|
|
31
25
|
supported: boolean;
|
|
32
26
|
quantity: LayerReport;
|
|
33
27
|
wording: LayerReport;
|
|
34
28
|
}
|
|
35
29
|
|
|
36
30
|
export interface Facts {
|
|
37
|
-
/** In any shape they are written: `39`, `'$1,299.00'`, `'4,8'`. Every numeral in one counts. */
|
|
38
31
|
values: readonly (string | number | bigint)[];
|
|
39
|
-
|
|
32
|
+
|
|
40
33
|
bannedPhrases?: readonly string[];
|
|
41
|
-
|
|
34
|
+
|
|
42
35
|
allowedPhrases?: readonly string[];
|
|
43
36
|
}
|
|
44
37
|
|
|
@@ -48,19 +41,12 @@ export interface FieldResult {
|
|
|
48
41
|
}
|
|
49
42
|
|
|
50
43
|
export interface BatchResult {
|
|
51
|
-
/** True when every field is, and when the fields read as one carry no banned claim. */
|
|
52
44
|
supported: boolean;
|
|
53
45
|
fields: FieldResult[];
|
|
54
|
-
|
|
46
|
+
|
|
55
47
|
acrossFields: LayerReport;
|
|
56
48
|
}
|
|
57
49
|
|
|
58
|
-
/**
|
|
59
|
-
* One reading per fact list, keyed on the list itself, because core hands `verify` the
|
|
60
|
-
* same array again for every field it screens. Held against a copy of what the list
|
|
61
|
-
* carried: `readonly` on `Facts.values` is a compile-time view of one reference, and a
|
|
62
|
-
* host holding the array can write to it between two calls.
|
|
63
|
-
*/
|
|
64
50
|
const readings = new WeakMap<object, { held: readonly unknown[]; supported: Set<string> }>();
|
|
65
51
|
|
|
66
52
|
function unchanged(held: readonly unknown[], values: readonly unknown[]): boolean {
|
|
@@ -73,7 +59,7 @@ function unchanged(held: readonly unknown[], values: readonly unknown[]): boolea
|
|
|
73
59
|
|
|
74
60
|
function valuesOf(facts: Facts): Set<string> {
|
|
75
61
|
const values = facts.values;
|
|
76
|
-
|
|
62
|
+
|
|
77
63
|
if (!Array.isArray(values)) return supportedValues(values);
|
|
78
64
|
|
|
79
65
|
const read = readings.get(values);
|
|
@@ -151,16 +137,12 @@ function checkWording(text: string, phrases: string[], allowed: string[]): Layer
|
|
|
151
137
|
const spans: Span[] = [];
|
|
152
138
|
for (const phrase of allowed) {
|
|
153
139
|
for (const span of spansIn(indexed, phrase)) {
|
|
154
|
-
// The denylist reads through a break and the allowance does not, so both rules
|
|
155
|
-
// err toward reporting. A model that puts the negation in one block and the
|
|
156
|
-
// claim in another has written two things the shopper reads apart.
|
|
157
140
|
if (!normalised.slice(span.start, span.end + 1).includes('\n')) spans.push(span);
|
|
158
141
|
}
|
|
159
142
|
}
|
|
160
143
|
|
|
161
144
|
const findings: Finding[] = [];
|
|
162
145
|
for (const phrase of phrases) {
|
|
163
|
-
// One hit inside allowed wording is forgiven; the same phrase elsewhere is not.
|
|
164
146
|
let caught = false;
|
|
165
147
|
for (const hit of spansIn(indexed, phrase)) {
|
|
166
148
|
if (!standsBehind(spans, hit)) caught = true;
|
|
@@ -193,12 +175,10 @@ function check(
|
|
|
193
175
|
return { supported: quantity.supported && wording.supported, quantity, wording };
|
|
194
176
|
}
|
|
195
177
|
|
|
196
|
-
/** Whether one piece of model-written text stands on the facts the host supplied. */
|
|
197
178
|
export function verify(text: string, facts: Facts): VerifyResult {
|
|
198
179
|
return check(text, valuesOf(facts), phrasesOf(facts), allowedOf(facts));
|
|
199
180
|
}
|
|
200
181
|
|
|
201
|
-
/** The same check over every field of one product, without repeating the facts. */
|
|
202
182
|
export function verifyFields(fields: Record<string, string>, facts: Facts): BatchResult {
|
|
203
183
|
const supported = valuesOf(facts);
|
|
204
184
|
const phrases = phrasesOf(facts);
|
|
@@ -215,8 +195,6 @@ export function verifyFields(fields: Record<string, string>, facts: Facts): Batc
|
|
|
215
195
|
written.push(text);
|
|
216
196
|
}
|
|
217
197
|
|
|
218
|
-
// A card renders its fields next to each other, so `Free` and `delivery on every
|
|
219
|
-
// order` are one sentence to a shopper even though neither field carries one.
|
|
220
198
|
const acrossFields = checkWording(written.join(' '), phrases, allowed);
|
|
221
199
|
|
|
222
200
|
return { supported: allSupported && acrossFields.supported, fields: results, acrossFields };
|