@firedrill-tools/salesforce 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +156 -0
  3. package/firedrill/agent.target.json +17 -0
  4. package/firedrill/api-limit-exceeded.scenario.json +11 -0
  5. package/firedrill/baseline.scenario.json +2213 -0
  6. package/firedrill/conformance.suite.json +23 -0
  7. package/firedrill/row-locked.scenario.json +11 -0
  8. package/firedrill/salesforce-api-limit-exceeded.drill.json +336 -0
  9. package/firedrill/salesforce-collections-composite.drill.json +277 -0
  10. package/firedrill/salesforce-denied.drill.json +74 -0
  11. package/firedrill/salesforce-fresh-install.drill.json +86 -0
  12. package/firedrill/salesforce-inactive-user.drill.json +43 -0
  13. package/firedrill/salesforce-invalid-session.drill.json +336 -0
  14. package/firedrill/salesforce-large-responses.drill.json +138 -0
  15. package/firedrill/salesforce-mcp-aliases.drill.json +156 -0
  16. package/firedrill/salesforce-profile-permissions.drill.json +241 -0
  17. package/firedrill/salesforce-read-only.drill.json +154 -0
  18. package/firedrill/salesforce-rest-flow.drill.json +714 -0
  19. package/firedrill/salesforce-row-locked.drill.json +227 -0
  20. package/firedrill/salesforce-sharing.drill.json +201 -0
  21. package/firedrill/salesforce-soql.drill.json +139 -0
  22. package/firedrill/salesforce-tight-limits.drill.json +336 -0
  23. package/firedrill/salesforce-write-committed-lost.drill.json +176 -0
  24. package/firedrill/tight-limits.scenario.json +41 -0
  25. package/firedrill/tools/salesforce/behavior.mjs +637 -0
  26. package/firedrill/tools/salesforce/lib/bytes.mjs +56 -0
  27. package/firedrill/tools/salesforce/lib/ids.mjs +68 -0
  28. package/firedrill/tools/salesforce/lib/match.mjs +352 -0
  29. package/firedrill/tools/salesforce/lib/records.mjs +506 -0
  30. package/firedrill/tools/salesforce/lib/schema.mjs +429 -0
  31. package/firedrill/tools/salesforce/lib/search.mjs +92 -0
  32. package/firedrill/tools/salesforce/lib/soql.mjs +1119 -0
  33. package/firedrill/tools/salesforce/lib/state.mjs +378 -0
  34. package/firedrill/tools/salesforce/lib/wire.mjs +109 -0
  35. package/firedrill/tools/salesforce/salesforce.tool.json +3939 -0
  36. package/firedrill/world.json +2705 -0
  37. package/firedrill/write-committed-lost.scenario.json +11 -0
  38. package/firedrill.json +5 -0
  39. package/package.json +64 -0
  40. package/starter.json +2212 -0
  41. package/test/conformance.mjs +1122 -0
