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 +15 -9
- package/package.json +2 -2
- package/src/paths.mjs +16 -0
- package/src/report.mjs +50 -4
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 |
|
|
56
|
-
|
|
57
|
-
| puppeteer/puppeteer |
|
|
58
|
-
| webpack/webpack |
|
|
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.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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.
|
|
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 {
|
|
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) =>
|
|
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
|
|
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');
|