ancient-fences 0.2.0 → 0.3.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.
package/README.md CHANGED
@@ -52,17 +52,23 @@ that no longer exists.
52
52
 
53
53
  Two runs, full clones, August 2026:
54
54
 
55
- | Repository | Fences | Because of someone else's bug | Untouched 3+ years | Oldest |
56
- |---|---|---|---|---|
57
- | puppeteer/puppeteer | 77 | 62 | 48 | 9.1 yr |
58
- | webpack/webpack | 93 | 65 | 23 | 8.8 yr |
55
+ | Repository | In source | In tests | Oldest |
56
+ |---|---|---|---|
57
+ | puppeteer/puppeteer | 32 | 45 | 9.1 yr |
58
+ | webpack/webpack | 54 | 39 | 8.8 yr |
59
+ | eslint/eslint | 43 | 399 | 8.2 yr |
60
+ | expressjs/express | 0 | 2 | 1.9 yr |
59
61
 
60
62
  Both are well-maintained projects by good engineers. That is the point.
61
63
 
62
- Measured on a full clone, because a shallow one cannot date a line. Numbers
63
- from earlier versions were higher and worse: counting the word "until" as
64
- evidence turned two hundred ordinary comments in webpack into findings. A
65
- number you have to discount is not worth printing.
64
+ Measured on a full clone, because a shallow one cannot date a line.
65
+
66
+ The two columns are the point. A comment in a test that links to an issue is
67
+ usually the regression test for that bug: it exists because of the bug, exactly
68
+ like a workaround, but a closed issue is the reason to **keep** it. eslint has
69
+ 399 of those and 43 in its source. Mixing them into one number would have made
70
+ eslint look ten times worse than it is, and would have told an agent to delete
71
+ the tests that guard fixed bugs.
66
72
 
67
73
  ## What it finds
68
74
 
@@ -122,7 +128,7 @@ green light. A tool that reassures you without grounds is worse than no tool.
122
128
  --report[=file] write a shareable HTML report (default: ancient-fences.html)
123
129
  --tasks[=file] write the dead fences as instructions for a coding agent
124
130
  --api-base=URL alternate API (GitHub Enterprise, or a mock in tests)
125
- --json full machine-readable output
131
+ --json full machine-readable output (counts split by source and tests)
126
132
  --no-blame skip fence age (faster, tells you less)
127
133
  --include-generated scan bundles and minified builds too
128
134
  --cache=FILE where to keep issue states
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ancient-fences",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Finds the code you wrote because of someone else's bug, and checks whether that bug is still there.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -25,7 +25,7 @@
25
25
  },
26
26
  "license": "MIT",
27
27
  "scripts": {
28
- "test": "node test/detect.test.mjs && node test/tracker.test.mjs && node test/versions.test.mjs && node test/lockfile.test.mjs && node test/walk.test.mjs && node test/cli.test.mjs"
28
+ "test": "node test/detect.test.mjs && node test/tracker.test.mjs && node test/versions.test.mjs && node test/lockfile.test.mjs && node test/paths.test.mjs && node test/walk.test.mjs && node test/cli.test.mjs"
29
29
  },