@@ -0,0 +1,56 @@
1
+ // Response byte budgets. The framework refuses any HTTP route response over 1 MiB, so every body whose
2
+ // size depends on stored rows (query pages, search results, collection retrieves, composite responses)
3
+ // is measured in encoded UTF-8 bytes of the JSON actually sent, entry by entry, before it is admitted.
4
+
5
+ /** Budget for one response body: comfortably below the framework's 1 MiB (1,048,576-byte) cap. */
6
+ export const RESPONSE_BYTE_BUDGET = 900000;
7
+
8
+ /**
9
+ * UTF-8 byte length of a string: 1 byte below U+0080, 2 below U+0800, 4 for a surrogate pair (2 per
10
+ * code unit) and 3 otherwise. JSON.stringify escapes lone surrogates, so pairs are always well formed.
11
+ */
12
+ export function utf8Length(text) {
13
+ let bytes = 0;
14
+ for (let index = 0; index < text.length; index += 1) {
15
+ const code = text.charCodeAt(index);
16
+ if (code < 0x80) bytes += 1;
17
+ else if (code < 0x800) bytes += 2;
18
+ else if (code >= 0xd800 && code <= 0xdfff) bytes += 2;
19
+ else bytes += 3;
20
+ }
21
+ return bytes;
22
+ }
23
+
24
+ /** Encoded size of `value` as the JSON body the route sends. */
25
+ export function jsonBytes(value) {
26
+ const text = JSON.stringify(value);
27
+ return text === undefined ? 0 : utf8Length(text);
28
+ }
29
+
30
+ /** Tracks the bytes admitted to one array inside a body; `admit` refuses an entry that would pass the limit. */
31
+ export function byteBudget(limit, overhead) {
32
+ let used = overhead;
33
+ let count = 0;
34
+ return {
35
+ get used() {
36
+ return used;
37
+ },
38
+ /** Size an entry (plus its separating comma) without admitting it. */
39
+ measure(entry) {
40
+ return jsonBytes(entry) + (count > 0 ? 1 : 0);
41
+ },
42
+ fits(size) {
43
+ return used + size <= limit;
44
+ },
45
+ add(size) {
46
+ used += size;
47
+ count += 1;
48
+ },
49
+ admit(entry) {
50
+ const size = this.measure(entry);
51
+ if (!this.fits(size)) return false;
52
+ this.add(size);
53
+ return true;
54
+ },
55
+ };
56
+ }
@@ -0,0 +1,68 @@
1
+ // Salesforce-shaped record ids: 3-character key prefix + `Fd0` + 9-digit record number (15
2
+ // characters, case-sensitive) + the 3-character case-safe suffix computed with Salesforce's real
3
+ // algorithm. The record number comes from the `meta/counters` row; nothing here is random.
4
+
5
+ import { KEY_PREFIXES, typeByPrefix } from "./schema.mjs";
6
+
7
+ const SUFFIX_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ012345";
8
+ const ID_15 = /^[a-zA-Z0-9]{15}$/;
9
+ const ID_18 = /^[a-zA-Z0-9]{18}$/;
10
+
11
+ /** The case-safe suffix of a 15-character id (per 5-character chunk, bit i set when char i is upper-case). */
12
+ export function caseSafeSuffix(id15) {
13
+ let suffix = "";
14
+ for (let chunk = 0; chunk < 3; chunk += 1) {
15
+ let bits = 0;
16
+ for (let index = 0; index < 5; index += 1) {
17
+ const char = id15[chunk * 5 + index];
18
+ if (char >= "A" && char <= "Z") bits |= 1 << index;
19
+ }
20
+ suffix += SUFFIX_ALPHABET[bits];
21
+ }
22
+ return suffix;
23
+ }
24
+
25
+ /** True when `value` has the shape of a Salesforce id (15 or 18 alphanumerics). */
26
+ export function isWellFormedId(value) {
27
+ return typeof value === "string" && (ID_15.test(value) || ID_18.test(value));
28
+ }
29
+
30
+ /**
31
+ * 15- or 18-character id → canonical 18-character id. A 15-character id is case-sensitive and gets
32
+ * its suffix computed. An 18-character id is case-insensitive as in Salesforce: the suffix (any
33
+ * case) re-cases the first 15 characters, and a suffix that is not a valid checksum for them (a
34
+ * character outside the suffix alphabet, or a case bit set on a digit) is MALFORMED (returns null).
35
+ */
36
+ export function normalizeId(value) {
37
+ if (!isWellFormedId(value)) return null;
38
+ if (value.length === 15) return `${value}${caseSafeSuffix(value)}`;
39
+ const suffix = value.slice(15).toUpperCase();
40
+ let base = "";
41
+ for (let chunk = 0; chunk < 3; chunk += 1) {
42
+ const bits = SUFFIX_ALPHABET.indexOf(suffix[chunk]);
43
+ if (bits < 0) return null;
44
+ for (let index = 0; index < 5; index += 1) {
45
+ const char = value[chunk * 5 + index];
46
+ base += (bits & (1 << index)) !== 0 ? char.toUpperCase() : char.toLowerCase();
47
+ }
48
+ }
49
+ if (caseSafeSuffix(base) !== suffix) return null;
50
+ return `${base}${suffix}`;
51
+ }
52
+
53
+ export function prefixOf(id) {
54
+ return id.slice(0, 3);
55
+ }
56
+
57
+ /** The sObject type a well-formed id belongs to, by key prefix (null for unknown prefixes). */
58
+ export function typeOfId(id) {
59
+ return typeByPrefix(prefixOf(id));
60
+ }
61
+
62
+ /** Mint the id for record number `n` of `type` (n < 10^9). */
63
+ export function mintId(type, recordNumber) {
64
+ const prefix = KEY_PREFIXES[type];
65
+ const body = String(recordNumber).padStart(9, "0");
66
+ const base = `${prefix}Fd0${body}`;
67
+ return `${base}${caseSafeSuffix(base)}`;
68
+ }
@@ -0,0 +1,352 @@
1
+ // Wildcard matching for SOQL LIKE and parameterized search terms, in time linear in the value length.
2
+ // No regular expression is ever built from caller input (a `%` → `.*` regex backtracks polynomially),
3
+ // and no candidate position is re-tested character by character (a naive "try every start" scan costs
4
+ // value length × segment length, which a 32,000-character textarea and a long near-miss segment turn
5
+ // into seconds per row). Values are folded once into code-point arrays; then:
6
+ // - SOQL LIKE: `%`-free literal segments are located with Knuth–Morris–Pratt; segments that contain
7
+ // the one-character wildcard `_` are located with a bit-parallel shift-and automaton. Segments are
8
+ // placed leftmost from left to right, so the scanned text regions are disjoint: O(value + pattern).
9
+ // - Search terms: every term of the search string is simulated at once as one bit-parallel automaton
10
+ // (at most 200 symbols, so at most 7 machine words per character): O(value).
11
+
12
+ // ---------------------------------------------------------------------------------------------
13
+ // Case folding
14
+ // ---------------------------------------------------------------------------------------------
15
+
16
+ /** Case folding per code point; keeps the UTF-16 length so the fold never merges or splits characters. */
17
+ function foldChar(char) {
18
+ const upper = char.toUpperCase();
19
+ if (upper.length === char.length) {
20
+ const lower = upper.toLowerCase();
21
+ if (lower.length === char.length) return lower;
22
+ }
23
+ const lower = char.toLowerCase();
24
+ return lower.length === char.length ? lower : char;
25
+ }
26
+
27
+ function foldCode(code) {
28
+ if (code < 0x80) return code >= 0x41 && code <= 0x5a ? code + 32 : code;
29
+ return foldChar(String.fromCodePoint(code)).codePointAt(0);
30
+ }
31
+
32
+ /** Fold a string into an Int32Array of folded code points (ASCII takes a fast path). */
33
+ export function foldText(text) {
34
+ const out = new Int32Array(text.length);
35
+ let count = 0;
36
+ for (let index = 0; index < text.length; index += 1) {
37
+ let code = text.charCodeAt(index);
38
+ if (code >= 0xd800 && code <= 0xdbff && index + 1 < text.length) {
39
+ const low = text.charCodeAt(index + 1);
40
+ if (low >= 0xdc00 && low <= 0xdfff) {
41
+ code = (code - 0xd800) * 0x400 + (low - 0xdc00) + 0x10000;
42
+ index += 1;
43
+ }
44
+ }
45
+ out[count] = foldCode(code);
46
+ count += 1;
47
+ }
48
+ return count === out.length ? out : out.subarray(0, count);
49
+ }
50
+
51
+ const ANY = -1; // one-character wildcard inside a segment (never a code point)
52
+ const WORD = 32;
53
+
54
+ // ---------------------------------------------------------------------------------------------
55
+ // SOQL LIKE
56
+ // ---------------------------------------------------------------------------------------------
57
+
58
+ /**
59
+ * Longest `%`-free segment containing `_` this Tool locates (shift-and state is ceil(length / 32) words
60
+ * per character). Longer ones fail MALFORMED_QUERY through `reject`; `_`-free segments are unbounded.
61
+ */
62
+ export const MAX_WILDCARD_SEGMENT = 256;
63
+
64
+ /** Does `segment` match `chars` at `at` exactly (O(segment length), used for the anchored ends)? */
65
+ function segmentAt(chars, at, segment) {
66
+ if (at < 0 || at + segment.length > chars.length) return false;
67
+ for (let offset = 0; offset < segment.length; offset += 1) {
68
+ const want = segment[offset];
69
+ if (want !== ANY && want !== chars[at + offset]) return false;
70
+ }
71
+ return true;
72
+ }
73
+
74
+ /** Leftmost occurrence finder for a wildcard-free segment (KMP). Returns the end index or -1. */
75
+ function literalFinder(segment) {
76
+ const m = segment.length;
77
+ const failure = new Int32Array(m);
78
+ for (let i = 1, k = 0; i < m; i += 1) {
79
+ while (k > 0 && segment[i] !== segment[k]) k = failure[k - 1];
80
+ if (segment[i] === segment[k]) k += 1;
81
+ failure[i] = k;
82
+ }
83
+ return (chars, from, to) => {
84
+ let k = 0;
85
+ for (let i = from; i < to; i += 1) {
86
+ const c = chars[i];
87
+ while (k > 0 && c !== segment[k]) k = failure[k - 1];
88
+ if (c === segment[k]) k += 1;
89
+ if (k === m) return i + 1;
90
+ }
91
+ return -1;
92
+ };
93
+ }
94
+
95
+ /** Leftmost occurrence finder for a segment with `_` (bit-parallel shift-and). Returns the end index or -1. */
96
+ function wildcardFinder(segment) {
97
+ const m = segment.length;
98
+ const words = Math.ceil(m / WORD);
99
+ const anyMask = new Uint32Array(words);
100
+ const masks = new Map();
101
+ for (let j = 0; j < m; j += 1) if (segment[j] === ANY) anyMask[j >>> 5] |= 1 << (j & 31);
102
+ for (let j = 0; j < m; j += 1) {
103
+ const code = segment[j];
104
+ if (code === ANY || masks.has(code)) continue;
105
+ const mask = Uint32Array.from(anyMask);
106
+ for (let k = 0; k < m; k += 1) if (segment[k] === code) mask[k >>> 5] |= 1 << (k & 31);
107
+ masks.set(code, mask);
108
+ }
109
+ const lastWord = (m - 1) >>> 5;
110
+ const lastBit = 1 << ((m - 1) & 31);
111
+ if (words === 1) {
112
+ const plain = anyMask[0];
113
+ const single = new Map([...masks].map(([code, mask]) => [code, mask[0]]));
114
+ return (chars, from, to) => {
115
+ let state = 0;
116
+ for (let i = from; i < to; i += 1) {
117
+ const mask = single.get(chars[i]);
118
+ state = ((state << 1) | 1) & (mask === undefined ? plain : mask);
119
+ if ((state & lastBit) !== 0) return i + 1;
120
+ }
121
+ return -1;
122
+ };
123
+ }
124
+ return (chars, from, to) => {
125
+ const state = new Uint32Array(words);
126
+ for (let i = from; i < to; i += 1) {
127
+ const mask = masks.get(chars[i]) ?? anyMask;
128
+ let carry = 1;
129
+ for (let w = 0; w < words; w += 1) {
130
+ const current = state[w];
131
+ state[w] = ((current << 1) | carry) & mask[w];
132
+ carry = current >>> 31;
133
+ }
134
+ if ((state[lastWord] & lastBit) !== 0) return i + 1;
135
+ }
136
+ return -1;
137
+ };
138
+ }
139
+
140
+ /**
141
+ * Compile a SOQL LIKE pattern. `value` is the decoded literal and `wild` a same-length (UTF-16) marker
142
+ * string from the tokenizer: `%` / `_` where the literal carried an unescaped wildcard, anything else
143
+ * for a literal character (so `\%` and `\_` stay literal). Returns `(text) => boolean`: case-insensitive,
144
+ * `%` any sequence (including empty), `_` exactly one character (code point), whole-value match.
145
+ * `reject(message)` must throw; it is called for a `_`-bearing segment longer than MAX_WILDCARD_SEGMENT.
146
+ */
147
+ export function compileLike(value, wild, reject) {
148
+ const segments = [[]];
149
+ let index = 0;
150
+ for (const char of value) {
151
+ const marker = wild[index];
152
+ index += char.length;
153
+ if (marker === "%") segments.push([]);
154
+ else if (marker === "_") segments[segments.length - 1].push(ANY);
155
+ else segments[segments.length - 1].push(foldCode(char.codePointAt(0)));
156
+ }
157
+ const first = segments[0];
158
+ const last = segments.length > 1 ? segments[segments.length - 1] : null;
159
+ const minLength = segments.reduce((sum, segment) => sum + segment.length, 0);
160
+ const middle = [];
161
+ for (const segment of segments.slice(1, -1)) {
162
+ if (segment.length === 0) continue;
163
+ const hasAny = segment.includes(ANY);
164
+ const allAny = segment.every((code) => code === ANY);
165
+ if (hasAny && !allAny && segment.length > MAX_WILDCARD_SEGMENT) {
166
+ reject(`LIKE pattern segment with '_' exceeds the supported length of ${MAX_WILDCARD_SEGMENT} characters between '%' wildcards`);
167
+ }
168
+ middle.push({ length: segment.length, find: allAny ? null : hasAny ? wildcardFinder(segment) : literalFinder(segment) });
169
+ }
170
+ // `text` is the raw stored value; `folded` (optional) returns its folded code points, so a caller that
171
+ // evaluates several clauses over the same value folds it once. The anchored first and last segments are
172
+ // tested on the raw value first (O(segment)), so a value they reject is never folded or scanned.
173
+ return (text, folded) => {
174
+ if (text.length < minLength) return false;
175
+ if (last === null) {
176
+ // Exact match: more than 2 UTF-16 units per pattern character cannot match.
177
+ if (text.length > 2 * first.length) return false;
178
+ const chars = folded === undefined ? foldText(text) : folded();
179
+ return chars.length === first.length && segmentAt(chars, 0, first);
180
+ }
181
+ if (!prefixAt(text, first) || !suffixAt(text, last)) return false;
182
+ // Enough code points for both anchors without overlap: at least ceil(length / 2) code points.
183
+ if (middle.length === 0 && text.length >= 2 * minLength) return true;
184
+ const chars = folded === undefined ? foldText(text) : folded();
185
+ if (chars.length < minLength) return false;
186
+ const end = chars.length - last.length;
187
+ if (!segmentAt(chars, 0, first) || !segmentAt(chars, end, last)) return false;
188
+ let position = first.length;
189
+ for (const segment of middle) {
190
+ if (segment.find === null) {
191
+ if (position + segment.length > end) return false;
192
+ position += segment.length;
193
+ continue;
194
+ }
195
+ const found = segment.find(chars, position, end);
196
+ if (found < 0) return false;
197
+ position = found;
198
+ }
199
+ return position <= end;
200
+ };
201
+ }
202
+
203
+ /** Does the raw `text` start with `segment` (folded code points, ANY for `_`)? O(segment length). */
204
+ function prefixAt(text, segment) {
205
+ let index = 0;
206
+ for (let offset = 0; offset < segment.length; offset += 1) {
207
+ if (index >= text.length) return false;
208
+ let code = text.charCodeAt(index);
209
+ index += 1;
210
+ if (code >= 0xd800 && code <= 0xdbff && index < text.length) {
211
+ const low = text.charCodeAt(index);
212
+ if (low >= 0xdc00 && low <= 0xdfff) {
213
+ code = (code - 0xd800) * 0x400 + (low - 0xdc00) + 0x10000;
214
+ index += 1;
215
+ }
216
+ }
217
+ const want = segment[offset];
218
+ if (want !== ANY && want !== foldCode(code)) return false;
219
+ }
220
+ return true;
221
+ }
222
+
223
+ /** Does the raw `text` end with `segment`? Pairs surrogates exactly as `foldText` does. O(segment length). */
224
+ function suffixAt(text, segment) {
225
+ let index = text.length;
226
+ for (let offset = segment.length - 1; offset >= 0; offset -= 1) {
227
+ if (index <= 0) return false;
228
+ index -= 1;
229
+ let code = text.charCodeAt(index);
230
+ if (code >= 0xdc00 && code <= 0xdfff && index > 0) {
231
+ const high = text.charCodeAt(index - 1);
232
+ if (high >= 0xd800 && high <= 0xdbff) {
233
+ code = (high - 0xd800) * 0x400 + (code - 0xdc00) + 0x10000;
234
+ index -= 1;
235
+ }
236
+ }
237
+ const want = segment[offset];
238
+ if (want !== ANY && want !== foldCode(code)) return false;
239
+ }
240
+ return true;
241
+ }
242
+
243
+ // ---------------------------------------------------------------------------------------------
244
+ // Parameterized search terms
245
+ // ---------------------------------------------------------------------------------------------
246
+
247
+ // JavaScript's `\s` set, by code point.
248
+ function isSpaceCode(code) {
249
+ if (code <= 0x20) return code === 0x20 || (code >= 0x09 && code <= 0x0d);
250
+ if (code < 0xa0) return false;
251
+ return code === 0xa0 || code === 0x1680 || (code >= 0x2000 && code <= 0x200a) || code === 0x2028 || code === 0x2029 || code === 0x202f || code === 0x205f || code === 0x3000 || code === 0xfeff;
252
+ }
253
+
254
+ const TOKEN_BREAK_CODES = new Set([..."@./_-,;:()\"'"].map((char) => char.charCodeAt(0)));
255
+
256
+ /** Fold a search haystack once so every term reuses it. */
257
+ export function prepareHaystack(text) {
258
+ return foldText(text);
259
+ }
260
+
261
+ /**
262
+ * Compile the terms of one search string into `(prepared) => boolean` (true when every term matches).
263
+ * A term matches a case-insensitive prefix of a token that starts at the beginning of the haystack or
264
+ * after whitespace or one of `@ . / _ - , ; : ( ) " '`; `*` matches any run of non-whitespace
265
+ * characters and `?` exactly one non-whitespace character. All terms run as one shift-and automaton:
266
+ * bit j is "the term owning symbol j has matched through symbol j here".
267
+ */
268
+ export function compileTerms(terms) {
269
+ const symbols = [];
270
+ const starts = [];
271
+ const accepts = [];
272
+ const loops = [];
273
+ const leading = [];
274
+ for (const term of terms) {
275
+ const codes = [];
276
+ const starAfter = [];
277
+ let leadingStar = false;
278
+ for (const char of term) {
279
+ if (char === "*") {
280
+ if (codes.length === 0) leadingStar = true;
281
+ else starAfter[codes.length - 1] = true;
282
+ } else codes.push(char === "?" ? ANY : foldCode(char.codePointAt(0)));
283
+ }
284
+ if (codes.length === 0) continue; // only stars: matches at the first token start of any haystack
285
+ const base = symbols.length;
286
+ starts.push(base);
287
+ accepts.push(base + codes.length - 1);
288
+ if (leadingStar) leading.push(base);
289
+ codes.forEach((code, offset) => {
290
+ symbols.push(code);
291
+ if (starAfter[offset] === true) loops.push(base + offset);
292
+ });
293
+ }
294
+ const total = symbols.length;
295
+ if (total === 0) return () => true;
296
+ const words = Math.ceil(total / WORD);
297
+ const bits = (positions) => {
298
+ const mask = new Uint32Array(words);
299
+ for (const position of positions) mask[position >>> 5] |= 1 << (position & 31);
300
+ return mask;
301
+ };
302
+ const startMask = bits(starts);
303
+ const leadingMask = bits(leading);
304
+ const acceptMask = bits(accepts);
305
+ const loopMask = bits(loops);
306
+ const notStart = startMask.map((word) => ~word >>> 0);
307
+ const questionMask = bits(symbols.flatMap((code, position) => (code === ANY ? [position] : [])));
308
+ const literalMasks = new Map();
309
+ symbols.forEach((code, position) => {
310
+ if (code === ANY) return;
311
+ if (!literalMasks.has(code)) literalMasks.set(code, isSpaceCode(code) ? new Uint32Array(words) : Uint32Array.from(questionMask));
312
+ literalMasks.get(code)[position >>> 5] |= 1 << (position & 31);
313
+ });
314
+ const blank = new Uint32Array(words);
315
+ const termCount = accepts.length;
316
+
317
+ return (chars) => {
318
+ const n = chars.length;
319
+ const state = new Uint32Array(words);
320
+ const pending = Uint32Array.from(acceptMask);
321
+ let remaining = termCount;
322
+ let previous = -1; // code before position i (-1 at the start)
323
+ let inToken = false; // some token start s <= i has no whitespace in [s, i)
324
+ for (let i = 0; i < n; i += 1) {
325
+ const code = chars[i];
326
+ const previousSpace = previous >= 0 && isSpaceCode(previous);
327
+ const tokenStart = previous < 0 || previousSpace || TOKEN_BREAK_CODES.has(previous);
328
+ inToken = tokenStart || (inToken && !previousSpace);
329
+ const space = isSpaceCode(code);
330
+ const mask = literalMasks.get(code) ?? (space ? blank : questionMask);
331
+ let carry = 0;
332
+ for (let w = 0; w < words; w += 1) {
333
+ const current = state[w];
334
+ let inject = 0;
335
+ if (tokenStart) inject |= startMask[w];
336
+ else if (inToken) inject |= leadingMask[w];
337
+ const shifted = (((current << 1) | carry) & notStart[w]) | inject;
338
+ carry = current >>> 31;
339
+ const next = (shifted & mask[w]) | (space ? 0 : current & loopMask[w]);
340
+ state[w] = next;
341
+ const hit = next & pending[w];
342
+ if (hit !== 0) {
343
+ pending[w] &= ~hit;
344
+ for (let bit = hit; bit !== 0; bit &= bit - 1) remaining -= 1;
345
+ if (remaining === 0) return true;
346
+ }
347
+ }
348
+ previous = code;
349
+ }
350
+ return false;
351
+ };
352
+ }