jev-agent-tools 0.1.4 → 0.2.0

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 (131) hide show
  1. package/CHANGELOG.md +70 -1
  2. package/CONTRIBUTING.md +40 -0
  3. package/README.md +42 -9
  4. package/SECURITY.md +27 -0
  5. package/dist/adapters/analysis-context.js +75 -0
  6. package/dist/adapters/ask-files.js +189 -0
  7. package/dist/adapters/ask-proof.js +144 -0
  8. package/dist/adapters/ask-syntax.js +385 -0
  9. package/dist/adapters/canonical-path.js +17 -0
  10. package/dist/adapters/command.js +181 -0
  11. package/dist/adapters/docs.js +172 -0
  12. package/dist/adapters/exec.js +207 -0
  13. package/dist/adapters/files.js +293 -0
  14. package/dist/adapters/find.js +122 -0
  15. package/dist/adapters/git-base.js +26 -0
  16. package/dist/adapters/git-inventory.js +71 -0
  17. package/dist/adapters/git.js +439 -0
  18. package/dist/adapters/locate-file.js +159 -0
  19. package/dist/adapters/output-lines.js +46 -0
  20. package/dist/adapters/private-storage.js +98 -0
  21. package/dist/adapters/risk-callers.js +426 -0
  22. package/dist/adapters/runner-version.js +78 -0
  23. package/dist/adapters/shell.js +76 -0
  24. package/dist/adapters/syntax.js +187 -0
  25. package/dist/adapters/test-inventory.js +131 -0
  26. package/dist/adapters/usage.js +20 -0
  27. package/dist/adapters/utf8.js +47 -0
  28. package/dist/configuration.js +257 -0
  29. package/dist/constants.js +119 -0
  30. package/dist/core/ask-closure.js +282 -0
  31. package/dist/core/ask-proof.js +1 -0
  32. package/dist/core/ask-references.js +194 -0
  33. package/dist/core/asks.js +436 -0
  34. package/dist/core/batches.js +65 -0
  35. package/dist/core/command-output.js +224 -0
  36. package/dist/core/diff.js +178 -0
  37. package/dist/core/docs.js +302 -0
  38. package/dist/core/find.js +108 -0
  39. package/dist/core/git.js +1 -0
  40. package/dist/core/imports.js +550 -0
  41. package/dist/core/integrity.js +45 -0
  42. package/dist/core/lexical.js +132 -0
  43. package/dist/core/locate.js +169 -0
  44. package/dist/core/output.js +120 -0
  45. package/dist/core/pointer.js +29 -0
  46. package/dist/core/risk-callers.js +851 -0
  47. package/dist/core/runner-version.js +45 -0
  48. package/dist/core/sections.js +230 -0
  49. package/dist/core/state.js +44 -0
  50. package/dist/core/syntax.js +1 -0
  51. package/dist/core/test-commands.js +334 -0
  52. package/dist/core/test-coverage.js +74 -0
  53. package/dist/core/test-discovery.js +1382 -0
  54. package/dist/core/test-evidence.js +527 -0
  55. package/dist/core/test-state.js +81 -0
  56. package/dist/core/truncate.js +12 -0
  57. package/dist/core/units.js +349 -0
  58. package/dist/describe.js +23 -0
  59. package/dist/guide.js +33 -0
  60. package/dist/host.js +24 -0
  61. package/dist/jev/client.js +434 -0
  62. package/dist/jev/pool.js +54 -0
  63. package/dist/jev/types.js +1 -0
  64. package/dist/mcp/main.js +124 -0
  65. package/dist/mcp/protocol.js +187 -0
  66. package/dist/mcp/tools.js +116 -0
  67. package/dist/presets/docs.js +62 -0
  68. package/dist/presets/risk.js +179 -0
  69. package/dist/presets/spec.js +81 -0
  70. package/dist/presets/witnesses.js +249 -0
  71. package/dist/render.js +42 -0
  72. package/dist/result.js +3 -0
  73. package/dist/runtime.js +1 -0
  74. package/dist/session.js +147 -0
  75. package/dist/texts/ask-files.js +1 -0
  76. package/dist/texts/ask.js +2 -0
  77. package/dist/texts/check-diff.js +17 -0
  78. package/dist/texts/configuration.js +1 -0
  79. package/dist/texts/find.js +14 -0
  80. package/dist/texts/guide.js +16 -0
  81. package/dist/texts/locate.js +10 -0
  82. package/dist/texts/select-tests.js +2 -0
  83. package/dist/tools/ask-files.js +217 -0
  84. package/dist/tools/ask-schema.js +70 -0
  85. package/dist/tools/ask.js +686 -0
  86. package/dist/tools/check-diff.js +402 -0
  87. package/dist/tools/docs-check.js +299 -0
  88. package/dist/tools/find.js +389 -0
  89. package/dist/tools/locate.js +303 -0
  90. package/dist/tools/select-tests.js +567 -0
  91. package/dist/tools/spec-check.js +166 -0
  92. package/docs/adr/0001-strict-typescript-pure-core-offline-tests.md +31 -0
  93. package/docs/adr/0002-one-http-protocol-across-hosts.md +17 -0
  94. package/docs/adr/0003-explicit-scope-conservative-automation.md +19 -0
  95. package/docs/adr/0004-compiled-typed-intents.md +19 -0
  96. package/docs/adr/0005-evidence-construction-before-judgment.md +19 -0
  97. package/docs/adr/0006-visible-uncertainty-constrained-controls.md +21 -0
  98. package/docs/adr/0007-bounded-evidence-visible-limits.md +21 -0
  99. package/docs/adr/0008-static-test-discovery-conservative-plans.md +19 -0
  100. package/docs/adr/0009-session-cache-requested-model-identity.md +17 -0
  101. package/docs/adr/0010-mcp-server-thin-host.md +23 -0
  102. package/docs/agent-instructions.md +91 -0
  103. package/docs/design.md +3 -3
  104. package/docs/mcp.md +231 -0
  105. package/package.json +19 -4
  106. package/server.json +57 -0
  107. package/src/adapters/canonical-path.ts +18 -0
  108. package/src/adapters/command.ts +7 -4
  109. package/src/adapters/exec.ts +226 -0
  110. package/src/adapters/private-storage.ts +143 -0
  111. package/src/adapters/risk-callers.ts +4 -2
  112. package/src/adapters/shell.ts +97 -0
  113. package/src/configuration.ts +39 -12
  114. package/src/constants.ts +11 -0
  115. package/src/core/command-output.ts +17 -1
  116. package/src/host.ts +11 -0
  117. package/src/jev/client.ts +12 -0
  118. package/src/jev/types.ts +6 -0
  119. package/src/mcp/main.ts +135 -0
  120. package/src/mcp/protocol.ts +282 -0
  121. package/src/mcp/tools.ts +166 -0
  122. package/src/session.ts +59 -0
  123. package/src/setup.ts +13 -5
  124. package/src/tools/ask-files.ts +5 -7
  125. package/src/tools/ask.ts +26 -22
  126. package/src/tools/check-diff.ts +8 -5
  127. package/src/tools/docs-check.ts +1 -0
  128. package/src/tools/find.ts +5 -2
  129. package/src/tools/locate.ts +5 -8
  130. package/src/tools/select-tests.ts +7 -4
  131. package/src/tools/spec-check.ts +1 -0
