tewip-ai 0.69.1 → 0.69.3

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.
@@ -69,6 +69,15 @@ export function testFiles(root, limit = 4000) {
69
69
  * pull request; saying "nothing tests ROADMAP.md" is noise, and noise is how a review gets skipped.
70
70
  */
71
71
  export const isCode = (file) => /\.(?:[cm]?[jt]sx?|py|rb|go|java|kt|cs|php|rs|vue|svelte|scala|swift|sql)$/i.test(file) && !/(?:^|[\/])(?:package-lock\.json|yarn\.lock)$/i.test(file);
72
+ /**
73
+ * Code a test could be expected to cover - which a test file and a declaration file are not.
74
+ *
75
+ * A changed test file is its own test, and a review that lists `fix-data.test.ts` as covering
76
+ * `extension.test.ts` is saying nothing. A `.d.ts` has no behaviour at all: `version-compare.d.ts`
77
+ * was matched to `perf.test.ts` because both contain the words "version" and "compare", and the
78
+ * match was not wrong by the rule - the file simply should never have been asked about.
79
+ */
80
+ export const isCoverableCode = (file) => isCode(file) && !TEST_FILE.test(file) && !/\.d\.[cm]?ts$/i.test(file);
72
81
  /** The module names a file can be imported by: its path, its name, and its folder's front door. */
73
82
  export function importNames(root, changed) {
74
83
  const full = resolve(root, changed);
@@ -132,12 +141,35 @@ export function importsIt(source, names) {
132
141
  }
133
142
  const words = (text) => (text.toLowerCase().match(/[a-z][a-z0-9]{2,}/g) ?? []).filter((w) => !STOP.has(w));
134
143
  const STOP = new Set(['src', 'lib', 'app', 'test', 'tests', 'spec', 'index', 'main', 'the', 'and', 'for', 'com', 'org', 'utils', 'util', 'js', 'ts', 'py']);
144
+ /**
145
+ * A word that most test files contain is not evidence about any of them.
146
+ *
147
+ * Found by reading what `review` said about a real change to `extensions/vscode/engine-version.js`:
148
+ * **eleven** test files were listed as covering it - `a2a`, `junit`, `perf`, `schema`, `write`,
149
+ * `web-scan` - none of which has anything to do with the extension. Two shared words were required
150
+ * and two were always found: *engine*, because every test lives in `engine/test/`, and *version*,
151
+ * because fixtures everywhere say `schemaVersion: 1`. Both are structural noise. Fourteen of the
152
+ * fifteen tests in that section were guesses and eleven were the same wrong guess, in the headline
153
+ * section of the command - and a section that is mostly noise is a section a reviewer learns to skip.
154
+ *
155
+ * So a word is only evidence if it is rare across the test corpus, which is the rule `recall`
156
+ * already uses on error text. A quarter is generous: 'engine' was in 100% of them.
157
+ */
158
+ const NOISE_SHARE = 0.25;
159
+ function informative(byTest) {
160
+ const seen = new Map();
161
+ for (const bag of byTest.values())
162
+ for (const word of bag)
163
+ seen.set(word, (seen.get(word) ?? 0) + 1);
164
+ const limit = Math.max(2, Math.floor(byTest.size * NOISE_SHARE));
165
+ return (word) => (seen.get(word) ?? 0) <= limit;
166
+ }
135
167
  /**
136
168
  * The tests that reach a set of changed files. Imports first; then, for changed files nothing
137
169
  * imports, the tests whose words overlap - marked as a guess.
138
170
  */
139
171
  export function coverageFor(root, changedAll, tests = testFiles(root)) {
140
- const changed = changedAll.filter(isCode);
172
+ const changed = changedAll.filter(isCoverableCode);
141
173
  const sources = new Map();
142
174
  const read = (file) => {
143
175
  if (!sources.has(file)) {
@@ -181,18 +213,31 @@ export function coverageFor(root, changedAll, tests = testFiles(root)) {
181
213
  }
182
214
  // Only for the changed files nothing imports: the words, which is a weaker kind of knowing.
183
215
  const orphans = changed.filter((f) => !coveredByImport.has(f));
184
- for (const file of orphans) {
185
- const wanted = new Set(words(file));
186
- if (wanted.size === 0)
187
- continue;
216
+ if (orphans.length > 0) {
217
+ const bags = new Map();
188
218
  for (const test of tests) {
189
219
  const rel = relative(root, test).split(sep).join('/');
190
- if (byTest.has(rel))
191
- continue;
192
- const shared = words(`${rel} ${read(test).slice(0, 4000)}`).filter((w) => wanted.has(w));
193
- if (new Set(shared).size < 2)
220
+ bags.set(rel, new Set(words(`${rel} ${read(test).slice(0, 4000)}`)));
221
+ }
222
+ const isRare = informative(bags);
223
+ for (const file of orphans) {
224
+ // The file's own name, not its folders. A test that matches only on the directory a change
225
+ // happens to live in knows nothing about the change.
226
+ const named = new Set(words(basename(file)).filter(isRare));
227
+ if (named.size === 0)
194
228
  continue;
195
- add(test, file, 'name');
229
+ const wanted = new Set(words(file).filter(isRare));
230
+ for (const test of tests) {
231
+ const rel = relative(root, test).split(sep).join('/');
232
+ if (byTest.has(rel))
233
+ continue;
234
+ const bag = bags.get(rel) ?? new Set();
235
+ const shared = new Set([...wanted].filter((w) => bag.has(w)));
236
+ // Two rare words, and at least one of them from the changed file's own name.
237
+ if (shared.size < 2 || ![...named].some((w) => shared.has(w)))
238
+ continue;
239
+ add(test, file, 'name');
240
+ }
196
241
  }
197
242
  }
198
243
  // Direct imports first, then the package ones - and inside each group the test whose words match
@@ -204,8 +249,15 @@ export function coverageFor(root, changedAll, tests = testFiles(root)) {
204
249
  return [...byTest.values()].sort((a, b) => RANK[a.how] - RANK[b.how] || overlap(b) - overlap(a) || b.covers.length - a.covers.length);
205
250
  }
206
251
  const RANK = { imports: 0, package: 1, name: 2 };
207
- /** Changed files that no test reaches at all: the part of a change nobody will notice breaking. */
252
+ /**
253
+ * Changed files that no test reaches at all: the part of a change nobody will notice breaking.
254
+ *
255
+ * The same rule as the coverage search, and for the same reason. "`version-compare.d.ts` has no
256
+ * test that imports it. A mistake there would reach production unnoticed" is a sentence about a
257
+ * file that declares types and executes nothing - a demand the reader cannot act on, in the section
258
+ * meant to name the real risk of the change.
259
+ */
208
260
  export function uncovered(changed, coverage) {
209
261
  const reached = new Set(coverage.flatMap((c) => c.covers));
210
- return changed.filter((f) => isCode(f) && !reached.has(f) && !TEST_FILE.test(f));
262
+ return changed.filter((f) => isCoverableCode(f) && !reached.has(f));
211
263
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tewip-ai",
3
- "version": "0.69.1",
3
+ "version": "0.69.3",
4
4
  "mcpName": "io.github.hmamut39/tewip",
5
5
  "description": "Tewip finds why each failing test failed - real bug, flaky test, environment, test data or changed selector - for any stack, from the command line, CI, or your editor.",
6
6
  "keywords": [