@rudra-js/attested 0.7.4 → 0.7.6

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/verify.js CHANGED
@@ -1,43 +1,47 @@
1
1
  import { numeralsIn, supportedValues } from './numerals.js';
2
2
  import { BANNED_PHRASES, indexPhrasing, normalisePhrasing, spansIn, } from './phrases.js';
3
- const readings = new WeakMap();
4
- function unchanged(held, values) {
5
- if (held.length !== values.length)
3
+ const cache = new WeakMap();
4
+ function sameValues(a, b) {
5
+ if (a.length !== b.length)
6
6
  return false;
7
- for (let i = 0; i < held.length; i += 1) {
8
- if (held[i] !== values[i])
7
+ for (let i = 0; i < a.length; i += 1) {
8
+ if (a[i] !== b[i])
9
9
  return false;
10
10
  }
11
11
  return true;
12
12
  }
13
- function valuesOf(facts) {
13
+ function supportedOf(facts) {
14
14
  const values = facts.values;
15
15
  if (!Array.isArray(values))
16
16
  return supportedValues(values);
17
- const read = readings.get(values);
18
- if (read !== undefined && unchanged(read.held, values))
19
- return read.supported;
17
+ const cached = cache.get(values);
18
+ if (cached !== undefined && sameValues(cached.copy, values))
19
+ return cached.supported;
20
20
  const supported = supportedValues(values);
21
- readings.set(values, { held: [...values], supported });
21
+ cache.set(values, { copy: [...values], supported });
22
22
  return supported;
23
23
  }
24
- function phrasesOf(facts) {
25
- const phrases = new Set(BANNED_PHRASES);
26
- for (const phrase of facts.bannedPhrases ?? []) {
24
+ function normaliseAll(phrases) {
25
+ const out = [];
26
+ for (const phrase of phrases ?? []) {
27
27
  const normalised = normalisePhrasing(phrase);
28
28
  if (normalised.length > 0)
29
- phrases.add(normalised);
29
+ out.push(normalised);
30
30
  }
31
- return [...phrases];
31
+ return out;
32
32
  }
33
- function allowedOf(facts) {
34
- const allowed = [];
35
- for (const phrase of facts.allowedPhrases ?? []) {
36
- const normalised = normalisePhrasing(phrase);
37
- if (normalised.length > 0)
38
- allowed.push(normalised);
39
- }
40
- return allowed;
33
+ function bannedOf(facts) {
34
+ const banned = new Set(BANNED_PHRASES);
35
+ for (const phrase of normaliseAll(facts.bannedPhrases))
36
+ banned.add(phrase);
37
+ return [...banned];
38
+ }
39
+ function readFacts(facts) {
40
+ return {
41
+ supported: supportedOf(facts),
42
+ banned: bannedOf(facts),
43
+ allowed: normaliseAll(facts.allowedPhrases),
44
+ };
41
45
  }
42
46
  function reasonFor(numeral) {
43
47
  if (numeral.kind === 'other-numeral') {
@@ -53,12 +57,9 @@ function checkQuantities(text, supported) {
53
57
  const findings = [];
54
58
  const reported = new Set();
55
59
  for (const numeral of numerals) {
56
- let backed = false;
57
- for (const form of numeral.forms) {
58
- if (supported.has(form))
59
- backed = true;
60
- }
61
- if (backed || reported.has(numeral.token))
60
+ if (numeral.forms.some((form) => supported.has(form)))
61
+ continue;
62
+ if (reported.has(numeral.token))
62
63
  continue;
63
64
  reported.add(numeral.token);
64
65
  findings.push({ layer: 'quantity', token: numeral.token, reason: reasonFor(numeral) });
@@ -70,41 +71,43 @@ function checkQuantities(text, supported) {
70
71
  findings,
71
72
  };
72
73
  }
73
- function standsBehind(allowed, hit) {
74
- for (const span of allowed) {
74
+ function isInside(spans, hit) {
75
+ for (const span of spans) {
75
76
  if (span.start <= hit.start && hit.end <= span.end)
76
77
  return true;
77
78
  }
78
79
  return false;
79
80
  }
80
- function checkWording(text, phrases, allowed) {
81
- // Read twice, since Ν reads as N but ν as v; both readings are the same length, so spans line up.
82
- const texts = [normalisePhrasing(text)];
83
- const byLook = normalisePhrasing(text, true);
84
- if (byLook !== texts[0])
85
- texts.push(byLook);
86
- const scans = [];
87
- const spans = [];
88
- for (const normalised of texts) {
89
- const indexed = indexPhrasing(normalised);
90
- scans.push(indexed);
81
+ function forgivenInEitherReading(scans, allowed) {
82
+ const forgiven = [];
83
+ for (const scan of scans) {
91
84
  for (const phrase of allowed) {
92
- for (const span of spansIn(indexed, phrase)) {
93
- if (!normalised.slice(span.start, span.end + 1).includes('\n'))
94
- spans.push(span);
85
+ for (const span of spansIn(scan, phrase)) {
86
+ if (!scan.text.slice(span.start, span.end + 1).includes('\n'))
87
+ forgiven.push(span);
95
88
  }
96
89
  }
97
90
  }
98
- const findings = [];
99
- for (const phrase of phrases) {
100
- let caught = false;
101
- for (const indexed of scans) {
102
- for (const hit of spansIn(indexed, phrase)) {
103
- if (!standsBehind(spans, hit))
104
- caught = true;
105
- }
91
+ return forgiven;
92
+ }
93
+ function isCaught(scans, phrase, forgiven) {
94
+ for (const scan of scans) {
95
+ for (const hit of spansIn(scan, phrase)) {
96
+ if (!isInside(forgiven, hit))
97
+ return true;
106
98
  }
107
- if (!caught)
99
+ }
100
+ return false;
101
+ }
102
+ function checkWording(text, prepared) {
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);
108
+ const findings = [];
109
+ for (const phrase of banned) {
110
+ if (!isCaught(scans, phrase, forgiven))
108
111
  continue;
109
112
  findings.push({
110
113
  layer: 'wording',
@@ -115,34 +118,32 @@ function checkWording(text, phrases, allowed) {
115
118
  return {
116
119
  supported: findings.length === 0,
117
120
  strength: 'best-effort',
118
- checked: phrases.length,
121
+ checked: banned.length,
119
122
  findings,
120
123
  };
121
124
  }
122
- function check(text, supported, phrases, allowed) {
123
- const quantity = checkQuantities(text, supported);
124
- const wording = checkWording(text, phrases, allowed);
125
+ function check(text, prepared) {
126
+ const quantity = checkQuantities(text, prepared.supported);
127
+ const wording = checkWording(text, prepared);
125
128
  return { supported: quantity.supported && wording.supported, quantity, wording };
126
129
  }
127
130
  export function verify(text, facts) {
128
- return check(text, valuesOf(facts), phrasesOf(facts), allowedOf(facts));
131
+ return check(text, readFacts(facts));
129
132
  }
130
133
  export function verifyFields(fields, facts) {
131
- const supported = valuesOf(facts);
132
- const phrases = phrasesOf(facts);
133
- const allowed = allowedOf(facts);
134
+ const prepared = readFacts(facts);
134
135
  const results = [];
135
- const written = [];
136
+ const texts = [];
136
137
  let allSupported = true;
137
138
  for (const field of Object.keys(fields)) {
138
139
  const text = fields[field] ?? '';
139
- const result = check(text, supported, phrases, allowed);
140
+ const result = check(text, prepared);
140
141
  if (!result.supported)
141
142
  allSupported = false;
142
143
  results.push({ field, result });
143
- written.push(text);
144
+ texts.push(text);
144
145
  }
145
- const acrossFields = checkWording(written.join(' '), phrases, allowed);
146
+ const acrossFields = checkWording(texts.join(' '), prepared);
146
147
  return { supported: allSupported && acrossFields.supported, fields: results, acrossFields };
147
148
  }
148
149
  //# sourceMappingURL=verify.js.map
@@ -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,14 +1,17 @@
1
1
  {
2
2
  "name": "@rudra-js/attested",
3
- "version": "0.7.4",
4
- "description": "Checks model-written product copy against the facts a shop stands behind",
3
+ "version": "0.7.6",
4
+ "description": "From rudra-js: checks model-written product copy against the facts a shop stands behind",
5
5
  "keywords": [
6
+ "rudra-js",
7
+ "product-recommendations",
6
8
  "llm",
7
9
  "hallucination",
8
10
  "grounding",
9
11
  "verification",
10
12
  "ecommerce",
11
- "guardrails"
13
+ "guardrails",
14
+ "typescript"
12
15
  ],
13
16
  "license": "MIT",
14
17
  "author": "Clive Dsouza",
package/src/index.ts CHANGED
@@ -10,4 +10,4 @@ export {
10
10
  type VerifyResult,
11
11
  } from './verify.js';
12
12
 
13
- export { BANNED_PHRASES } from './phrases.js';
13
+ export { BANNED_PHRASES, foldLookalikes } from './phrases.js';
package/src/numerals.ts CHANGED
@@ -1,21 +1,17 @@
1
1
  import { stripInvisible, stripMarks } from './hidden.js';
2
2
 
3
3
  const AMBIGUOUS = '.,';
4
-
5
4
  const DECIMAL_ONLY = '\u066b';
6
-
7
5
  const GROUPING_ONLY = '\u066c\u0020\u00a0\u2007\u2008\u2009\u202f\u0027\u2019';
8
-
9
6
  const SEPARATORS = AMBIGUOUS + DECIMAL_ONLY + GROUPING_ONLY;
10
7
 
11
8
  const RUN = `\\p{Nd}+(?:[${SEPARATORS}]\\p{Nd}+)*`;
12
-
13
9
  const MAGNITUDE = '(?:[十百千万萬億亿兆]|[kKMB](?!\\p{L}))';
14
-
15
10
  const OTHER_NUMERAL = '[\\p{No}\\p{Nl}]+';
16
11
 
17
12
  const SCAN = new RegExp(`${RUN}${MAGNITUDE}|${RUN}|${OTHER_NUMERAL}`, 'gu');
18
- const LOOSE = new RegExp(`[${GROUPING_ONLY}]`, 'u');
13
+ const GROUPING_MARK = new RegExp(`[${GROUPING_ONLY}]`, 'u');
14
+ const SEPARATOR = new RegExp(`[${SEPARATORS}]`, 'gu');
19
15
 
20
16
  const IS_DIGIT = /^\p{Nd}$/u;
21
17
  const STARTS_DIGIT = /^\p{Nd}/u;
@@ -23,25 +19,24 @@ const ENDS_DIGIT = /\p{Nd}$/u;
23
19
 
24
20
  export interface Numeral {
25
21
  token: string;
26
-
27
22
  forms: string[];
28
-
29
23
  kind: 'digits' | 'magnitude' | 'other-numeral';
30
24
  }
31
25
 
32
- const zeros = new Map<number, number>();
26
+ const runStartOf = new Map<number, number>();
33
27
 
34
28
  function digitValue(char: string): number {
35
29
  const code = char.codePointAt(0) ?? 0;
36
30
 
37
- let zero = zeros.get(code);
38
- if (zero === undefined) {
39
- zero = code;
40
- while (zero > 0 && IS_DIGIT.test(String.fromCodePoint(zero - 1))) zero -= 1;
41
- zeros.set(code, zero);
31
+ let runStart = runStartOf.get(code);
32
+ if (runStart === undefined) {
33
+ runStart = code;
34
+ while (runStart > 0 && IS_DIGIT.test(String.fromCodePoint(runStart - 1))) runStart -= 1;
35
+ runStartOf.set(code, runStart);
42
36
  }
43
37
 
44
- return (code - zero) % 10;
38
+ const offsetInRun = code - runStart;
39
+ return offsetInRun % 10;
45
40
  }
46
41
 
47
42
  function toAscii(token: string): string {
@@ -66,119 +61,78 @@ function canonical(whole: string, fraction: string): string {
66
61
 
67
62
  function isGrouping(groups: string[]): boolean {
68
63
  if (groups.length === 1) return true;
69
- if ((groups[groups.length - 1] ?? '').length !== 3) return false;
70
-
71
- let three = true;
72
- let two = true;
73
- for (let index = 1; index < groups.length - 1; index += 1) {
74
- const size = (groups[index] ?? '').length;
75
- if (size !== 3) three = false;
76
- if (size !== 2) two = false;
77
- }
64
+ if ((groups.at(-1) ?? '').length !== 3) return false;
78
65
 
79
66
  const lead = (groups[0] ?? '').length;
80
- if (three) return lead >= 1 && lead <= 3;
81
-
82
- if (two) return lead >= 1 && lead <= 2;
67
+ const middle = groups.slice(1, -1);
68
+ if (middle.every((group) => group.length === 3)) return lead >= 1 && lead <= 3;
69
+ if (middle.every((group) => group.length === 2)) return lead >= 1 && lead <= 2;
83
70
  return false;
84
71
  }
85
72
 
86
- function allGrouping(separators: string[]): boolean {
87
- for (const separator of separators) {
88
- if (separator !== separators[0]) return false;
89
- if (DECIMAL_ONLY.includes(separator)) return false;
73
+ function sameGroupingMark(marks: string[]): boolean {
74
+ for (const mark of marks) {
75
+ if (mark !== marks[0] || mark === DECIMAL_ONLY) return false;
90
76
  }
91
77
  return true;
92
78
  }
93
79
 
94
80
  function formsOf(token: string): string[] {
95
81
  const ascii = toAscii(token);
96
-
97
- const groups: string[] = [];
98
- const separators: string[] = [];
99
- let current = '';
100
- for (const char of ascii) {
101
- if (SEPARATORS.includes(char)) {
102
- groups.push(current);
103
- separators.push(char);
104
- current = '';
105
- } else {
106
- current += char;
107
- }
108
- }
109
- groups.push(current);
110
-
111
- if (separators.length === 0) return [canonical(groups[0] ?? '', '')];
82
+ const groups = ascii.split(SEPARATOR);
83
+ const marks = ascii.match(SEPARATOR) ?? [];
84
+ if (marks.length === 0) return [canonical(ascii, '')];
112
85
 
113
86
  const forms: string[] = [];
114
- if (allGrouping(separators) && isGrouping(groups)) forms.push(canonical(groups.join(''), ''));
87
+ const grouped = sameGroupingMark(marks) && isGrouping(groups);
88
+ if (grouped) forms.push(canonical(groups.join(''), ''));
115
89
 
116
90
  const whole = groups.slice(0, -1);
117
- const leading = separators.slice(0, -1);
118
- const point = separators[separators.length - 1] ?? '';
119
- const fraction = groups[groups.length - 1] ?? '';
120
-
121
- const groupingWins = forms.length > 0 && leading.length === 0 && fraction.length === 3;
122
-
123
- if (
124
- !groupingWins &&
125
- !GROUPING_ONLY.includes(point) &&
126
- allGrouping(leading) &&
127
- leading[0] !== point &&
128
- isGrouping(whole)
129
- ) {
130
- forms.push(canonical(whole.join(''), fraction));
131
- }
91
+ const fraction = groups.at(-1) ?? '';
92
+ const groupingMarks = marks.slice(0, -1);
93
+ const decimalMark = marks.at(-1) ?? '';
94
+
95
+ if (grouped && groupingMarks.length === 0 && fraction.length === 3) return forms;
96
+ if (GROUPING_ONLY.includes(decimalMark)) return forms;
97
+ if (!sameGroupingMark(groupingMarks)) return forms;
98
+ if (groupingMarks[0] === decimalMark) return forms;
99
+ if (!isGrouping(whole)) return forms;
132
100
 
101
+ forms.push(canonical(whole.join(''), fraction));
133
102
  return forms;
134
103
  }
135
104
 
136
- function collect(found: Numeral[], token: string): void {
137
- if (!STARTS_DIGIT.test(token)) {
138
- found.push({ token, forms: [], kind: 'other-numeral' });
139
- return;
140
- }
141
- if (!ENDS_DIGIT.test(token)) {
142
- found.push({ token, forms: [], kind: 'magnitude' });
143
- return;
144
- }
105
+ function numeralsOf(token: string): Numeral[] {
106
+ if (!STARTS_DIGIT.test(token)) return [{ token, forms: [], kind: 'other-numeral' }];
107
+ if (!ENDS_DIGIT.test(token)) return [{ token, forms: [], kind: 'magnitude' }];
145
108
 
146
109
  const forms = formsOf(token);
147
- if (forms.length > 0) {
148
- found.push({ token, forms, kind: 'digits' });
149
- return;
150
- }
110
+ if (forms.length > 0) return [{ token, forms, kind: 'digits' }];
111
+ if (!GROUPING_MARK.test(token)) return [{ token, forms: [toAscii(token)], kind: 'digits' }];
151
112
 
152
- if (LOOSE.test(token)) {
153
- for (const part of token.split(LOOSE)) {
154
- if (part.length > 0) collect(found, part);
155
- }
156
- return;
157
- }
158
-
159
- found.push({ token, forms: [toAscii(token)], kind: 'digits' });
113
+ return token
114
+ .split(GROUPING_MARK)
115
+ .filter((part) => part.length > 0)
116
+ .flatMap((part) => numeralsOf(part));
160
117
  }
161
118
 
162
119
  export function numeralsIn(text: string): Numeral[] {
163
- const found: Numeral[] = [];
164
- for (const match of stripMarks(stripInvisible(text)).matchAll(SCAN)) collect(found, match[0]);
165
- return found;
120
+ const visible = stripMarks(stripInvisible(text));
121
+ return [...visible.matchAll(SCAN)].flatMap((match) => numeralsOf(match[0]));
166
122
  }
167
123
 
168
124
  const BARE_EXPONENT = /^[+-]?\d+(?:\.\d+)?[eE][+-]?\d+$/;
169
-
170
125
  const DIGIT_LIMIT = 1000;
171
126
 
172
127
  function positional(written: string): string {
173
- const marker = written.search(/[eE]/);
174
- if (marker < 0) return written;
175
-
176
- const power = Number(written.slice(marker + 1));
128
+ const exponentAt = written.search(/[eE]/);
129
+ if (exponentAt < 0) return written;
177
130
 
178
- const body = written.slice(0, marker).replace(/^[+-]/, '');
179
- const point = body.indexOf('.');
180
- const digits = point < 0 ? body : body.slice(0, point) + body.slice(point + 1);
181
- const place = (point < 0 ? body.length : point) + power;
131
+ const power = Number(written.slice(exponentAt + 1));
132
+ const mantissa = written.slice(0, exponentAt).replace(/^[+-]/, '');
133
+ const dot = mantissa.indexOf('.');
134
+ const digits = mantissa.replace('.', '');
135
+ const place = (dot < 0 ? mantissa.length : dot) + power;
182
136
 
183
137
  if (place <= 0) {
184
138
  if (digits.length - place > DIGIT_LIMIT) return '';
@@ -191,7 +145,7 @@ function positional(written: string): string {
191
145
  return `${digits.slice(0, place)}.${digits.slice(place)}`;
192
146
  }
193
147
 
194
- function digitsOf(value: string | number | bigint): string {
148
+ function textOf(value: string | number | bigint): string {
195
149
  if (typeof value === 'number') return positional(String(value));
196
150
  if (typeof value !== 'string') return String(value);
197
151
 
@@ -202,7 +156,7 @@ function digitsOf(value: string | number | bigint): string {
202
156
  export function supportedValues(values: readonly (string | number | bigint)[]): Set<string> {
203
157
  const supported = new Set<string>();
204
158
  for (const value of values) {
205
- for (const numeral of numeralsIn(digitsOf(value))) {
159
+ for (const numeral of numeralsIn(textOf(value))) {
206
160
  for (const form of numeral.forms) supported.add(form);
207
161
  }
208
162
  }
package/src/phrases.ts CHANGED
@@ -158,7 +158,6 @@ const CONFUSABLES: Record<string, string> = {
158
158
  ѵ: 'v',
159
159
  };
160
160
 
161
- // Capitals that lowercasing reads wrong: Ν becomes ν, which reads as v.
162
161
  const CAPITALS: Record<string, string> = {
163
162
  Β: 'b',
164
163
  Ζ: 'z',
@@ -175,23 +174,28 @@ const CAPITALS: Record<string, string> = {
175
174
  Ԍ: 'g',
176
175
  };
177
176
 
177
+ const BEFORE_NFKC: Record<string, string> = {
178
+ ϲ: 'c',
179
+ Ϲ: 'c',
180
+ };
181
+
178
182
  const LINE_BREAK = /[\n\v\f\r\u0085\u2028\u2029]/;
179
183
 
180
- export function normalisePhrasing(text: string, byLook = false): string {
181
- // NFKC would turn the lunate sigmas into ς and Σ, which the fold can't read as c.
182
- const compatible = stripInvisible(text).replace(/[ϲϹ]/g, 'c').normalize('NFKC');
184
+ function foldChars(text: string, table: Record<string, string>): string {
185
+ let folded = '';
186
+ for (const char of text) folded += table[char] ?? char;
187
+ return folded;
188
+ }
183
189
 
184
- let cased = compatible;
185
- if (byLook) {
186
- cased = '';
187
- for (const char of compatible) cased += CAPITALS[char] ?? char;
188
- }
190
+ export function foldLookalikes(text: string, byLook = false): string {
191
+ const compatible = foldChars(stripInvisible(text), BEFORE_NFKC).normalize('NFKC');
192
+ const cased = byLook ? foldChars(compatible, CAPITALS) : compatible;
189
193
  const lowered = stripMarks(cased.toLowerCase().normalize('NFC'));
194
+ return foldChars(lowered, CONFUSABLES);
195
+ }
190
196
 
191
- let folded = '';
192
- for (const char of lowered) folded += CONFUSABLES[char] ?? char;
193
-
194
- return folded
197
+ export function normalisePhrasing(text: string, byLook = false): string {
198
+ return foldLookalikes(text, byLook)
195
199
  .replace(/[\u2018\u2019]/g, "'")
196
200
  .replace(/[-_\u2010-\u2015]+/g, ' ')
197
201
  .replace(/\s+/g, (run) => (LINE_BREAK.test(run) ? '\n' : ' '))
@@ -206,37 +210,34 @@ function isGlued(char: string | undefined): boolean {
206
210
 
207
211
  export interface Span {
208
212
  start: number;
209
-
210
213
  end: number;
211
214
  }
212
215
 
213
216
  export interface Indexed {
214
217
  text: string;
215
-
216
- haystack: string;
217
-
218
- spots: number[];
218
+ squeezed: string;
219
+ offsets: number[];
219
220
  }
220
221
 
221
222
  export function indexPhrasing(text: string): Indexed {
222
- const tight: string[] = [];
223
- const spots: number[] = [];
223
+ const kept: string[] = [];
224
+ const offsets: number[] = [];
224
225
  for (let index = 0; index < text.length; index += 1) {
225
226
  const char = text[index] ?? '';
226
227
 
227
228
  if (char === ' ' || char === '\n') continue;
228
- tight.push(char);
229
- spots.push(index);
229
+ kept.push(char);
230
+ offsets.push(index);
230
231
  }
231
232
 
232
- return { text, haystack: tight.join(''), spots };
233
+ return { text, squeezed: kept.join(''), offsets };
233
234
  }
234
235
 
235
236
  export function spansIn(indexed: Indexed, phrase: string): Span[] {
236
- const { text, haystack, spots } = indexed;
237
+ const { text, squeezed, offsets } = indexed;
237
238
 
238
239
  const spans: Span[] = [];
239
- const needle = phrase.replaceAll(' ', '').replaceAll('\n', '');
240
+ const needle = indexPhrasing(phrase).squeezed;
240
241
  if (needle.length === 0) return spans;
241
242
 
242
243
  const guardStart = ASCII_WORD.test(needle[0] ?? '');
@@ -244,18 +245,18 @@ export function spansIn(indexed: Indexed, phrase: string): Span[] {
244
245
 
245
246
  let from = 0;
246
247
  for (;;) {
247
- const at = haystack.indexOf(needle, from);
248
+ const at = squeezed.indexOf(needle, from);
248
249
  if (at === -1) return spans;
249
250
 
250
- const start = spots[at] ?? 0;
251
- const end = spots[at + needle.length - 1] ?? 0;
251
+ const start = offsets[at] ?? 0;
252
+ const end = offsets[at + needle.length - 1] ?? 0;
252
253
  const next = text[end + 1];
253
254
 
254
255
  const plural = next === 's' && !isGlued(text[end + 2]);
255
256
 
256
- const before = guardStart && isGlued(text[start - 1]);
257
- const after = guardEnd && isGlued(next) && !plural;
258
- if (!before && !after) spans.push({ start, end });
257
+ const gluedBefore = guardStart && isGlued(text[start - 1]);
258
+ const gluedAfter = guardEnd && isGlued(next) && !plural;
259
+ if (!gluedBefore && !gluedAfter) spans.push({ start, end });
259
260
 
260
261
  from = at + 1;
261
262
  }