@@ -0,0 +1,224 @@
1
+ import { OUTPUT_CHUNK_CHARS, OUTPUT_FAILURE_WINDOW_LINES, OUTPUT_HEAD_SHARE, OUTPUT_REPEAT_MIN, OUTPUT_TAIL_SHARE, } from "../constants.js";
2
+ const terminalSequences =
3
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI stripping deliberately matches terminal control sequences.
4
+ /\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1b\\))/g;
5
+ export function cleanOutput(text) {
6
+ return text.replace(terminalSequences, "").replace(/\r\n?/g, "\n");
7
+ }
8
+ export function lineShape(line) {
9
+ return line
10
+ .replace(/\d+/g, "0")
11
+ .replace(/[\p{L}_][\p{L}\p{N}_]*/gu, (word) => /^[A-Z_]+$/.test(word) ? word : "x");
12
+ }
13
+ export function createRarityCompressor(frequencies, emit) {
14
+ const markerFor = (hidden) => `[… ${hidden} repetitive lines …]`;
15
+ let first = "";
16
+ let last = "";
17
+ let count = 0;
18
+ // Length of the buffered run joined with newlines, without building it.
19
+ let length = 0;
20
+ let buffer = [];
21
+ let folding = false;
22
+ const flush = () => {
23
+ if (!count)
24
+ return;
25
+ if (folding) {
26
+ emit(first);
27
+ if (count > 2)
28
+ emit(markerFor(count - 2));
29
+ if (count > 1)
30
+ emit(last);
31
+ }
32
+ else {
33
+ for (const line of buffer)
34
+ emit(line);
35
+ }
36
+ first = "";
37
+ last = "";
38
+ count = 0;
39
+ length = 0;
40
+ buffer = [];
41
+ folding = false;
42
+ };
43
+ return {
44
+ line(line) {
45
+ if ((frequencies.get(lineShape(line)) ?? 0) >= OUTPUT_REPEAT_MIN) {
46
+ if (folding) {
47
+ last = line;
48
+ count++;
49
+ }
50
+ else {
51
+ if (!count)
52
+ first = line;
53
+ last = line;
54
+ count++;
55
+ buffer.push(line);
56
+ length += line.length + (count > 1 ? 1 : 0);
57
+ // The fold decision is monotonic: once the folded text is shorter,
58
+ // later lines keep it shorter, so switching once is safe.
59
+ if (count > 2) {
60
+ const marker = markerFor(count - 2);
61
+ if (length > first.length + 1 + marker.length + 1 + last.length) {
62
+ folding = true;
63
+ buffer = [];
64
+ }
65
+ }
66
+ }
67
+ }
68
+ else {
69
+ flush();
70
+ emit(line);
71
+ }
72
+ },
73
+ finish: flush,
74
+ };
75
+ }
76
+ export function outputChunks(text) {
77
+ const chunks = [];
78
+ let start = 0;
79
+ while (start < text.length) {
80
+ let end = Math.min(start + OUTPUT_CHUNK_CHARS, text.length);
81
+ if (end < text.length) {
82
+ const newline = text.lastIndexOf("\n", end - 1);
83
+ if (newline >= start)
84
+ end = newline + 1;
85
+ if (end < text.length && /[\uD800-\uDBFF]/.test(text[end - 1] ?? ""))
86
+ end--;
87
+ }
88
+ chunks.push({ start, end, text: text.slice(start, end) });
89
+ start = end;
90
+ }
91
+ return chunks;
92
+ }
93
+ export function selectOutput(text, chunks, scores, budget) {
94
+ if (JSON.stringify(text).length <= budget)
95
+ return text;
96
+ const ranges = [];
97
+ const head = Math.floor(budget * OUTPUT_HEAD_SHARE);
98
+ const tail = Math.floor(budget * OUTPUT_TAIL_SHARE);
99
+ if (head)
100
+ ranges.push({ start: 0, end: head });
101
+ if (tail)
102
+ ranges.push({ start: Math.max(0, text.length - tail), end: text.length });
103
+ const render = () => {
104
+ const sorted = [...ranges].sort((a, b) => a.start - b.start);
105
+ let end = 0;
106
+ let result = "";
107
+ for (const range of sorted) {
108
+ if (range.start > end)
109
+ result += `[… ${range.start - end} chars omitted …]\n`;
110
+ if (range.end > end)
111
+ result += text.slice(Math.max(end, range.start), range.end);
112
+ end = Math.max(end, range.end);
113
+ }
114
+ if (end < text.length)
115
+ result += `\n[… ${text.length - end} chars omitted …]`;
116
+ return result;
117
+ };
118
+ for (const candidate of chunks
119
+ .map((chunk, index) => ({ chunk, score: scores[index] ?? 0 }))
120
+ .filter(({ score }) => score >= 0.5)
121
+ .sort((a, b) => b.score - a.score || a.chunk.start - b.chunk.start)) {
122
+ ranges.push(candidate.chunk);
123
+ if (JSON.stringify(render()).length > budget)
124
+ ranges.pop();
125
+ }
126
+ return render();
127
+ }
128
+ export function failureTargets(text) {
129
+ const targets = [];
130
+ let signature;
131
+ const lines = text.split("\n");
132
+ const firstFailure = lines.findIndex((line) => /^\S+\.py:\d+: \w*(?:Assertion)?Error$|^E\s+assert\b|^\s*(?:AssertionError\b|E\s+AssertionError\b)|_test\.go:\d+:.*\b(?:want|expected)\b|--- FAIL|^FAILED |^ERROR |^\s*(?:FAIL|❯|×|✖)\s|no such file or directory|ECONNREFUSED|permission denied|error TS\d+|undefined:|ModuleNotFoundError/i.test(line));
133
+ if (firstFailure < 0)
134
+ return { assertion: false, targets };
135
+ // node:test prints its failing-test block ("✖ failing tests:" then
136
+ // "test at <file>:<line>:<col>") after one summary line per test, so the
137
+ // location can sit more than one window past the first anchor. Only the
138
+ // header re-anchors: a bare "test at" line can also be printed by a
139
+ // passing test and must not open a window on its own.
140
+ const secondAnchor = lines.findIndex((line, index) => index > firstFailure && /^\s*(?:✖\s+)?failing tests:?\s*$/.test(line));
141
+ for (const [position, line] of lines.entries()) {
142
+ const inSecondWindow = secondAnchor >= 0 &&
143
+ position >= secondAnchor &&
144
+ position <= secondAnchor + OUTPUT_FAILURE_WINDOW_LINES;
145
+ if ((position < firstFailure - OUTPUT_FAILURE_WINDOW_LINES ||
146
+ position > firstFailure + OUTPUT_FAILURE_WINDOW_LINES) &&
147
+ !inSecondWindow)
148
+ continue;
149
+ if (/^\s*(?:--- PASS|=== RUN|.*\bPASSED\b)/.test(line))
150
+ continue;
151
+ if ((position >= firstFailure || inSecondWindow) &&
152
+ !signature &&
153
+ /no such file or directory|ECONNREFUSED|permission denied|refusing to allow|error TS\d+|undefined:|ModuleNotFoundError/i.test(line))
154
+ signature = "environment";
155
+ if ((position >= firstFailure || inSecondWindow) &&
156
+ !signature &&
157
+ /^\S+\.py:\d+: \w*(?:Assertion)?Error$|^E\s+assert\b|^\s*(?:AssertionError\b|E\s+AssertionError\b)|_test\.go:\d+:.*\b(?:want|expected)\b|--- FAIL|^FAILED |^\s*(?:FAIL|❯|×|✖)\s|^\s*test at .+?:\d+:\d+\s*$/.test(line))
158
+ signature = "assertion";
159
+ const inFirstWindow = position >= firstFailure - OUTPUT_FAILURE_WINDOW_LINES &&
160
+ position <= firstFailure + OUTPUT_FAILURE_WINDOW_LINES;
161
+ const pytest = /^(FAILED|ERROR)\s+(.+?)(?: - |$)/.exec(line);
162
+ if (pytest) {
163
+ const nodeId = pytest[2]?.trim() ?? "";
164
+ targets.push({
165
+ path: nodeId.split("::")[0] ?? "",
166
+ nodeId,
167
+ assertion: pytest[1] === "FAILED",
168
+ });
169
+ }
170
+ if (firstFailure >= 0) {
171
+ const go = lines
172
+ .slice(firstFailure, firstFailure + OUTPUT_FAILURE_WINDOW_LINES + 1)
173
+ .join("\n")
174
+ .match(/--- FAIL:\s+(\S+)/);
175
+ const run = go
176
+ ? lines.findLastIndex((item) => item.startsWith(`=== RUN ${go[1]}`))
177
+ : -1;
178
+ const failLine = go
179
+ ? lines.findIndex((item, index) => index >= firstFailure && item.includes(`--- FAIL: ${go[1]}`))
180
+ : -1;
181
+ if (go &&
182
+ position >= Math.max(run, firstFailure - 1) &&
183
+ position <= failLine) {
184
+ for (const match of line.matchAll(/(?:^|\s|\()([^\s():]+_test\.go):\d+/g))
185
+ targets.push({
186
+ path: match[1] ?? "",
187
+ testName: go[1]?.split("/")[0],
188
+ assertion: true,
189
+ });
190
+ }
191
+ }
192
+ const js = /^\s*(?:FAIL|❯|×)\s+(.+?\.[cm]?[jt]sx?)(?:\s|$)/.exec(line);
193
+ if (js)
194
+ targets.push({ path: js[1] ?? "", assertion: true });
195
+ const node = /^\s*test at (.+?):\d+:\d+\s*$/.exec(line);
196
+ // A passing node:test can print "test at ..." as ordinary output; it
197
+ // only counts inside the failing-tests block, or inside the first
198
+ // window when no failing-tests block exists (short outputs).
199
+ if (node && (inSecondWindow || (secondAnchor < 0 && inFirstWindow)))
200
+ targets.push({
201
+ path: nodeTestPath(node[1] ?? ""),
202
+ assertion: true,
203
+ });
204
+ }
205
+ return { assertion: signature === "assertion", signature, targets };
206
+ }
207
+ /**
208
+ * node:test reports absolute locations as file URLs: file:///repo/a.mjs on
209
+ * POSIX and file:///C:/repo/a%20b.mjs on Windows. Convert them to plain,
210
+ * decoded paths; other locations are returned unchanged.
211
+ */
212
+ export function nodeTestPath(location) {
213
+ if (!location.startsWith("file://"))
214
+ return location;
215
+ let path = location.slice("file://".length);
216
+ if (/^\/[A-Za-z]:\//.test(path))
217
+ path = path.slice(1);
218
+ try {
219
+ return decodeURIComponent(path);
220
+ }
221
+ catch {
222
+ return path;
223
+ }
224
+ }
@@ -0,0 +1,178 @@
1
+ import { UNIT_MAX_CHARS } from "../constants.js";
2
+ import { truncate } from "./truncate.js";
3
+ function quotePath(path) {
4
+ if (!Array.from(path).some((char) => char.charCodeAt(0) <= 32 ||
5
+ char.charCodeAt(0) >= 127 ||
6
+ char === '"' ||
7
+ char === "\\"))
8
+ return path;
9
+ return `"${Array.from(Buffer.from(path))
10
+ .map((byte) => byte === 34
11
+ ? '\\"'
12
+ : byte === 92
13
+ ? "\\\\"
14
+ : byte === 9
15
+ ? "\\t"
16
+ : byte === 10
17
+ ? "\\n"
18
+ : byte === 13
19
+ ? "\\r"
20
+ : byte < 32 || byte >= 127
21
+ ? `\\${byte.toString(8).padStart(3, "0")}`
22
+ : String.fromCharCode(byte))
23
+ .join("")}"`;
24
+ }
25
+ function boundedAppend(text, line) {
26
+ const value = text === undefined ? line : `${text}\n${line}`;
27
+ return truncate(value, UNIT_MAX_CHARS);
28
+ }
29
+ /** Paths come exclusively from NUL-delimited metadata, never patch headers. */
30
+ export function parseDiff(input) {
31
+ const fields = input.raw.split("\0");
32
+ const files = [];
33
+ for (let i = 0; i < fields.length && fields[i];) {
34
+ const header = fields[i++] ?? "";
35
+ const match = /^:[0-7]+ [0-7]+ [0-9a-f]+ [0-9a-f]+ ([A-Z][0-9]*)$/.exec(header);
36
+ if (!match || fields[i] === undefined)
37
+ throw new Error("Invalid raw diff metadata");
38
+ const oldPath = fields[i++] ?? "";
39
+ const status = match[1] ?? "";
40
+ const path = /^[RC]/.test(status) ? fields[i++] : oldPath;
41
+ if (path === undefined)
42
+ throw new Error("Missing rename destination");
43
+ files.push({
44
+ path,
45
+ oldPath,
46
+ status,
47
+ oldMode: header.split(" ")[0]?.slice(1),
48
+ newMode: header.split(" ")[1],
49
+ binary: false,
50
+ added: 0,
51
+ deleted: 0,
52
+ hunks: [],
53
+ });
54
+ }
55
+ const byPath = new Map(files.map((file) => [file.path, file]));
56
+ const stats = input.numstat.split("\0");
57
+ for (let i = 0; i < stats.length && stats[i];) {
58
+ const match = /^(\d+|-)\t(\d+|-)\t([\s\S]*)$/.exec(stats[i++] ?? "");
59
+ if (!match)
60
+ throw new Error("Invalid numstat metadata");
61
+ let path = match[3] ?? "";
62
+ if (!path) {
63
+ i++;
64
+ path = stats[i++] ?? "";
65
+ }
66
+ const file = byPath.get(path);
67
+ if (!file)
68
+ throw new Error("Numstat path absent from raw metadata");
69
+ file.binary = match[1] === "-" || match[2] === "-";
70
+ file.added = file.binary ? 0 : Number(match[1]);
71
+ file.deleted = file.binary ? 0 : Number(match[2]);
72
+ }
73
+ let fileIndex = -1;
74
+ let hunk;
75
+ let before = 0;
76
+ let after = 0;
77
+ let change;
78
+ for (const line of input.patch.split("\n")) {
79
+ if (line.startsWith("diff --git ")) {
80
+ const header = line.slice("diff --git ".length);
81
+ const verifies = (file) => header === `a/${file.oldPath} b/${file.path}` ||
82
+ header ===
83
+ `${quotePath(`a/${file.oldPath}`)} ${quotePath(`b/${file.path}`)}`;
84
+ const current = files[fileIndex];
85
+ if (!current || !verifies(current))
86
+ fileIndex++;
87
+ const next = files[fileIndex];
88
+ if (next && !verifies(next)) {
89
+ const verified = files.findIndex(verifies);
90
+ if (verified >= 0)
91
+ fileIndex = verified;
92
+ }
93
+ if (input.verifyPaths &&
94
+ (!files[fileIndex] || !verifies(files[fileIndex])))
95
+ throw new Error("Patch path absent from raw metadata");
96
+ hunk = undefined;
97
+ change = undefined;
98
+ continue;
99
+ }
100
+ const match = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/.exec(line);
101
+ if (match) {
102
+ hunk = {
103
+ beforeStart: Number(match[1]),
104
+ beforeCount: Number(match[2] ?? 1),
105
+ afterStart: Number(match[3]),
106
+ afterCount: Number(match[4] ?? 1),
107
+ changes: [],
108
+ };
109
+ files[fileIndex]?.hunks.push(hunk);
110
+ before = hunk.beforeStart;
111
+ after = hunk.afterStart;
112
+ change = undefined;
113
+ }
114
+ else if (hunk &&
115
+ (before < hunk.beforeStart + hunk.beforeCount ||
116
+ after < hunk.afterStart + hunk.afterCount) &&
117
+ (line === "" || /^[ +-]/.test(line))) {
118
+ const active = hunk;
119
+ const append = (side) => {
120
+ const text = active[side];
121
+ if ((text?.length ?? 0) +
122
+ (text === undefined ? 0 : 1) +
123
+ Math.max(0, line.length - 1) >
124
+ UNIT_MAX_CHARS)
125
+ active.truncated = true;
126
+ active[side] = boundedAppend(text, line.slice(1));
127
+ };
128
+ if (line === "" || line[0] === " ") {
129
+ append("beforeText");
130
+ append("afterText");
131
+ before++;
132
+ after++;
133
+ change = undefined;
134
+ }
135
+ else {
136
+ if (!change) {
137
+ change = {
138
+ beforeStart: before,
139
+ beforeCount: 0,
140
+ afterStart: after,
141
+ afterCount: 0,
142
+ };
143
+ hunk.changes.push(change);
144
+ }
145
+ if (line[0] === "-") {
146
+ append("beforeText");
147
+ change.beforeCount++;
148
+ before++;
149
+ }
150
+ else {
151
+ append("afterText");
152
+ change.afterCount++;
153
+ after++;
154
+ }
155
+ }
156
+ }
157
+ }
158
+ for (const path of input.untracked ?? [])
159
+ if (!byPath.has(path))
160
+ files.push({
161
+ path,
162
+ oldPath: path,
163
+ status: "?",
164
+ binary: false,
165
+ added: 0,
166
+ deleted: 0,
167
+ hunks: [],
168
+ });
169
+ return files;
170
+ }
171
+ /** Test evidence classification, deliberately independent from runner discovery. */
172
+ export function isTestFile(path) {
173
+ return (/(?:^|\/)(?:test|tests|__tests__)(?:\/|$)/.test(path) ||
174
+ /\.(?:test|spec|vitest)\.[cm]?[jt]sx?$/.test(path) ||
175
+ /\.(?:test|spec)-d\.[cm]?tsx?$/.test(path) ||
176
+ /(?:^|\/)conftest\.py$/.test(path) ||
177
+ /(?:^|\/)(?:test_[^/]*\.py|[^/]*_test\.(?:py|go))$/.test(path));
178
+ }