jev-mcp-python 0.1.0__py3-none-any.whl

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 (65) hide show
  1. jev_mcp/__init__.py +1 -0
  2. jev_mcp/__main__.py +3 -0
  3. jev_mcp/domain/__init__.py +32 -0
  4. jev_mcp/domain/answers.py +25 -0
  5. jev_mcp/domain/json.py +49 -0
  6. jev_mcp/domain/questions.py +75 -0
  7. jev_mcp/domain/usage.py +16 -0
  8. jev_mcp/errors.py +59 -0
  9. jev_mcp/extract/__init__.py +1 -0
  10. jev_mcp/extract/candidates.py +75 -0
  11. jev_mcp/extract/dialect.py +400 -0
  12. jev_mcp/extract/executor.py +118 -0
  13. jev_mcp/extract/worker.py +198 -0
  14. jev_mcp/ids.py +49 -0
  15. jev_mcp/limits.py +218 -0
  16. jev_mcp/policy/__init__.py +98 -0
  17. jev_mcp/policy/actions.py +41 -0
  18. jev_mcp/policy/claims.py +103 -0
  19. jev_mcp/policy/extract.py +73 -0
  20. jev_mcp/policy/ranking.py +41 -0
  21. jev_mcp/policy/review.py +73 -0
  22. jev_mcp/policy/screen.py +48 -0
  23. jev_mcp/policy/thresholds.py +74 -0
  24. jev_mcp/providers/__init__.py +26 -0
  25. jev_mcp/providers/base.py +236 -0
  26. jev_mcp/providers/cloudflare.py +59 -0
  27. jev_mcp/providers/compatible.py +43 -0
  28. jev_mcp/providers/openrouter.py +47 -0
  29. jev_mcp/providers/resolver.py +106 -0
  30. jev_mcp/providers/typesafe.py +127 -0
  31. jev_mcp/py.typed +0 -0
  32. jev_mcp/serialize.py +199 -0
  33. jev_mcp/server.py +176 -0
  34. jev_mcp/settings.py +73 -0
  35. jev_mcp/stdio.py +99 -0
  36. jev_mcp/telemetry.py +223 -0
  37. jev_mcp/text.py +42 -0
  38. jev_mcp/tools/__init__.py +20 -0
  39. jev_mcp/tools/arguments.py +447 -0
  40. jev_mcp/tools/base.py +153 -0
  41. jev_mcp/tools/classify.py +187 -0
  42. jev_mcp/tools/common.py +96 -0
  43. jev_mcp/tools/compare.py +143 -0
  44. jev_mcp/tools/decide.py +206 -0
  45. jev_mcp/tools/extract.py +262 -0
  46. jev_mcp/tools/find.py +113 -0
  47. jev_mcp/tools/gate.py +236 -0
  48. jev_mcp/tools/observed.py +69 -0
  49. jev_mcp/tools/rerank.py +139 -0
  50. jev_mcp/tools/review.py +236 -0
  51. jev_mcp/tools/screen.py +126 -0
  52. jev_mcp/tools/toolset.py +92 -0
  53. jev_mcp/tools/verify.py +141 -0
  54. jev_mcp/validation/__init__.py +25 -0
  55. jev_mcp/validation/caps.py +93 -0
  56. jev_mcp/validation/choice.py +65 -0
  57. jev_mcp/validation/extract.py +48 -0
  58. jev_mcp/validation/noul.py +15 -0
  59. jev_mcp/validation/numbers.py +21 -0
  60. jev_mcp/validation/score.py +20 -0
  61. jev_mcp_python-0.1.0.dist-info/METADATA +18 -0
  62. jev_mcp_python-0.1.0.dist-info/RECORD +65 -0
  63. jev_mcp_python-0.1.0.dist-info/WHEEL +4 -0
  64. jev_mcp_python-0.1.0.dist-info/entry_points.txt +2 -0
  65. jev_mcp_python-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,400 @@