30
30
  "repository": {
31
31
  "type": "git",
package/src/paths.mjs ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Is this file a test?
3
+ *
4
+ * The distinction matters more than it looks. A comment in a test that links
5
+ * to an issue is usually a regression test: it exists *because of* that bug,
6
+ * exactly like a workaround, but the right thing to do when the bug is fixed
7
+ * is the opposite. You keep the test. Telling someone to delete the test that
8
+ * guards a fixed bug is the worst advice this tool could give.
9
+ */
10
+ const TEST_DIR = /(^|\/)(tests?|__tests__|spec|specs|e2e|fixtures?|testdata|__mocks__)(\/|$)/i;
11
+ const TEST_FILE = /(^|\/)[^/]*[._-](test|spec)\.[a-z]+$/i;
12
+
13
+ export function isTestPath(file) {
14
+ const path = String(file).replace(/\\/g, '/');
15
+ return TEST_DIR.test(path) || TEST_FILE.test(path);
16
+ }
package/src/report.mjs CHANGED
@@ -1,3 +1,5 @@
1
+ import { isTestPath } from './paths.mjs';
2
+
1
3
  const YEAR = 365.25 * 24 * 3600 * 1000;
2
4
 
3
5
  export function yearsSince(date) {
@@ -14,12 +16,14 @@ export const KIND_LABEL = {
14
16
 
15
17
  export function summarize(fences, states = new Map()) {
16
18
  const byKind = { code: 0, docs: 0, deadline: 0, unmarked: 0 };
19
+ let inTests = 0;
17
20
  const trackers = new Set();
18
21
  let overdue = 0;
19
22
  let old = 0;
20
23
  let oldest = null;
21
24
  for (const f of fences) {
22
25
  byKind[f.kind]++;
26
+ if (isTestPath(f.file)) inTests++;
23
27
  if (f.premise.type === 'tracker') for (const r of f.premise.refs) trackers.add(r.id);
24
28
  if (f.premise.type === 'date' && f.premise.overdue) overdue++;
25
29
  const y = yearsSince(f.lastTouched);
@@ -32,7 +36,18 @@ export function summarize(fences, states = new Map()) {
32
36
  if (states.size === 0 && f.premise.type !== 'date') continue;
33
37
  verdicts[f.verdict.level] = (verdicts[f.verdict.level] ?? 0) + 1;
34
38
  }
35
- return { total: fences.length, byKind, trackers: trackers.size, overdue, old, oldest, verdicts, checked: states.size };
39
+ return {
40
+ total: fences.length,
41
+ inTests,
42
+ inSource: fences.length - inTests,
43
+ byKind,
44
+ trackers: trackers.size,
45
+ overdue,
46
+ old,
47
+ oldest,
48
+ verdicts,
49
+ checked: states.size,
50
+ };
36
51
  }
37
52
 
38
53
  /**
@@ -55,7 +70,13 @@ export function byFile(fences, limit = 8) {
55
70
  export function ranked(fences) {
56
71
  return fences
57
72
  .filter((f) => f.premise.type === 'tracker' || (f.premise.type === 'date' && f.premise.overdue))
58
- .sort((a, b) => (yearsSince(b.lastTouched) ?? 0) - (yearsSince(a.lastTouched) ?? 0));
73
+ .sort((a, b) => {
74
+ // Tests come last: a link to an issue in a test is usually the bug that
75
+ // test guards, and a closed issue is a reason to keep it, not to remove it.
76
+ const t = Number(isTestPath(a.file)) - Number(isTestPath(b.file));
77
+ if (t !== 0) return t;
78
+ return (yearsSince(b.lastTouched) ?? 0) - (yearsSince(a.lastTouched) ?? 0);
79
+ });
59
80
  }
60
81
 
61
82
  function premiseOf(f) {
@@ -91,6 +112,10 @@ export function renderText(fences, summary, repoName, checked = false) {
91
112
  L.push(` ${n(summary.byKind.docs)} documented limitations pointing at an issue`);
92
113
  L.push(` ${n(summary.byKind.deadline)} deadlines in comments (passed: ${summary.overdue})`);
93
114
  L.push(` ${n(summary.byKind.unmarked)} fences with no sign`);
115
+ if (summary.inTests) {
116
+ L.push(` ${n(summary.inTests)} of them in tests, where a link to an issue is usually the bug`);
117
+ L.push(' that test guards, so a closed issue means keep it, not remove it');
118
+ }
94
119
  L.push(' ' + '-'.repeat(70));
95
120
  L.push(` ${n(summary.trackers)} distinct external issues to check`);
96
121
  if (summary.history && summary.history.usable === false) {
@@ -239,6 +264,7 @@ footer p{max-width:62ch}
239
264
  <div class="stats">
240
265
  <div class="stat"><b>${summary.total}</b><span>fences standing</span></div>
241
266
  <div class="stat"><b>${summary.byKind.code}</b><span>written because of someone else's bug</span></div>
267
+ <div class="stat"><b>${summary.inTests ?? 0}</b><span>of them in tests, where a closed issue means keep</span></div>
242
268
  <div class="stat"><b>${summary.trackers}</b><span>external issues to check</span></div>
243
269
  <div class="stat"><b>${summary.history && summary.history.usable === false ? '-' : summary.old}</b><span>untouched for 3+ years</span></div>
244
270
  <div class="stat"><b>${summary.history && summary.history.usable === false ? '-' : (summary.oldest === null ? '-' : summary.oldest.toFixed(1))}</b><span>years, the oldest one</span></div>
@@ -278,7 +304,9 @@ footer p{max-width:62ch}
278
304
  * part, and every editor now ships something that can do the deleting.
279
305
  */
280
306
  export function renderTasks(fences, repoName, checked = false) {
281
- const dead = fences.filter((f) => f.verdict && (f.verdict.level === 'remove' || f.verdict.level === 'upgrade first'));
307
+ const all = fences.filter((f) => f.verdict && (f.verdict.level === 'remove' || f.verdict.level === 'upgrade first'));
308
+ const dead = all.filter((f) => !isTestPath(f.file));
309
+ const inTests = all.filter((f) => isTestPath(f.file));
282
310
  const L = [];
283
311
  L.push(`# Dead fences in ${repoName}`);
284
312
  L.push('');
@@ -292,8 +320,12 @@ export function renderTasks(fences, repoName, checked = false) {
292
320
  L.push('');
293
321
  if (dead.length === 0) {
294
322
  L.push(checked
295
- ? 'Nothing to remove: every recorded reason still holds.'
323
+ ? 'Nothing to remove in source: every recorded reason still holds.'
296
324
  : 'Nothing to remove from deadlines alone. Run again with --check to ask the trackers.');
325
+ if (inTests.length > 0) {
326
+ L.push('');
327
+ L.push(`${inTests.length} finding${inTests.length === 1 ? ' is' : 's are'} in tests, listed at the end. They are not work.`);
328
+ }
297
329
  return L.join('\n');
298
330
  }
299
331
  dead.forEach((f, i) => {
@@ -314,6 +346,20 @@ export function renderTasks(fences, repoName, checked = false) {
314
346
  }
315
347
  L.push('');
316
348
  });
349
+ if (inTests.length > 0) {
350
+ L.push('---');
351
+ L.push('');
352
+ L.push(`## Not work: ${inTests.length} of these are in tests`);
353
+ L.push('');
354
+ L.push('A test that links to an issue is usually the regression test for that');
355
+ L.push('bug. The issue being closed is why the test exists, not a reason to');
356
+ L.push('delete it. Do not touch these unless the behaviour they check is gone:');
357
+ L.push('');
358
+ for (const f of inTests.slice(0, 40)) {
359
+ L.push(`- ${f.file}:${f.line} (${premiseOf(f)})`);
360
+ }
361
+ L.push('');
362
+ }
317
363
  L.push('---');
318
364
  L.push('Generated by Ancient Fences. Verify each removal with tests before merging.');
319
365
  return L.join('\n');