ancient-fences 0.3.2 → 0.4.1
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 +115 -76
- package/bin/ancient-fences.mjs +5 -1
- package/package.json +2 -2
- package/src/name.mjs +52 -0
- package/src/report.mjs +61 -10
package/README.md
CHANGED
|
@@ -3,9 +3,17 @@
|
|
|
3
3
|
<img src="https://raw.githubusercontent.com/Marcin1000/ancient-fences/main/assets/readme-banner-light.png" alt="Ancient Fences" width="100%">
|
|
4
4
|
</picture>
|
|
5
5
|
|
|
6
|
+
<p align="center">
|
|
7
|
+
<a href="https://www.npmjs.com/package/ancient-fences"><img alt="npm" src="https://img.shields.io/npm/v/ancient-fences?color=8B5514&labelColor=1E242B&style=flat-square"></a>
|
|
8
|
+
<img alt="MIT licence" src="https://img.shields.io/badge/licence-MIT-8B5514?labelColor=1E242B&style=flat-square">
|
|
9
|
+
<img alt="Node 20 or newer" src="https://img.shields.io/badge/node-%E2%89%A520-8B5514?labelColor=1E242B&style=flat-square">
|
|
10
|
+
<img alt="Works with Claude Code, Cursor and Copilot" src="https://img.shields.io/badge/output-agent%20tasks-8B5514?labelColor=1E242B&style=flat-square">
|
|
11
|
+
</p>
|
|
12
|
+
|
|
6
13
|
# Ancient Fences
|
|
7
14
|
|
|
8
|
-
**Find
|
|
15
|
+
**Find outdated workarounds in a long-lived codebase, and turn the verified ones
|
|
16
|
+
into tasks a coding agent can act on.**
|
|
9
17
|
|
|
10
18
|
Dependabot bumps the version. Nobody removes the workaround you wrote because
|
|
11
19
|
the old version was broken.
|
|
@@ -13,14 +21,23 @@ the old version was broken.
|
|
|
13
21
|
```bash
|
|
14
22
|
npx ancient-fences . # what is standing in this codebase
|
|
15
23
|
npx ancient-fences . --check # and whether the reasons still hold
|
|
24
|
+
npx ancient-fences . --tasks # the dead ones, written as work for your agent
|
|
16
25
|
```
|
|
17
26
|
|
|
18
|
-
No installation, no configuration, no account. It reads the repository you
|
|
19
|
-
|
|
27
|
+
No installation, no configuration, no account. It reads the repository you point
|
|
28
|
+
it at and prints what it found.
|
|
29
|
+
|
|
30
|
+
<picture>
|
|
31
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Marcin1000/ancient-fences/main/assets/shot-fences.png">
|
|
32
|
+
<img src="https://raw.githubusercontent.com/Marcin1000/ancient-fences/main/assets/shot-fences-light.png" alt="Ancient Fences output for webpack: 90 fences standing, 39 of them in tests, the oldest untouched for 8.8 years" width="100%">
|
|
33
|
+
</picture>
|
|
34
|
+
|
|
35
|
+
A real run against a full clone of webpack. Nothing here is typed to look good.
|
|
20
36
|
|
|
21
37
|
## The problem
|
|
22
38
|
|
|
23
|
-
You hit a bug in a library. You write code around it and, if you are decent,
|
|
39
|
+
You hit a bug in a library. You write code around it and, if you are decent,
|
|
40
|
+
leave a note:
|
|
24
41
|
|
|
25
42
|
```js
|
|
26
43
|
// Workaround for https://github.com/some/lib/issues/2500 (remove when fixed)
|
|
@@ -28,14 +45,47 @@ You hit a bug in a library. You write code around it and, if you are decent, lea
|
|
|
28
45
|
|
|
29
46
|
Then the bug gets fixed. The issue is closed. The library is replaced. And
|
|
30
47
|
nothing happens, because there is no link between someone else's tracker and
|
|
31
|
-
your code. The workaround stays forever, and after two years nobody dares
|
|
32
|
-
|
|
48
|
+
your code. The workaround stays forever, and after two years nobody dares touch
|
|
49
|
+
it: the note says "bug", so maybe the bug is still there.
|
|
33
50
|
|
|
34
51
|
This is Chesterton's Fence at industrial scale. The code knows the fence is
|
|
35
|
-
there. Git knows how long it has stood. Nobody checks whether the reason
|
|
36
|
-
|
|
52
|
+
there. Git knows how long it has stood. Nobody checks whether the reason still
|
|
53
|
+
exists.
|
|
54
|
+
|
|
55
|
+
## From workaround to agent task
|
|
56
|
+
|
|
57
|
+
<picture>
|
|
58
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Marcin1000/ancient-fences/main/assets/diagram-fences.png">
|
|
59
|
+
<img src="https://raw.githubusercontent.com/Marcin1000/ancient-fences/main/assets/diagram-fences-light.png" alt="Three stages: detect the workaround, check the reason still holds, hand off a task an agent can act on" width="100%">
|
|
60
|
+
</picture>
|
|
61
|
+
|
|
62
|
+
Ancient Fences does not edit your code, and that is deliberate. Knowing that a
|
|
63
|
+
fence is dead is the scarce part; every editor now ships something that can do
|
|
64
|
+
the deleting. So `--tasks` writes the verified findings as work:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
npx ancient-fences . --check --tasks
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
You get a markdown file with one entry per dead fence: the file and line, the
|
|
71
|
+
reason originally recorded, what proves it no longer holds, and the instruction.
|
|
72
|
+
Each entry has this shape:
|
|
73
|
+
|
|
74
|
+
```markdown
|
|
75
|
+
## src/upload.js:212
|
|
76
|
+
|
|
77
|
+
- Recorded reason: github:some/lib#2500
|
|
78
|
+
- Evidence: the issue was closed on 2021-04-14, and the fix shipped in
|
|
79
|
+
1.9.0 (milestone). Your lockfile has some-lib 2.3.1.
|
|
80
|
+
- Untouched for 4.2 years.
|
|
37
81
|
|
|
38
|
-
|
|
82
|
+
Remove the workaround and the comment that explains it, then run the test suite.
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Point Claude Code, Cursor, Copilot or your own script at that file. The tests
|
|
86
|
+
are the safety net, which is why every instruction ends there.
|
|
87
|
+
|
|
88
|
+
## A real one, in full
|
|
39
89
|
|
|
40
90
|
In `webpack`, `test/configCases/plugins/terser-plugin/extract.js:3`:
|
|
41
91
|
|
|
@@ -44,31 +94,36 @@ In `webpack`, `test/configCases/plugins/terser-plugin/extract.js:3`:
|
|
|
44
94
|
// https://github.com/mishoo/UglifyJS2/issues/2500
|
|
45
95
|
```
|
|
46
96
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
97
|
+
The referenced issue is closed. Webpack dropped UglifyJS for terser in 2018, and
|
|
98
|
+
the string `uglify` does not appear anywhere else in the repository. The
|
|
99
|
+
directory is literally named `terser-plugin`. The line has stood untouched for
|
|
100
|
+
**8.8 years**, guarding a hole in a road that no longer exists.
|
|
101
|
+
|
|
102
|
+
That one lives in a test file, which is exactly why this tool would not ask you
|
|
103
|
+
to delete it. Read on.
|
|
52
104
|
|
|
53
|
-
|
|
105
|
+
## Tests are counted apart, and never listed as work
|
|
106
|
+
|
|
107
|
+
A comment in a test that links to an issue is usually the regression test for
|
|
108
|
+
that bug. It exists because of the bug, exactly like a workaround, but a closed
|
|
109
|
+
issue is the reason to **keep** it.
|
|
110
|
+
|
|
111
|
+
Four full clones, measured with this command:
|
|
54
112
|
|
|
55
113
|
| Repository | In source | In tests | Oldest |
|
|
56
114
|
|---|---|---|---|
|
|
57
115
|
| puppeteer/puppeteer | 32 | 45 | 9.1 yr |
|
|
58
|
-
| webpack/webpack |
|
|
116
|
+
| webpack/webpack | 51 | 39 | 8.8 yr |
|
|
59
117
|
| eslint/eslint | 43 | 399 | 8.2 yr |
|
|
60
118
|
| expressjs/express | 0 | 2 | 1.9 yr |
|
|
61
119
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
120
|
+
eslint has 399 findings in tests and 43 in its source. One combined number would
|
|
121
|
+
have made it look ten times worse than it is, and would have told an agent to
|
|
122
|
+
delete the tests that guard fixed bugs.
|
|
65
123
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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.
|
|
124
|
+
All four are maintained by good engineers. That is the point. The full reports
|
|
125
|
+
are published at
|
|
126
|
+
[ancientcode.net/reports](https://ancientcode.net/reports/).
|
|
72
127
|
|
|
73
128
|
## What it finds
|
|
74
129
|
|
|
@@ -82,11 +137,11 @@ the tests that guard fixed bugs.
|
|
|
82
137
|
A comment counts as a fence when it links to an external tracker, or says
|
|
83
138
|
something that only a fence says ("workaround", "kludge", "no longer needed",
|
|
84
139
|
"do not upgrade"). Words that merely appear in fences ("until", "polyfill",
|
|
85
|
-
"temporary", "regression") count only when the comment also names the
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
140
|
+
"temporary", "regression") count only when the comment also names the condition:
|
|
141
|
+
a deadline or a version. Phrases that read as prose elsewhere ("remove this",
|
|
142
|
+
"blocked by") count next to a `TODO`, `FIXME` or `HACK`, or next to a named
|
|
143
|
+
condition. Comment markers are read per language, so `#fff` in a stylesheet and
|
|
144
|
+
`a // b` in Python are not comments.
|
|
90
145
|
|
|
91
146
|
Trackers understood: GitHub issues and pull requests, Chromium (`crbug.com`),
|
|
92
147
|
Mozilla Bugzilla, WebKit.
|
|
@@ -105,8 +160,8 @@ VERDICT: upgrade first (fix shipped in 0.34.0, but sharp is pinned at 0.33.1)
|
|
|
105
160
|
```
|
|
106
161
|
|
|
107
162
|
Those are two different jobs. The first is code you can delete this afternoon.
|
|
108
|
-
The second is the more uncomfortable finding: you are still paying to maintain
|
|
109
|
-
|
|
163
|
+
The second is the more uncomfortable finding: you are still paying to maintain a
|
|
164
|
+
workaround for a bug that was fixed years ago, because nobody upgraded.
|
|
110
165
|
|
|
111
166
|
`package-lock.json`, `yarn.lock` and `pnpm-lock.yaml` are read when present. No
|
|
112
167
|
lockfile means the tool says less, never something false.
|
|
@@ -117,8 +172,21 @@ was fetched, so the report says age was not measured rather than printing a
|
|
|
117
172
|
confident zero.
|
|
118
173
|
|
|
119
174
|
**An unknown issue state never produces `still valid`.** Not knowing is not a
|
|
120
|
-
green light
|
|
121
|
-
|
|
175
|
+
green light, and when the tracker cannot answer the report says why. A tool that
|
|
176
|
+
reassures you without grounds is worse than no tool.
|
|
177
|
+
|
|
178
|
+
## A report you can forward
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
npx ancient-fences . --check --report=fences.html
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
<picture>
|
|
185
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Marcin1000/ancient-fences/main/assets/report-fences.png">
|
|
186
|
+
<img src="https://raw.githubusercontent.com/Marcin1000/ancient-fences/main/assets/report-fences-light.png" alt="The HTML report: counts, then the longest-untouched fences with their recorded reasons" width="100%">
|
|
187
|
+
</picture>
|
|
188
|
+
|
|
189
|
+
One HTML file, no scripts, no external requests.
|
|
122
190
|
|
|
123
191
|
## Options
|
|
124
192
|
|
|
@@ -137,45 +205,12 @@ green light. A tool that reassures you without grounds is worse than no tool.
|
|
|
137
205
|
(default 7)
|
|
138
206
|
```
|
|
139
207
|
|
|
140
|
-
An option
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
You can also point it at a single file: `npx ancient-fences src/thing.js`.
|
|
145
|
-
|
|
146
|
-
Bundles committed into a repository (`vendor.js`, a browserify or webpack
|
|
147
|
-
build, anything minified) are skipped, and the summary says how many were left
|
|
148
|
-
out. The fences inside them belong to the libraries they were built from, so
|
|
149
|
-
listing them buries the ones your team can actually act on. `dist`, `build`,
|
|
150
|
-
`node_modules`, `vendor` and `third_party` are skipped for the same reason.
|
|
151
|
-
|
|
152
|
-
Issue states are cached in your user cache directory (`$XDG_CACHE_HOME`,
|
|
153
|
-
`%LOCALAPPDATA%`, or `~/.cache`), never inside the repository being scanned.
|
|
154
|
-
Every entry records when it was read, entries older than a week are re-checked,
|
|
155
|
-
and if the tracker cannot be reached the report says which day the answer is
|
|
156
|
-
from:
|
|
157
|
-
|
|
158
|
-
```
|
|
159
|
-
VERDICT: remove (reason disappeared 2021-04-02 (state as of 2026-08-18))
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
An issue state is a snapshot, not a fact. Closed issues get reopened.
|
|
163
|
-
|
|
164
|
-
## Working with an agent
|
|
165
|
-
|
|
166
|
-
Ancient Fences does not edit your code, and that is deliberate. Knowing that a
|
|
167
|
-
fence is dead is the scarce part; every editor now ships something that can do
|
|
168
|
-
the deleting. So `--tasks` writes the verified findings as work:
|
|
169
|
-
|
|
170
|
-
```bash
|
|
171
|
-
npx ancient-fences . --check --tasks
|
|
172
|
-
```
|
|
208
|
+
An unknown option, or a number where a number cannot go, stops the run with exit
|
|
209
|
+
code 2 rather than being ignored. A mistyped flag that still prints a confident
|
|
210
|
+
report is worse than no report.
|
|
173
211
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
("remove the workaround and the comment, then run the test suite"). Point Claude
|
|
177
|
-
Code, Cursor, Copilot or your own script at it. The tests are the safety net,
|
|
178
|
-
which is why the instruction always ends there.
|
|
212
|
+
An issue state is a snapshot, not a fact. Closed issues get reopened, which is
|
|
213
|
+
why every cached state carries the date it was read.
|
|
179
214
|
|
|
180
215
|
## Tests
|
|
181
216
|
|
|
@@ -198,9 +233,13 @@ pull requests.
|
|
|
198
233
|
## Ancient Code
|
|
199
234
|
|
|
200
235
|
Ancient Fences answers one question about one repository. The same blindness
|
|
201
|
-
applies to everything else nobody re-checks in a long-running codebase:
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
236
|
+
applies to everything else nobody re-checks in a long-running codebase: which
|
|
237
|
+
parts only one person understands, whether it builds from scratch, whether the
|
|
238
|
+
documentation still describes the system. [Ancient Code](https://ancientcode.net)
|
|
239
|
+
asks all of them, ships this scanner inside it, and is free and open source too:
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
npx ancient-code .
|
|
243
|
+
```
|
|
205
244
|
|
|
206
245
|
MIT licensed.
|
package/bin/ancient-fences.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { detectFences } from '../src/detect.mjs';
|
|
|
8
8
|
import { blameAll, historyDepth } from '../src/age.mjs';
|
|
9
9
|
import { checkGithubRefs, verdict } from '../src/tracker.mjs';
|
|
10
10
|
import { renderText, renderHtml, renderTasks, summarize } from '../src/report.mjs';
|
|
11
|
+
import { projectName } from '../src/name.mjs';
|
|
11
12
|
import { readInstalled } from '../src/lockfile.mjs';
|
|
12
13
|
|
|
13
14
|
const args = process.argv.slice(2);
|
|
@@ -164,7 +165,10 @@ summary.skippedFiles = skipped;
|
|
|
164
165
|
summary.history = history;
|
|
165
166
|
summary.checkedAt = checking ? checkTimestamp(states) : null;
|
|
166
167
|
|
|
167
|
-
|
|
168
|
+
// Scanning one file, its own name is the subject. Scanning a repository, the
|
|
169
|
+
// folder is the last resort: a full clone of webpack in webpackfull/ produced
|
|
170
|
+
// a report titled "webpackfull".
|
|
171
|
+
const name = single ? basename(single) : await projectName(root);
|
|
168
172
|
|
|
169
173
|
const reportFlag = flags.find((f) => f === '--report' || f.startsWith('--report='));
|
|
170
174
|
if (reportFlag) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ancient-fences",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
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/paths.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/stylesheet.test.mjs && node test/cli.test.mjs"
|
|
29
29
|
},
|
|
30
30
|
"repository": {
|
|
31
31
|
"type": "git",
|
package/src/name.mjs
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { join, basename } from 'node:path';
|
|
5
|
+
|
|
6
|
+
const run = promisify(execFile);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* What to call the repository in a report somebody forwards.
|
|
10
|
+
*
|
|
11
|
+
* The folder name is the worst of the three answers and used to be the only
|
|
12
|
+
* one: a full clone of webpack sitting in webpackfull/ produced a report
|
|
13
|
+
* titled "webpackfull". The remote knows the real name, the manifest knows the
|
|
14
|
+
* published one, and the folder is only the last resort.
|
|
15
|
+
*/
|
|
16
|
+
export async function projectName(root) {
|
|
17
|
+
const remote = await gitRemote(root);
|
|
18
|
+
if (remote) return remote;
|
|
19
|
+
|
|
20
|
+
const pkg = await readJson(join(root, 'package.json'));
|
|
21
|
+
if (pkg?.name) return pkg.name;
|
|
22
|
+
|
|
23
|
+
return basename(root);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function readJson(path) {
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(await readFile(path, 'utf8'));
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function gitRemote(root) {
|
|
35
|
+
try {
|
|
36
|
+
const { stdout } = await run('git', ['remote', 'get-url', 'origin'], { cwd: root });
|
|
37
|
+
return ownerRepo(stdout.trim());
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** "git@github.com:webpack/webpack.git" and the https form both give webpack/webpack. */
|
|
44
|
+
export function ownerRepo(url) {
|
|
45
|
+
if (!url) return null;
|
|
46
|
+
const clean = String(url).trim().replace(/\.git$/, '').replace(/\/+$/, '');
|
|
47
|
+
const m = /[:/]([^/:]+)\/([^/]+)$/.exec(clean);
|
|
48
|
+
if (!m) return null;
|
|
49
|
+
// A local path clone has no owner worth printing, only a directory above it.
|
|
50
|
+
if (/^(\.|\/|[a-z]:\\)/i.test(clean) || clean.startsWith('file:')) return null;
|
|
51
|
+
return `${m[1]}/${m[2]}`;
|
|
52
|
+
}
|
package/src/report.mjs
CHANGED
|
@@ -149,6 +149,11 @@ export function renderText(fences, summary, repoName, checked = false) {
|
|
|
149
149
|
for (const [level, count] of Object.entries(summary.verdicts).sort((a, b) => b[1] - a[1])) {
|
|
150
150
|
L.push(` ${String(count).padStart(4)} ${level}`);
|
|
151
151
|
}
|
|
152
|
+
// "unchecked" on its own reads as a bug in this tool. The reason was
|
|
153
|
+
// recorded and then thrown away before anybody could see it.
|
|
154
|
+
for (const [why, count] of unreachable(fences)) {
|
|
155
|
+
L.push(` ${count} of them because the tracker could not answer: ${why}`);
|
|
156
|
+
}
|
|
152
157
|
L.push('');
|
|
153
158
|
}
|
|
154
159
|
|
|
@@ -192,6 +197,22 @@ export function renderText(fences, summary, repoName, checked = false) {
|
|
|
192
197
|
return L.join('\n');
|
|
193
198
|
}
|
|
194
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Why the tracker could not answer, grouped and counted. Rate limiting, no
|
|
202
|
+
* network and a repository that needs a token all land here, and each one is
|
|
203
|
+
* something the reader can act on. Silence is not.
|
|
204
|
+
*/
|
|
205
|
+
export function unreachable(fences) {
|
|
206
|
+
const reasons = new Map();
|
|
207
|
+
for (const f of fences) {
|
|
208
|
+
const why = f.verdict?.why;
|
|
209
|
+
if (f.verdict?.level !== 'unchecked' || !why) continue;
|
|
210
|
+
if (/no recorded reason/.test(why)) continue;
|
|
211
|
+
reasons.set(why, (reasons.get(why) ?? 0) + 1);
|
|
212
|
+
}
|
|
213
|
+
return [...reasons.entries()].sort((a, b) => b[1] - a[1]);
|
|
214
|
+
}
|
|
215
|
+
|
|
195
216
|
const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
|
|
196
217
|
|
|
197
218
|
/**
|
|
@@ -212,10 +233,10 @@ export function renderHtml(fences, summary, repoName, checked = false) {
|
|
|
212
233
|
const y = yearsSince(f.lastTouched);
|
|
213
234
|
const v = f.verdict;
|
|
214
235
|
return `<tr>
|
|
215
|
-
<td class="num">${y === null ? '-' : y.toFixed(1) + ' yr'}</td>
|
|
216
|
-
<td><code>${esc(f.file)}:${f.line}</code><p>${esc(f.text.slice(0, 130))}</p></td>
|
|
217
|
-
<td class="num">${esc(premiseOf(f))}</td>
|
|
218
|
-
<td>${showVerdict(f, checked) ? `<span class="v v-${esc(v.level.replace(/\s+/g, '-'))}">${esc(v.level)}</span><p>${esc(v.why)}</p>` : '<span class="v">not checked</span>'}</td>
|
|
236
|
+
<td data-label="Untouched" class="num">${y === null ? '-' : y.toFixed(1) + ' yr'}</td>
|
|
237
|
+
<td data-label="Where"><code>${esc(f.file)}:${f.line}</code><p>${esc(f.text.slice(0, 130))}</p></td>
|
|
238
|
+
<td data-label="Reason given" class="num">${esc(premiseOf(f))}</td>
|
|
239
|
+
<td data-label="${checked ? 'Verdict' : 'State'}">${showVerdict(f, checked) ? `<span class="v v-${esc(v.level.replace(/\s+/g, '-'))}">${esc(v.level)}</span><p>${esc(v.why)}</p>` : '<span class="v">not checked</span>'}</td>
|
|
219
240
|
</tr>`;
|
|
220
241
|
}).join('\n');
|
|
221
242
|
|
|
@@ -224,8 +245,8 @@ export function renderHtml(fences, summary, repoName, checked = false) {
|
|
|
224
245
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
225
246
|
<title>Ancient Fences: ${esc(repoName)}</title>
|
|
226
247
|
<style>
|
|
227
|
-
:root{--ground:#0F1216;--surface:#171C22;--surface2:#1E242B;--line:#2B333B;--ink:#E7E1D4;--dim:#A7A69C;--muted:#
|
|
228
|
-
@media (prefers-color-scheme:light){:root{--ground:#E3E2DC;--surface:#EDEBE4;--surface2:#F3F1EB;--line:#CFCCC1;--ink:#1A1E22;--dim:#4A4F53;--muted:#
|
|
248
|
+
:root{--ground:#0F1216;--surface:#171C22;--surface2:#1E242B;--line:#2B333B;--ink:#E7E1D4;--dim:#A7A69C;--muted:#878B83;--gold:#E0A45C;--inst:#8FC3D2}
|
|
249
|
+
@media (prefers-color-scheme:light){:root{--ground:#E3E2DC;--surface:#EDEBE4;--surface2:#F3F1EB;--line:#CFCCC1;--ink:#1A1E22;--dim:#4A4F53;--muted:#62665F;--gold:#8B5514;--inst:#2E6B7C}}
|
|
229
250
|
*{box-sizing:border-box}
|
|
230
251
|
body{margin:0;background:var(--ground);color:var(--ink);font:16px/1.6 ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;-webkit-font-smoothing:antialiased}
|
|
231
252
|
.wrap{max-width:68rem;margin:0 auto;padding:0 clamp(1rem,4vw,2.5rem)}
|
|
@@ -239,13 +260,20 @@ h1{font:300 clamp(2rem,5vw,3.2rem)/1.05 ui-serif,Georgia,serif;letter-spacing:-.
|
|
|
239
260
|
.stat b{font:400 2rem/1 ui-monospace,monospace;color:var(--gold);font-variant-numeric:tabular-nums}
|
|
240
261
|
.stat span{font-size:.78rem;color:var(--dim);line-height:1.35}
|
|
241
262
|
h2{font:300 1.6rem/1.1 ui-serif,Georgia,serif;margin:2.5rem 0 1rem}
|
|
263
|
+
.brand{display:flex;align-items:center;gap:.6rem;color:var(--ink);margin-bottom:1.4rem}
|
|
264
|
+
.brand span{font:300 1.2rem/1 ui-serif,Georgia,serif;transform:translateY(.09em)}
|
|
265
|
+
.brand b{color:var(--gold);font-weight:300}
|
|
242
266
|
.scroll{overflow-x:auto;border:1px solid var(--line);background:var(--surface)}
|
|
243
267
|
table{border-collapse:collapse;width:100%;min-width:40rem;font-size:.88rem;table-layout:fixed}
|
|
244
268
|
th,td{text-align:left;padding:.8rem 1rem;border-bottom:1px solid var(--line);vertical-align:top}
|
|
245
269
|
thead th{font-family:ui-monospace,monospace;font-size:.64rem;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);font-weight:400;background:var(--surface2);white-space:nowrap}
|
|
246
270
|
td p{margin:.35rem 0 0;color:var(--dim);font-size:.82rem}
|
|
247
271
|
td.num{color:var(--dim);font-variant-numeric:tabular-nums;font-size:.8rem;overflow-wrap:anywhere}
|
|
248
|
-
|
|
272
|
+
/* github:microsoft/TypeScript#41697 is 30 characters, and at 14rem it broke in
|
|
273
|
+
the middle of the word. A tracker id split across two lines is not an id
|
|
274
|
+
anybody can search for. Measured on webpack: 29 of 40 broke at 14rem, 2 at
|
|
275
|
+
19rem, and widening further changed nothing. */
|
|
276
|
+
col.age{width:6rem}col.reason{width:19rem}col.state{width:11rem}
|
|
249
277
|
code{overflow-wrap:anywhere}
|
|
250
278
|
code{font-size:.82rem;color:var(--ink)}
|
|
251
279
|
.v{font-family:ui-monospace,monospace;font-size:.7rem;letter-spacing:.06em;text-transform:uppercase;color:var(--muted)}
|
|
@@ -254,9 +282,31 @@ code{font-size:.82rem;color:var(--ink)}
|
|
|
254
282
|
footer{border-top:1px solid var(--line);margin-top:3rem;padding:2rem 0 4rem;color:var(--muted);font-size:.85rem}
|
|
255
283
|
footer strong{color:var(--ink)}
|
|
256
284
|
footer p{max-width:62ch}
|
|
285
|
+
/* Four columns of file paths never fit on a phone. Forced side by side, one
|
|
286
|
+
column collapses to nothing and its text prints a letter per line. Below this
|
|
287
|
+
width every row becomes a small block with its label in front. */
|
|
288
|
+
@media (max-width:640px){
|
|
289
|
+
.scroll{overflow-x:visible}
|
|
290
|
+
table,tbody{display:block;width:100%;min-width:0}
|
|
291
|
+
thead{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
|
|
292
|
+
tr{display:flex;flex-wrap:wrap;border-bottom:1px solid var(--line);padding:.55rem .2rem}
|
|
293
|
+
tr:last-child{border-bottom:0}
|
|
294
|
+
td{display:block;flex:1 1 100%;min-width:0;border-bottom:0;padding:.3rem 1rem;overflow-wrap:anywhere}
|
|
295
|
+
td.num{flex:0 0 auto;min-width:7rem;max-width:100%;white-space:normal}
|
|
296
|
+
td[data-label]::before{display:block;content:attr(data-label);font-family:ui-monospace,monospace;font-size:.6rem;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);margin-bottom:.15rem}
|
|
297
|
+
}
|
|
257
298
|
</style></head><body>
|
|
258
299
|
<header><div class="wrap">
|
|
259
|
-
<
|
|
300
|
+
<div class="brand">
|
|
301
|
+
<svg viewBox="0 0 24 24" width="26" height="26" aria-hidden="true">
|
|
302
|
+
<rect x="2.6" y="3.2" width="18.8" height="2.4" fill="currentColor"/>
|
|
303
|
+
<rect x="5.6" y="7.4" width="2.4" height="13.4" fill="currentColor"/>
|
|
304
|
+
<rect x="10.8" y="7.4" width="2.4" height="13.4" fill="currentColor"/>
|
|
305
|
+
<rect x="16" y="7.4" width="2.4" height="13.4" fill="currentColor"/>
|
|
306
|
+
<rect x="3.4" y="12.6" width="17.2" height="1.8" fill="var(--gold)"/>
|
|
307
|
+
</svg><span>Ancient <b>Fences</b></span>
|
|
308
|
+
</div>
|
|
309
|
+
<p class="mono">Scanned ${esc(new Date().toISOString().slice(0, 10))}</p>
|
|
260
310
|
<h1>${esc(repoName)}</h1>
|
|
261
311
|
<p class="sub">Code that exists because of an external problem, and whether that problem is still there.</p>
|
|
262
312
|
</div></header>
|
|
@@ -272,6 +322,7 @@ footer p{max-width:62ch}
|
|
|
272
322
|
${summary.history && summary.history.usable === false ? `<p class="sub">Age was not measured: ${esc(summary.history.why)}.</p>` : ''}
|
|
273
323
|
${summary.checkedAt ? `<p class="sub">Issue states read ${esc(summary.checkedAt.newest.slice(0, 10))}.</p>` : ''}
|
|
274
324
|
${!checked && summary.trackers > 0 ? `<p class="sub">The trackers were not consulted in this run, so the state column is empty. <code>--check</code> asks them whether these issues are still open.</p>` : ''}
|
|
325
|
+
${checked && unreachable(fences).length ? `<p class="sub">The tracker could not answer for ${unreachable(fences).reduce((a, [, n]) => a + n, 0)} of these: ${esc(unreachable(fences).map(([why, n]) => `${n} × ${why}`).join(', '))}. Their state is unknown, which is not the same as still valid.</p>` : ''}
|
|
275
326
|
${summary.total === 0 ? `<h2>Nothing found</h2>
|
|
276
327
|
<p class="sub">No comment in this codebase records an external reason for the code around it: no tracker link, no deadline, no note about a workaround. That is either a clean codebase or an undocumented one, and this tool cannot tell those apart.</p>` : `<h2>Check these first</h2>
|
|
277
328
|
<div class="scroll"><table>
|
|
@@ -282,13 +333,13 @@ footer p{max-width:62ch}
|
|
|
282
333
|
${fences.length ? `<h2>Where they are</h2>
|
|
283
334
|
<div class="scroll"><table>
|
|
284
335
|
<thead><tr><th>File</th><th>Fences</th><th>Kinds</th></tr></thead>
|
|
285
|
-
<tbody>${byFile(fences, 15).map((r) => `<tr><td><code>${esc(r.file)}</code></td><td class="num">${r.total}</td><td class="num">${esc(Object.entries(r.kinds).map(([k, n]) => `${n} ${k}`).join(', '))}</td></tr>`).join('\n')}</tbody>
|
|
336
|
+
<tbody>${byFile(fences, 15).map((r) => `<tr><td data-label="File"><code>${esc(r.file)}</code></td><td data-label="Fences" class="num">${r.total}</td><td data-label="Kinds" class="num">${esc(Object.entries(r.kinds).map(([k, n]) => `${n} ${k}`).join(', '))}</td></tr>`).join('\n')}</tbody>
|
|
286
337
|
</table></div>` : ''}
|
|
287
338
|
${summary.skipped ? `<h2>Left out</h2>
|
|
288
339
|
<p class="sub">${summary.skipped} file${summary.skipped === 1 ? ' was' : 's were'} skipped as a build product. The fences inside a bundle belong to the libraries it was built from, not to this team.</p>
|
|
289
340
|
<div class="scroll"><table>
|
|
290
341
|
<thead><tr><th>File</th><th>Why</th></tr></thead>
|
|
291
|
-
<tbody>${(summary.skippedFiles ?? []).map((f) => `<tr><td><code>${esc(f.path)}</code></td><td class="num">${esc(f.why)}</td></tr>`).join('\n')}</tbody>
|
|
342
|
+
<tbody>${(summary.skippedFiles ?? []).map((f) => `<tr><td data-label="File"><code>${esc(f.path)}</code></td><td data-label="Why" class="num">${esc(f.why)}</td></tr>`).join('\n')}</tbody>
|
|
292
343
|
</table></div>` : ''}
|
|
293
344
|
</main>
|
|
294
345
|
<footer><div class="wrap">
|