1
+ """The jev_extract regex dialect: an ECMAScript subset, translated to Python and matched in UTF-16 units (ADR-0004).
2
+
3
+ Both the pattern and the document are read as UTF-16 code units, one Python character per unit
4
+ (`to_units`), so `.`, quantifiers, and classes see what V8 sees without the `u` flag. The translation
5
+ spells every construct out explicitly, so no Python default leaks in:
6
+
7
+ - `.` is one unit other than U+000A, U+000D, U+2028, U+2029. `^` and `$` are the ends of the input.
8
+ - `\\d`, `\\w`, `\\b` are ASCII. `\\s` is ECMAScript WhiteSpace plus LineTerminator.
9
+ - Literals are escaped as `\\uXXXX`, so a JS literal `{` or `]` can never become Python syntax.
10
+ - `i` is ASCII-only case folding, and is refused when a literal or class endpoint is not ASCII.
11
+
12
+ Anything else is refused with a named reason rather than guessed at: the flags `d m s u v y`, named
13
+ groups, backreferences, legacy octal escapes, property escapes, `\\u{...}`, lookbehind that is not
14
+ fixed-length, quantified assertions, a variable repeat of a group that can match empty (V8 and `re`
15
+ treat its empty iterations differently), and alphanumeric identity escapes. Flags V8 itself rejects keep
16
+ V8's message, so that text matches the reference.
17
+ """
18
+
19
+ import re
20
+ import sys
21
+ from array import array
22
+ from dataclasses import dataclass
23
+ from typing import NoReturn
24
+
25
+ V8_FLAGS = "dgimsuvy"
26
+ SUBSET_REFUSED_FLAGS = "dmsuvy"
27
+ MAX_QUANTIFIER_BOUND = 2**31 - 1
28
+
29
+ _UNIT_CODEC = "utf-16-le" if sys.byteorder == "little" else "utf-16-be"
30
+ _WORD = "A-Za-z0-9_"
31
+ _WHITESPACE_UNITS = (
32
+ 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x20, 0xA0, 0x1680,
33
+ *range(0x2000, 0x200B), 0x2028, 0x2029, 0x202F, 0x205F, 0x3000, 0xFEFF,
34
+ ) # fmt: skip
35
+ _LINE_TERMINATORS = (0x0A, 0x0D, 0x2028, 0x2029)
36
+
37
+
38
+ class PatternRejected(Exception):
39
+ """The pattern or flags are invalid, or outside the subset. `str()` is the `invalid_pattern` reason."""
40
+
41
+
42
+ @dataclass(frozen=True, slots=True)
43
+ class Translated:
44
+ """A Python `re` pattern over unit-space text, and its compile flags."""
45
+
46
+ source: str
47
+ flags: int
48
+
49
+
50
+ def to_units(text: str) -> str:
51
+ """One character per UTF-16 code unit, each with the unit's value. Lone surrogates are units too."""
52
+ units = array("H")
53
+ units.frombytes(text.encode(_UNIT_CODEC, "surrogatepass"))
54
+ return "".join(map(chr, units))
55
+
56
+
57
+ def from_units(units: str) -> str:
58
+ """The text a unit-space string stands for: pairs rejoin, a lone surrogate stays one code point."""
59
+ return units.encode(_UNIT_CODEC, "surrogatepass").decode(_UNIT_CODEC, "surrogatepass")
60
+
61
+
62
+ def translate(pattern: str, flags: str) -> Translated:
63
+ """The Python translation of JS `new RegExp(pattern, flags)`, or `PatternRejected`.
64
+
65
+ `flags` is already normalized as the reference does: letters only, with one `g`.
66
+ """
67
+ if any(flag not in V8_FLAGS for flag in flags) or len(set(flags)) != len(flags) or {"u", "v"} <= set(flags):
68
+ raise PatternRejected(f"Invalid flags supplied to RegExp constructor '{flags}'")
69
+ for flag in flags:
70
+ if flag in SUBSET_REFUSED_FLAGS:
71
+ _refuse(f"the '{flag}' flag")
72
+ parser = _Parser(to_units(pattern))
73
+ try:
74
+ body = parser.parse()
75
+ except RecursionError:
76
+ _refuse("group nesting this deep")
77
+ ignore_case = "i" in flags
78
+ if ignore_case and parser.non_ascii:
79
+ _refuse("the 'i' flag with a non-ASCII character in the pattern")
80
+ python_flags = re.ASCII | (re.IGNORECASE if ignore_case else 0)
81
+ try:
82
+ re.compile(body.text, python_flags)
83
+ except (re.error, RecursionError, OverflowError) as error:
84
+ _refuse(f"the pattern cannot be matched here ({error})")
85
+ return Translated(body.text, python_flags)
86
+
87
+
88
+ def _refuse(what: str) -> NoReturn:
89
+ raise PatternRejected(f"unsupported regular expression: {what} is outside the supported subset")
90
+
91
+
92
+ def _invalid(what: str) -> NoReturn:
93
+ raise PatternRejected(f"invalid regular expression: {what}")
94
+
95
+
96
+ @dataclass(frozen=True, slots=True)
97
+ class _Piece:
98
+ text: str
99
+ min: int
100
+ max: int | None
101
+ """Match length bounds in units; `None` is unbounded."""
102
+ assertion: bool = False
103
+ lookaround: bool = False
104
+ """Contains a lookaround somewhere inside."""
105
+
106
+
107
+ def _unit(value: int) -> str:
108
+ """One unit as Python regex syntax: ASCII letters and digits raw, everything else escaped."""
109
+ char = chr(value)
110
+ return char if char.isascii() and char.isalnum() else f"\\u{value:04x}"
111
+
112
+
113
+ def _ranges(units: tuple[int, ...] | list[int]) -> str:
114
+ return "".join(_unit(u) for u in units)
115
+
116
+
117
+ def _complement(members: set[int]) -> str:
118
+ """Class content matching every unit (0-FFFF) outside `members`, as ranges."""
119
+ out: list[str] = []
120
+ start = 0
121
+ for member in [*sorted(members), 0x10000]:
122
+ if member > start:
123
+ out.append(_unit(start) if member - 1 == start else f"{_unit(start)}-{_unit(member - 1)}")
124
+ start = member + 1
125
+ return "".join(out)
126
+
127
+
128
+ _DIGITS = set(range(0x30, 0x3A))
129
+ _WORDS = _DIGITS | set(range(0x41, 0x5B)) | set(range(0x61, 0x7B)) | {0x5F}
130
+ _CLASS_ESCAPES = {
131
+ "d": "0-9",
132
+ "w": _WORD,
133
+ "s": _ranges(_WHITESPACE_UNITS),
134
+ "D": _complement(_DIGITS),
135
+ "W": _complement(_WORDS),
136
+ "S": _complement(set(_WHITESPACE_UNITS)),
137
+ }
138
+ """Class escapes as class content, so they can stand inside a bracketed class too."""
139
+ _DOT = f"[^{_ranges(_LINE_TERMINATORS)}]"
140
+ _NOTHING = "[^\\u0000-\\uffff]"
141
+ _ANYTHING = "[\\u0000-\\uffff]"
142
+ _BOUNDARY = f"(?:(?<=[{_WORD}])(?![{_WORD}])|(?<![{_WORD}])(?=[{_WORD}]))"
143
+ _NOT_BOUNDARY = f"(?:(?<=[{_WORD}])(?=[{_WORD}])|(?<![{_WORD}])(?![{_WORD}]))"
144
+ _QUANTIFIER = re.compile(r"\{([0-9]+)(,([0-9]*))?\}")
145
+ _ASCII_DIGITS = "0123456789"
146
+ _CONTROL_ESCAPES = {"f": 0x0C, "n": 0x0A, "r": 0x0D, "t": 0x09, "v": 0x0B}
147
+ _HEX = set("0123456789abcdefABCDEF")
148
+
149
+
150
+ class _Parser:
151
+ """Recursive descent over the ECMAScript `Pattern` grammar without `u`, restricted to the subset."""
152
+
153
+ def __init__(self, units: str) -> None:
154
+ self.units = units
155
+ self.at = 0
156
+ self.non_ascii = False
157
+
158
+ def parse(self) -> _Piece:
159
+ body = self._disjunction()
160
+ if self.at < len(self.units):
161
+ _invalid("unmatched ')'") # the only way a top-level disjunction stops early
162
+ return body
163
+
164
+ def _peek(self, offset: int = 0) -> str | None:
165
+ index = self.at + offset
166
+ return self.units[index] if index < len(self.units) else None
167
+
168
+ def _take(self) -> str:
169
+ char = self.units[self.at]
170
+ self.at += 1
171
+ return char
172
+
173
+ def _disjunction(self) -> _Piece:
174
+ alternatives = [self._alternative()]
175
+ while self._peek() == "|":
176
+ self.at += 1
177
+ alternatives.append(self._alternative())
178
+ if len(alternatives) == 1:
179
+ return alternatives[0]
180
+ maxima = [a.max for a in alternatives]
181
+ return _Piece(
182
+ "|".join(a.text for a in alternatives),
183
+ min(a.min for a in alternatives),
184
+ None if None in maxima else max(m for m in maxima if m is not None),
185
+ lookaround=any(a.lookaround for a in alternatives),
186
+ )
187
+
188
+ def _alternative(self) -> _Piece:
189
+ terms: list[_Piece] = []
190
+ while (char := self._peek()) is not None and char not in "|)":
191
+ terms.append(self._term())
192
+ maxima = [t.max for t in terms]
193
+ return _Piece(
194
+ "".join(t.text for t in terms),
195
+ sum(t.min for t in terms),
196
+ None if None in maxima else sum(m for m in maxima if m is not None),
197
+ lookaround=any(t.lookaround for t in terms),
198
+ )
199
+
200
+ def _term(self) -> _Piece:
201
+ atom = self._atom()
202
+ quantifier = self._quantifier()
203
+ if quantifier is None:
204
+ return atom
205
+ low, high, suffix = quantifier
206
+ if atom.assertion and atom.lookaround:
207
+ _refuse("a quantified assertion")
208
+ if atom.assertion:
209
+ _invalid("nothing to repeat")
210
+ if atom.min == 0 and (high is None or high > low):
211
+ # V8 fails an optional iteration that matches empty and backtracks into the body for a
212
+ # longer one; Python's `re` accepts the empty iteration. The two disagree on the match.
213
+ _refuse("a repeated group that can match empty")
214
+ return _Piece(
215
+ f"{atom.text}{suffix}",
216
+ atom.min * low,
217
+ None if high is None or atom.max is None else atom.max * high,
218
+ lookaround=atom.lookaround,
219
+ )
220
+
221
+ def _quantifier(self) -> tuple[int, int | None, str] | None:
222
+ char = self._peek()
223
+ if char in ("*", "+", "?"):
224
+ self.at += 1
225
+ low, high = {"*": (0, None), "+": (1, None), "?": (0, 1)}[char]
226
+ text = char
227
+ elif char == "{" and (braced := _QUANTIFIER.match(self.units, self.at)):
228
+ self.at = braced.end()
229
+ low = int(braced.group(1))
230
+ high = low if braced.group(2) is None else (int(braced.group(3)) if braced.group(3) else None)
231
+ if max(low, high or 0) > MAX_QUANTIFIER_BOUND:
232
+ _refuse("a quantifier bound above 2147483647")
233
+ if high is not None and high < low:
234
+ _invalid("numbers out of order in {} quantifier")
235
+ text = f"{{{low}}}" if braced.group(2) is None else f"{{{low},{'' if high is None else high}}}"
236
+ else:
237
+ return None
238
+ if self._peek() == "?":
239
+ self.at += 1
240
+ text += "?"
241
+ return low, high, text
242
+
243
+ def _atom(self) -> _Piece:
244
+ char = self._take()
245
+ match char:
246
+ case "^":
247
+ return _Piece("\\A", 0, 0, assertion=True)
248
+ case "$":
249
+ return _Piece("\\Z", 0, 0, assertion=True)
250
+ case ".":
251
+ return _Piece(_DOT, 1, 1)
252
+ case "(":
253
+ return self._group()
254
+ case "[":
255
+ return self._class()
256
+ case "\\":
257
+ return self._atom_escape()
258
+ case "*" | "+" | "?":
259
+ _invalid("nothing to repeat")
260
+ case "{" if _QUANTIFIER.match(self.units, self.at - 1):
261
+ _invalid("nothing to repeat")
262
+ case _:
263
+ return self._literal(ord(char))
264
+
265
+ def _literal(self, value: int) -> _Piece:
266
+ if value > 0x7F:
267
+ self.non_ascii = True
268
+ return _Piece(_unit(value), 1, 1)
269
+
270
+ def _group(self) -> _Piece:
271
+ if self._peek() != "?":
272
+ body = self._disjunction()
273
+ self._close_group()
274
+ # Captures are never read (only the whole match is), so every group is non-capturing.
275
+ return _Piece(f"(?:{body.text})", body.min, body.max, lookaround=body.lookaround)
276
+ self.at += 1
277
+ kind = self._peek()
278
+ if kind == ":":
279
+ self.at += 1
280
+ body = self._disjunction()
281
+ self._close_group()
282
+ return _Piece(f"(?:{body.text})", body.min, body.max, lookaround=body.lookaround)
283
+ if kind in ("=", "!"):
284
+ self.at += 1
285
+ body = self._disjunction()
286
+ self._close_group()
287
+ return _Piece(f"(?{kind}{body.text})", 0, 0, assertion=True, lookaround=True)
288
+ if kind == "<" and self._peek(1) in ("=", "!"):
289
+ sign = self._peek(1)
290
+ self.at += 2
291
+ body = self._disjunction()
292
+ self._close_group()
293
+ if body.lookaround:
294
+ _refuse("a lookaround inside a lookbehind")
295
+ if body.max != body.min:
296
+ _refuse("variable-length lookbehind")
297
+ return _Piece(f"(?<{sign}{body.text})", 0, 0, assertion=True, lookaround=True)
298
+ if kind == "<":
299
+ _refuse("a named group")
300
+ _invalid("invalid group")
301
+
302
+ def _close_group(self) -> None:
303
+ if self._peek() != ")":
304
+ _invalid("unterminated group")
305
+ self.at += 1
306
+
307
+ def _atom_escape(self) -> _Piece:
308
+ char = self._peek()
309
+ if char is None:
310
+ _invalid("\\ at end of pattern")
311
+ if char == "b":
312
+ self.at += 1
313
+ return _Piece(_BOUNDARY, 0, 0, assertion=True)
314
+ if char == "B":
315
+ self.at += 1
316
+ return _Piece(_NOT_BOUNDARY, 0, 0, assertion=True)
317
+ if char in _CLASS_ESCAPES:
318
+ self.at += 1
319
+ return _Piece(f"[{_CLASS_ESCAPES[char]}]", 1, 1)
320
+ return self._literal(self._character_escape())
321
+
322
+ def _character_escape(self) -> int:
323
+ """The unit a `\\` escape stands for (the `\\` already consumed), shared by atoms and classes."""
324
+ char = self._take()
325
+ if char in _CONTROL_ESCAPES:
326
+ return _CONTROL_ESCAPES[char]
327
+ if char == "c":
328
+ letter = self._peek()
329
+ if letter is None or not (letter.isascii() and letter.isalpha()):
330
+ _refuse("'\\c' without a control letter")
331
+ self.at += 1
332
+ return ord(letter) % 32
333
+ following = self._peek()
334
+ if char == "0" and (following is None or following not in _ASCII_DIGITS):
335
+ return 0
336
+ if char in _ASCII_DIGITS:
337
+ _refuse("a backreference or legacy octal escape")
338
+ if char in ("x", "u"):
339
+ width = 2 if char == "x" else 4
340
+ digits = self.units[self.at : self.at + width]
341
+ if len(digits) == width and set(digits) <= _HEX:
342
+ self.at += width
343
+ return int(digits, 16)
344
+ _refuse("'\\u{...}'" if char == "u" and self._peek() == "{" else f"an incomplete '\\{char}' escape")
345
+ if char in ("k", "p", "P"):
346
+ _refuse("a named backreference" if char == "k" else "a Unicode property escape")
347
+ if char.isascii() and char.isalnum():
348
+ _refuse(f"the escape '\\{char}'")
349
+ return ord(char)
350
+
351
+ def _class(self) -> _Piece:
352
+ negate = self._peek() == "^"
353
+ if negate:
354
+ self.at += 1
355
+ members: list[str] = []
356
+ while True:
357
+ char = self._peek()
358
+ if char is None:
359
+ _invalid("unterminated character class")
360
+ if char == "]":
361
+ self.at += 1
362
+ break
363
+ start = self._class_atom()
364
+ if self._peek() == "-" and self._peek(1) not in (None, "]"):
365
+ self.at += 1
366
+ end = self._class_atom()
367
+ if isinstance(start, str) or isinstance(end, str):
368
+ _refuse("a class range with a class escape endpoint")
369
+ if start > end:
370
+ _invalid("range out of order in character class")
371
+ members.append(f"{self._endpoint(start)}-{self._endpoint(end)}")
372
+ else:
373
+ members.append(start if isinstance(start, str) else self._endpoint(start))
374
+ if not members:
375
+ return _Piece(_ANYTHING if negate else _NOTHING, 1, 1)
376
+ return _Piece(f"[{'^' if negate else ''}{''.join(members)}]", 1, 1)
377
+
378
+ def _endpoint(self, value: int) -> str:
379
+ if value > 0x7F:
380
+ self.non_ascii = True
381
+ return _unit(value)
382
+
383
+ def _class_atom(self) -> int | str:
384
+ """A unit, or a class escape's content (`str`)."""
385
+ char = self._take()
386
+ if char != "\\":
387
+ return ord(char)
388
+ escaped = self._peek()
389
+ if escaped is None:
390
+ _invalid("\\ at end of pattern")
391
+ if escaped == "b":
392
+ self.at += 1
393
+ return 0x08
394
+ if escaped == "-":
395
+ self.at += 1
396
+ return ord("-")
397
+ if escaped in _CLASS_ESCAPES:
398
+ self.at += 1
399
+ return _CLASS_ESCAPES[escaped]
400
+ return self._character_escape()
@@ -0,0 +1,118 @@
1
+ """The regex execution port (ADR-0016): an executor compiles and matches a translated pattern.
2
+
3
+ `RegexExecutor.find` runs one translated pattern over unit-space text under an absolute monotonic
4
+ deadline and answers with a `MatchResult`: `Matches`, or one of three plain refusals — `Timeout`
5
+ (the deadline passed), `Saturated` (admission refused at the queue bound, ADR-0025), `Invalid` (no
6
+ result: the pattern did not compile or the executor could not produce one). Results carry no
7
+ caller-facing text; `extract/candidates.py` owns what each one tells the caller.
8
+
9
+ Two adapters: `worker.ProcessRegexExecutor`, killable worker processes for production, and
10
+ `InProcessRegexExecutor` here, which cannot stop a runaway match and exists for trusted corpora
11
+ such as the Node differential test.
12
+ """
13
+
14
+ import re
15
+ from dataclasses import dataclass
16
+ from enum import Enum
17
+ from time import monotonic
18
+ from typing import Protocol
19
+
20
+ from jev_mcp.extract.dialect import Translated
21
+ from jev_mcp.limits import EXTRACT, ExtractCaps
22
+
23
+
24
+ @dataclass(frozen=True, slots=True)
25
+ class Matches:
26
+ """The kept matches, in unit space (`dialect.from_units` maps them back)."""
27
+
28
+ candidates: list[str]
29
+ truncated: bool
30
+ too_long: int
31
+
32
+
33
+ @dataclass(frozen=True, slots=True)
34
+ class Timeout:
35
+ """The deadline passed before a result; a process executor killed the run."""
36
+
37
+
38
+ @dataclass(frozen=True, slots=True)
39
+ class Saturated:
40
+ """Admission refused at the queue bound: no time was spent, nothing ran."""
41
+
42
+
43
+ class Unavailable(Enum):
44
+ NO_RESULT = "no_result"
45
+ """The run ended without a result: the pattern did not compile, or its worker exited."""
46
+ NOT_STARTED = "not_started"
47
+ """The executor could not start somewhere to run the pattern."""
48
+
49
+
50
+ @dataclass(frozen=True, slots=True)
51
+ class Invalid:
52
+ cause: Unavailable
53
+
54
+
55
+ type MatchResult = Matches | Timeout | Saturated | Invalid
56
+
57
+
58
+ class RegexExecutor(Protocol):
59
+ async def find(self, pattern: Translated, text: str, *, deadline: float) -> MatchResult:
60
+ """Match `pattern` over unit-space `text`, finishing by monotonic `deadline`."""
61
+ ...
62
+
63
+ async def aclose(self) -> None: ...
64
+
65
+
66
+ def match_all(pattern: Translated, units: str, max_candidates: int, max_units: int) -> Matches:
67
+ """`document.matchAll` with the reference's frozen pipeline (`index.ts:894-903`).
68
+
69
+ Drop zero-length matches, dedup by value (first wins), skip and count matches over `max_units`,
70
+ then stop at `max_candidates` kept, flagging truncation. After an empty match the scan moves one
71
+ unit on, as `AdvanceStringIndex` does without `u`; `finditer` would retry the same position.
72
+ Raises `re.error` if the pattern does not compile.
73
+ """
74
+ compiled = re.compile(pattern.source, pattern.flags)
75
+ seen: set[str] = set()
76
+ candidates: list[str] = []
77
+ truncated = False
78
+ too_long = 0
79
+ position = 0
80
+ while position <= len(units):
81
+ match = compiled.search(units, position)
82
+ if match is None:
83
+ break
84
+ start, end = match.span()
85
+ position = end if end > start else end + 1
86
+ value = units[start:end]
87
+ if not value or value in seen:
88
+ continue
89
+ seen.add(value)
90
+ if len(value) > max_units:
91
+ too_long += 1
92
+ continue
93
+ if len(candidates) >= max_candidates:
94
+ truncated = True
95
+ break
96
+ candidates.append(value)
97
+ return Matches(candidates, truncated, too_long)
98
+
99
+
100
+ class InProcessRegexExecutor:
101
+ """Matches on the calling thread. `re` cannot be interrupted mid-match, so a result that lands
102
+ after the deadline is reported as `Timeout`, but a catastrophic pattern still blocks the caller:
103
+ never serve untrusted patterns with it."""
104
+
105
+ def __init__(self, caps: ExtractCaps = EXTRACT) -> None:
106
+ self._caps = caps
107
+
108
+ async def find(self, pattern: Translated, text: str, *, deadline: float) -> MatchResult:
109
+ if monotonic() >= deadline:
110
+ return Timeout()
111
+ try:
112
+ matches = match_all(pattern, text, self._caps.candidates_per_field, self._caps.candidate_units)
113
+ except re.error:
114
+ return Invalid(Unavailable.NO_RESULT)
115
+ return Timeout() if monotonic() > deadline else matches
116
+
117
+ async def aclose(self) -> None:
118
+ return None