@aiready/doc-drift 0.1.4 → 0.1.6
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/.turbo/turbo-build.log +10 -10
- package/.turbo/turbo-test.log +6 -6
- package/README.md +1 -1
- package/dist/chunk-BBGJNBVI.mjs +189 -0
- package/dist/cli.js +44 -12
- package/dist/cli.mjs +11 -3
- package/dist/index.js +34 -10
- package/dist/index.mjs +1 -1
- package/package.json +8 -8
- package/src/__tests__/analyzer.test.ts +11 -5
- package/src/analyzer.ts +89 -28
- package/src/cli.ts +17 -7
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
|
-
> @aiready/doc-drift@0.1.
|
|
3
|
+
> @aiready/doc-drift@0.1.6 build /Users/pengcao/projects/aiready/packages/doc-drift
|
|
4
4
|
> tsup src/index.ts src/cli.ts --format cjs,esm --dts
|
|
5
5
|
|
|
6
6
|
[34mCLI[39m Building entry: src/cli.ts, src/index.ts
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
[34mCJS[39m Build start
|
|
11
11
|
[34mESM[39m Build start
|
|
12
12
|
|
|
13
|
-
[90m[[
|
|
13
|
+
[90m[[90m10:18:49 PM[90m][39m [43m[30m WARN [39m[49m [33m▲ [43;33m[[43;30mWARNING[43;33m][0m [1mThe condition "types" here will never be used as it comes after both "import" and "require"[0m [package.json]
|
|
14
14
|
|
|
15
15
|
package.json:33:6:
|
|
16
16
|
[37m 33 │ [32m"types"[37m: "./dist/index.d.ts"
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
[90m[[
|
|
34
|
+
[90m[[90m10:18:49 PM[90m][39m [43m[30m WARN [39m[49m [33m▲ [43;33m[[43;30mWARNING[43;33m][0m [1mThe condition "types" here will never be used as it comes after both "import" and "require"[0m [package.json]
|
|
35
35
|
|
|
36
36
|
package.json:33:6:
|
|
37
37
|
[37m 33 │ [32m"types"[37m: "./dist/index.d.ts"
|
|
@@ -51,15 +51,15 @@
|
|
|
51
51
|
|
|
52
52
|
|
|
53
53
|
|
|
54
|
-
[
|
|
55
|
-
[
|
|
56
|
-
[32mCJS[39m ⚡️ Build success in 269ms
|
|
54
|
+
[32mESM[39m [1mdist/cli.mjs [22m[32m1.39 KB[39m
|
|
55
|
+
[32mESM[39m [1mdist/chunk-BBGJNBVI.mjs [22m[32m5.95 KB[39m
|
|
57
56
|
[32mESM[39m [1mdist/index.mjs [22m[32m88.00 B[39m
|
|
58
|
-
[32mESM[39m
|
|
59
|
-
[
|
|
60
|
-
[
|
|
57
|
+
[32mESM[39m ⚡️ Build success in 49ms
|
|
58
|
+
[32mCJS[39m [1mdist/cli.js [22m[32m8.76 KB[39m
|
|
59
|
+
[32mCJS[39m [1mdist/index.js [22m[32m6.73 KB[39m
|
|
60
|
+
[32mCJS[39m ⚡️ Build success in 51ms
|
|
61
61
|
DTS Build start
|
|
62
|
-
DTS ⚡️ Build success in
|
|
62
|
+
DTS ⚡️ Build success in 4363ms
|
|
63
63
|
DTS dist/cli.d.ts 108.00 B
|
|
64
64
|
DTS dist/index.d.ts 950.00 B
|
|
65
65
|
DTS dist/cli.d.mts 108.00 B
|
package/.turbo/turbo-test.log
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
|
-
> @aiready/doc-drift@0.1.
|
|
3
|
+
> @aiready/doc-drift@0.1.6 test /Users/pengcao/projects/aiready/packages/doc-drift
|
|
4
4
|
> vitest run
|
|
5
5
|
|
|
6
|
+
[?25l
|
|
7
|
+
[1m[46m RUN [49m[22m [36mv4.0.18 [39m[90m/Users/pengcao/projects/aiready/packages/doc-drift[39m
|
|
6
8
|
|
|
7
|
-
[
|
|
8
|
-
|
|
9
|
-
[32m✓[39m src/__tests__/analyzer.test.ts [2m ([22m[2m1 test[22m[2m)[22m[90m 26[2mms[22m[39m
|
|
9
|
+
[32m✓[39m src/__tests__/analyzer.test.ts [2m([22m[2m1 test[22m[2m)[22m[32m 19[2mms[22m[39m
|
|
10
10
|
|
|
11
11
|
[2m Test Files [22m [1m[32m1 passed[39m[22m[90m (1)[39m
|
|
12
12
|
[2m Tests [22m [1m[32m1 passed[39m[22m[90m (1)[39m
|
|
13
|
-
[2m Start at [22m
|
|
14
|
-
[2m Duration [22m
|
|
13
|
+
[2m Start at [22m 22:19:16
|
|
14
|
+
[2m Duration [22m 1.48s[2m (transform 208ms, setup 0ms, import 1.21s, tests 19ms, environment 0ms)[22m
|
|
15
15
|
|
|
16
16
|
[?25h
|
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
## Overview
|
|
9
9
|
|
|
10
|
-
The **Documentation Drift** analyzer combines AST parsing with git log traversal to identify instances where comments are likely lagging behind actual implementation logic.
|
|
10
|
+
The **Documentation Drift** analyzer combines AST parsing with git log traversal to identify instances where comments are likely lagging behind actual implementation logic.
|
|
11
11
|
|
|
12
12
|
## 🏛️ Architecture
|
|
13
13
|
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
// src/analyzer.ts
|
|
9
|
+
import { calculateDocDrift } from "@aiready/core";
|
|
10
|
+
import { readdirSync, statSync, readFileSync } from "fs";
|
|
11
|
+
import { join, extname } from "path";
|
|
12
|
+
import { parse } from "@typescript-eslint/typescript-estree";
|
|
13
|
+
import { execSync } from "child_process";
|
|
14
|
+
var SRC_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
15
|
+
var DEFAULT_EXCLUDES = [
|
|
16
|
+
"node_modules",
|
|
17
|
+
"dist",
|
|
18
|
+
".git",
|
|
19
|
+
"coverage",
|
|
20
|
+
".turbo",
|
|
21
|
+
"build"
|
|
22
|
+
];
|
|
23
|
+
function collectFiles(dir, options, depth = 0) {
|
|
24
|
+
if (depth > 20) return [];
|
|
25
|
+
const excludes = [...DEFAULT_EXCLUDES, ...options.exclude ?? []];
|
|
26
|
+
let entries;
|
|
27
|
+
try {
|
|
28
|
+
entries = readdirSync(dir);
|
|
29
|
+
} catch {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
const files = [];
|
|
33
|
+
for (const entry of entries) {
|
|
34
|
+
if (excludes.some((ex) => entry === ex || entry.includes(ex))) continue;
|
|
35
|
+
const full = join(dir, entry);
|
|
36
|
+
let stat;
|
|
37
|
+
try {
|
|
38
|
+
stat = statSync(full);
|
|
39
|
+
} catch {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (stat.isDirectory()) {
|
|
43
|
+
files.push(...collectFiles(full, options, depth + 1));
|
|
44
|
+
} else if (stat.isFile() && SRC_EXTENSIONS.has(extname(full))) {
|
|
45
|
+
if (!options.include || options.include.some((p) => full.includes(p))) {
|
|
46
|
+
files.push(full);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return files;
|
|
51
|
+
}
|
|
52
|
+
function getLineRangeLastModified(file, startLine, endLine) {
|
|
53
|
+
try {
|
|
54
|
+
const output = execSync(
|
|
55
|
+
`git log -1 --format=%ct -L ${startLine},${endLine}:"${file}"`,
|
|
56
|
+
{
|
|
57
|
+
encoding: "utf-8",
|
|
58
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
const match = output.trim().split("\n")[0];
|
|
62
|
+
if (match && !isNaN(parseInt(match, 10))) {
|
|
63
|
+
return parseInt(match, 10);
|
|
64
|
+
}
|
|
65
|
+
} catch {
|
|
66
|
+
}
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
async function analyzeDocDrift(options) {
|
|
70
|
+
const rootDir = options.rootDir;
|
|
71
|
+
const files = collectFiles(rootDir, options);
|
|
72
|
+
const staleMonths = options.staleMonths ?? 6;
|
|
73
|
+
const staleSeconds = staleMonths * 30 * 24 * 60 * 60;
|
|
74
|
+
let uncommentedExports = 0;
|
|
75
|
+
let totalExports = 0;
|
|
76
|
+
let outdatedComments = 0;
|
|
77
|
+
let undocumentedComplexity = 0;
|
|
78
|
+
const issues = [];
|
|
79
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
80
|
+
for (const file of files) {
|
|
81
|
+
let code;
|
|
82
|
+
try {
|
|
83
|
+
code = readFileSync(file, "utf-8");
|
|
84
|
+
} catch {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
let ast;
|
|
88
|
+
try {
|
|
89
|
+
ast = parse(code, {
|
|
90
|
+
jsx: file.endsWith(".tsx") || file.endsWith(".jsx"),
|
|
91
|
+
loc: true,
|
|
92
|
+
comment: true
|
|
93
|
+
});
|
|
94
|
+
} catch {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const comments = ast.comments || [];
|
|
98
|
+
for (const node of ast.body) {
|
|
99
|
+
if (node.type === "ExportNamedDeclaration" || node.type === "ExportDefaultDeclaration") {
|
|
100
|
+
const decl = node.declaration;
|
|
101
|
+
if (!decl) continue;
|
|
102
|
+
if (decl.type === "FunctionDeclaration" || decl.type === "ClassDeclaration" || decl.type === "VariableDeclaration") {
|
|
103
|
+
totalExports++;
|
|
104
|
+
const nodeLine = node.loc.start.line;
|
|
105
|
+
const jsdocs = comments.filter(
|
|
106
|
+
(c) => c.type === "Block" && c.value.startsWith("*") && c.loc.end.line === nodeLine - 1
|
|
107
|
+
);
|
|
108
|
+
if (jsdocs.length === 0) {
|
|
109
|
+
uncommentedExports++;
|
|
110
|
+
if (decl.type === "FunctionDeclaration" && decl.body?.loc) {
|
|
111
|
+
const lines = decl.body.loc.end.line - decl.body.loc.start.line;
|
|
112
|
+
if (lines > 20) undocumentedComplexity++;
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
const jsdoc = jsdocs[0];
|
|
116
|
+
const jsdocText = jsdoc.value;
|
|
117
|
+
if (decl.type === "FunctionDeclaration") {
|
|
118
|
+
const params = decl.params.map((p) => p.name || p.left && p.left.name).filter(Boolean);
|
|
119
|
+
const paramTags = Array.from(
|
|
120
|
+
jsdocText.matchAll(/@param\s+(?:\{[^}]+\}\s+)?([a-zA-Z0-9_]+)/g)
|
|
121
|
+
).map((m) => m[1]);
|
|
122
|
+
const missingParams = params.filter(
|
|
123
|
+
(p) => !paramTags.includes(p)
|
|
124
|
+
);
|
|
125
|
+
if (missingParams.length > 0) {
|
|
126
|
+
outdatedComments++;
|
|
127
|
+
issues.push({
|
|
128
|
+
type: "doc-drift",
|
|
129
|
+
severity: "major",
|
|
130
|
+
message: `JSDoc @param mismatch: function has parameters (${missingParams.join(", ")}) not documented in JSDoc.`,
|
|
131
|
+
location: { file, line: nodeLine }
|
|
132
|
+
});
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const commentModified = getLineRangeLastModified(
|
|
137
|
+
file,
|
|
138
|
+
jsdoc.loc.start.line,
|
|
139
|
+
jsdoc.loc.end.line
|
|
140
|
+
);
|
|
141
|
+
const bodyModified = getLineRangeLastModified(
|
|
142
|
+
file,
|
|
143
|
+
decl.loc.start.line,
|
|
144
|
+
decl.loc.end.line
|
|
145
|
+
);
|
|
146
|
+
if (commentModified > 0 && bodyModified > 0) {
|
|
147
|
+
if (now - commentModified > staleSeconds && bodyModified - commentModified > staleSeconds / 2) {
|
|
148
|
+
outdatedComments++;
|
|
149
|
+
issues.push({
|
|
150
|
+
type: "doc-drift",
|
|
151
|
+
severity: "minor",
|
|
152
|
+
message: `JSDoc is significantly older than the function body implementation. Code may have drifted.`,
|
|
153
|
+
location: { file, line: jsdoc.loc.start.line }
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const riskResult = calculateDocDrift({
|
|
163
|
+
uncommentedExports,
|
|
164
|
+
totalExports,
|
|
165
|
+
outdatedComments,
|
|
166
|
+
undocumentedComplexity
|
|
167
|
+
});
|
|
168
|
+
return {
|
|
169
|
+
summary: {
|
|
170
|
+
filesAnalyzed: files.length,
|
|
171
|
+
functionsAnalyzed: totalExports,
|
|
172
|
+
score: riskResult.score,
|
|
173
|
+
rating: riskResult.rating
|
|
174
|
+
},
|
|
175
|
+
issues,
|
|
176
|
+
rawData: {
|
|
177
|
+
uncommentedExports,
|
|
178
|
+
totalExports,
|
|
179
|
+
outdatedComments,
|
|
180
|
+
undocumentedComplexity
|
|
181
|
+
},
|
|
182
|
+
recommendations: riskResult.recommendations
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export {
|
|
187
|
+
__require,
|
|
188
|
+
analyzeDocDrift
|
|
189
|
+
};
|
package/dist/cli.js
CHANGED
|
@@ -42,7 +42,14 @@ var import_path = require("path");
|
|
|
42
42
|
var import_typescript_estree = require("@typescript-eslint/typescript-estree");
|
|
43
43
|
var import_child_process = require("child_process");
|
|
44
44
|
var SRC_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
45
|
-
var DEFAULT_EXCLUDES = [
|
|
45
|
+
var DEFAULT_EXCLUDES = [
|
|
46
|
+
"node_modules",
|
|
47
|
+
"dist",
|
|
48
|
+
".git",
|
|
49
|
+
"coverage",
|
|
50
|
+
".turbo",
|
|
51
|
+
"build"
|
|
52
|
+
];
|
|
46
53
|
function collectFiles(dir, options, depth = 0) {
|
|
47
54
|
if (depth > 20) return [];
|
|
48
55
|
const excludes = [...DEFAULT_EXCLUDES, ...options.exclude ?? []];
|
|
@@ -74,10 +81,13 @@ function collectFiles(dir, options, depth = 0) {
|
|
|
74
81
|
}
|
|
75
82
|
function getLineRangeLastModified(file, startLine, endLine) {
|
|
76
83
|
try {
|
|
77
|
-
const output = (0, import_child_process.execSync)(
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
84
|
+
const output = (0, import_child_process.execSync)(
|
|
85
|
+
`git log -1 --format=%ct -L ${startLine},${endLine}:"${file}"`,
|
|
86
|
+
{
|
|
87
|
+
encoding: "utf-8",
|
|
88
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
89
|
+
}
|
|
90
|
+
);
|
|
81
91
|
const match = output.trim().split("\n")[0];
|
|
82
92
|
if (match && !isNaN(parseInt(match, 10))) {
|
|
83
93
|
return parseInt(match, 10);
|
|
@@ -122,7 +132,9 @@ async function analyzeDocDrift(options) {
|
|
|
122
132
|
if (decl.type === "FunctionDeclaration" || decl.type === "ClassDeclaration" || decl.type === "VariableDeclaration") {
|
|
123
133
|
totalExports++;
|
|
124
134
|
const nodeLine = node.loc.start.line;
|
|
125
|
-
const jsdocs = comments.filter(
|
|
135
|
+
const jsdocs = comments.filter(
|
|
136
|
+
(c) => c.type === "Block" && c.value.startsWith("*") && c.loc.end.line === nodeLine - 1
|
|
137
|
+
);
|
|
126
138
|
if (jsdocs.length === 0) {
|
|
127
139
|
uncommentedExports++;
|
|
128
140
|
if (decl.type === "FunctionDeclaration" && decl.body?.loc) {
|
|
@@ -134,8 +146,12 @@ async function analyzeDocDrift(options) {
|
|
|
134
146
|
const jsdocText = jsdoc.value;
|
|
135
147
|
if (decl.type === "FunctionDeclaration") {
|
|
136
148
|
const params = decl.params.map((p) => p.name || p.left && p.left.name).filter(Boolean);
|
|
137
|
-
const paramTags = Array.from(
|
|
138
|
-
|
|
149
|
+
const paramTags = Array.from(
|
|
150
|
+
jsdocText.matchAll(/@param\s+(?:\{[^}]+\}\s+)?([a-zA-Z0-9_]+)/g)
|
|
151
|
+
).map((m) => m[1]);
|
|
152
|
+
const missingParams = params.filter(
|
|
153
|
+
(p) => !paramTags.includes(p)
|
|
154
|
+
);
|
|
139
155
|
if (missingParams.length > 0) {
|
|
140
156
|
outdatedComments++;
|
|
141
157
|
issues.push({
|
|
@@ -147,8 +163,16 @@ async function analyzeDocDrift(options) {
|
|
|
147
163
|
continue;
|
|
148
164
|
}
|
|
149
165
|
}
|
|
150
|
-
const commentModified = getLineRangeLastModified(
|
|
151
|
-
|
|
166
|
+
const commentModified = getLineRangeLastModified(
|
|
167
|
+
file,
|
|
168
|
+
jsdoc.loc.start.line,
|
|
169
|
+
jsdoc.loc.end.line
|
|
170
|
+
);
|
|
171
|
+
const bodyModified = getLineRangeLastModified(
|
|
172
|
+
file,
|
|
173
|
+
decl.loc.start.line,
|
|
174
|
+
decl.loc.end.line
|
|
175
|
+
);
|
|
152
176
|
if (commentModified > 0 && bodyModified > 0) {
|
|
153
177
|
if (now - commentModified > staleSeconds && bodyModified - commentModified > staleSeconds / 2) {
|
|
154
178
|
outdatedComments++;
|
|
@@ -192,7 +216,13 @@ async function analyzeDocDrift(options) {
|
|
|
192
216
|
// src/cli.ts
|
|
193
217
|
var import_picocolors = __toESM(require("picocolors"));
|
|
194
218
|
function createCommand() {
|
|
195
|
-
const program = new import_commander.Command("doc-drift").description(
|
|
219
|
+
const program = new import_commander.Command("doc-drift").description(
|
|
220
|
+
"Scan for documentation drift (outdated comments, mismatched signatures)"
|
|
221
|
+
).option("--include <patterns...>", "File patterns to include").option("--exclude <patterns...>", "File patterns to exclude").option(
|
|
222
|
+
"--stale-months <number>",
|
|
223
|
+
"Months before a comment is considered potentially outdated",
|
|
224
|
+
"6"
|
|
225
|
+
).action(async (options) => {
|
|
196
226
|
console.log(import_picocolors.default.cyan("Analyzing documentation drift..."));
|
|
197
227
|
const report = await analyzeDocDrift({
|
|
198
228
|
rootDir: process.cwd(),
|
|
@@ -201,7 +231,9 @@ function createCommand() {
|
|
|
201
231
|
staleMonths: parseInt(options.staleMonths, 10)
|
|
202
232
|
});
|
|
203
233
|
console.log(import_picocolors.default.bold("Doc Drift Analysis Results:"));
|
|
204
|
-
console.log(
|
|
234
|
+
console.log(
|
|
235
|
+
`Rating: ${report.summary.rating.toUpperCase()} (Score: ${report.summary.score})`
|
|
236
|
+
);
|
|
205
237
|
if (report.issues.length > 0) {
|
|
206
238
|
console.log(import_picocolors.default.red(`
|
|
207
239
|
Found ${report.issues.length} drift issues.`));
|
package/dist/cli.mjs
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import {
|
|
2
2
|
__require,
|
|
3
3
|
analyzeDocDrift
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-BBGJNBVI.mjs";
|
|
5
5
|
|
|
6
6
|
// src/cli.ts
|
|
7
7
|
import { Command } from "commander";
|
|
8
8
|
import pc from "picocolors";
|
|
9
9
|
function createCommand() {
|
|
10
|
-
const program = new Command("doc-drift").description(
|
|
10
|
+
const program = new Command("doc-drift").description(
|
|
11
|
+
"Scan for documentation drift (outdated comments, mismatched signatures)"
|
|
12
|
+
).option("--include <patterns...>", "File patterns to include").option("--exclude <patterns...>", "File patterns to exclude").option(
|
|
13
|
+
"--stale-months <number>",
|
|
14
|
+
"Months before a comment is considered potentially outdated",
|
|
15
|
+
"6"
|
|
16
|
+
).action(async (options) => {
|
|
11
17
|
console.log(pc.cyan("Analyzing documentation drift..."));
|
|
12
18
|
const report = await analyzeDocDrift({
|
|
13
19
|
rootDir: process.cwd(),
|
|
@@ -16,7 +22,9 @@ function createCommand() {
|
|
|
16
22
|
staleMonths: parseInt(options.staleMonths, 10)
|
|
17
23
|
});
|
|
18
24
|
console.log(pc.bold("Doc Drift Analysis Results:"));
|
|
19
|
-
console.log(
|
|
25
|
+
console.log(
|
|
26
|
+
`Rating: ${report.summary.rating.toUpperCase()} (Score: ${report.summary.score})`
|
|
27
|
+
);
|
|
20
28
|
if (report.issues.length > 0) {
|
|
21
29
|
console.log(pc.red(`
|
|
22
30
|
Found ${report.issues.length} drift issues.`));
|
package/dist/index.js
CHANGED
|
@@ -31,7 +31,14 @@ var import_path = require("path");
|
|
|
31
31
|
var import_typescript_estree = require("@typescript-eslint/typescript-estree");
|
|
32
32
|
var import_child_process = require("child_process");
|
|
33
33
|
var SRC_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
34
|
-
var DEFAULT_EXCLUDES = [
|
|
34
|
+
var DEFAULT_EXCLUDES = [
|
|
35
|
+
"node_modules",
|
|
36
|
+
"dist",
|
|
37
|
+
".git",
|
|
38
|
+
"coverage",
|
|
39
|
+
".turbo",
|
|
40
|
+
"build"
|
|
41
|
+
];
|
|
35
42
|
function collectFiles(dir, options, depth = 0) {
|
|
36
43
|
if (depth > 20) return [];
|
|
37
44
|
const excludes = [...DEFAULT_EXCLUDES, ...options.exclude ?? []];
|
|
@@ -63,10 +70,13 @@ function collectFiles(dir, options, depth = 0) {
|
|
|
63
70
|
}
|
|
64
71
|
function getLineRangeLastModified(file, startLine, endLine) {
|
|
65
72
|
try {
|
|
66
|
-
const output = (0, import_child_process.execSync)(
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
73
|
+
const output = (0, import_child_process.execSync)(
|
|
74
|
+
`git log -1 --format=%ct -L ${startLine},${endLine}:"${file}"`,
|
|
75
|
+
{
|
|
76
|
+
encoding: "utf-8",
|
|
77
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
78
|
+
}
|
|
79
|
+
);
|
|
70
80
|
const match = output.trim().split("\n")[0];
|
|
71
81
|
if (match && !isNaN(parseInt(match, 10))) {
|
|
72
82
|
return parseInt(match, 10);
|
|
@@ -111,7 +121,9 @@ async function analyzeDocDrift(options) {
|
|
|
111
121
|
if (decl.type === "FunctionDeclaration" || decl.type === "ClassDeclaration" || decl.type === "VariableDeclaration") {
|
|
112
122
|
totalExports++;
|
|
113
123
|
const nodeLine = node.loc.start.line;
|
|
114
|
-
const jsdocs = comments.filter(
|
|
124
|
+
const jsdocs = comments.filter(
|
|
125
|
+
(c) => c.type === "Block" && c.value.startsWith("*") && c.loc.end.line === nodeLine - 1
|
|
126
|
+
);
|
|
115
127
|
if (jsdocs.length === 0) {
|
|
116
128
|
uncommentedExports++;
|
|
117
129
|
if (decl.type === "FunctionDeclaration" && decl.body?.loc) {
|
|
@@ -123,8 +135,12 @@ async function analyzeDocDrift(options) {
|
|
|
123
135
|
const jsdocText = jsdoc.value;
|
|
124
136
|
if (decl.type === "FunctionDeclaration") {
|
|
125
137
|
const params = decl.params.map((p) => p.name || p.left && p.left.name).filter(Boolean);
|
|
126
|
-
const paramTags = Array.from(
|
|
127
|
-
|
|
138
|
+
const paramTags = Array.from(
|
|
139
|
+
jsdocText.matchAll(/@param\s+(?:\{[^}]+\}\s+)?([a-zA-Z0-9_]+)/g)
|
|
140
|
+
).map((m) => m[1]);
|
|
141
|
+
const missingParams = params.filter(
|
|
142
|
+
(p) => !paramTags.includes(p)
|
|
143
|
+
);
|
|
128
144
|
if (missingParams.length > 0) {
|
|
129
145
|
outdatedComments++;
|
|
130
146
|
issues.push({
|
|
@@ -136,8 +152,16 @@ async function analyzeDocDrift(options) {
|
|
|
136
152
|
continue;
|
|
137
153
|
}
|
|
138
154
|
}
|
|
139
|
-
const commentModified = getLineRangeLastModified(
|
|
140
|
-
|
|
155
|
+
const commentModified = getLineRangeLastModified(
|
|
156
|
+
file,
|
|
157
|
+
jsdoc.loc.start.line,
|
|
158
|
+
jsdoc.loc.end.line
|
|
159
|
+
);
|
|
160
|
+
const bodyModified = getLineRangeLastModified(
|
|
161
|
+
file,
|
|
162
|
+
decl.loc.start.line,
|
|
163
|
+
decl.loc.end.line
|
|
164
|
+
);
|
|
141
165
|
if (commentModified > 0 && bodyModified > 0) {
|
|
142
166
|
if (now - commentModified > staleSeconds && bodyModified - commentModified > staleSeconds / 2) {
|
|
143
167
|
outdatedComments++;
|
package/dist/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiready/doc-drift",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "AI-Readiness: Documentation Drift Detection",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"@typescript-eslint/typescript-estree": "^
|
|
10
|
-
"commander": "^
|
|
11
|
-
"glob": "^
|
|
9
|
+
"@typescript-eslint/typescript-estree": "^8.0.0",
|
|
10
|
+
"commander": "^14.0.0",
|
|
11
|
+
"glob": "^13.0.0",
|
|
12
12
|
"picocolors": "^1.0.0",
|
|
13
|
-
"@aiready/core": "0.9.
|
|
13
|
+
"@aiready/core": "0.9.33"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
|
-
"@types/node": "^
|
|
17
|
-
"@typescript-eslint/types": "^
|
|
16
|
+
"@types/node": "^24.0.0",
|
|
17
|
+
"@typescript-eslint/types": "^8.0.0",
|
|
18
18
|
"tsup": "^8.0.2",
|
|
19
19
|
"typescript": "^5.4.5",
|
|
20
|
-
"vitest": "^
|
|
20
|
+
"vitest": "^4.0.0"
|
|
21
21
|
},
|
|
22
22
|
"exports": {
|
|
23
23
|
".": {
|
|
@@ -13,7 +13,9 @@ describe('Doc Drift Analyzer', () => {
|
|
|
13
13
|
|
|
14
14
|
// File with signature mismatch
|
|
15
15
|
const file1 = join(tmpDir, 'file1.ts');
|
|
16
|
-
writeFileSync(
|
|
16
|
+
writeFileSync(
|
|
17
|
+
file1,
|
|
18
|
+
`
|
|
17
19
|
/**
|
|
18
20
|
* Adds numbers.
|
|
19
21
|
* @param a First number
|
|
@@ -21,11 +23,14 @@ describe('Doc Drift Analyzer', () => {
|
|
|
21
23
|
export function add(a: number, b: number) {
|
|
22
24
|
return a + b;
|
|
23
25
|
}
|
|
24
|
-
`
|
|
26
|
+
`
|
|
27
|
+
);
|
|
25
28
|
|
|
26
29
|
// File with undocumented complexity (simulated by lines > 20)
|
|
27
30
|
const file2 = join(tmpDir, 'file2.ts');
|
|
28
|
-
writeFileSync(
|
|
31
|
+
writeFileSync(
|
|
32
|
+
file2,
|
|
33
|
+
`
|
|
29
34
|
export function complexFunction(data: any) {
|
|
30
35
|
let result = 0;
|
|
31
36
|
for (let i = 0; i < 10; i++) {
|
|
@@ -51,7 +56,8 @@ export function complexFunction(data: any) {
|
|
|
51
56
|
}
|
|
52
57
|
return result;
|
|
53
58
|
}
|
|
54
|
-
`
|
|
59
|
+
`
|
|
60
|
+
);
|
|
55
61
|
});
|
|
56
62
|
|
|
57
63
|
afterAll(() => {
|
|
@@ -72,6 +78,6 @@ export function complexFunction(data: any) {
|
|
|
72
78
|
expect(report.rawData.undocumentedComplexity).toBe(1);
|
|
73
79
|
|
|
74
80
|
expect(report.issues.length).toBeGreaterThan(0);
|
|
75
|
-
expect(report.issues.some(i => i.message.includes('b'))).toBe(true);
|
|
81
|
+
expect(report.issues.some((i) => i.message.includes('b'))).toBe(true);
|
|
76
82
|
});
|
|
77
83
|
});
|
package/src/analyzer.ts
CHANGED
|
@@ -7,24 +7,43 @@ import type { TSESTree } from '@typescript-eslint/types';
|
|
|
7
7
|
import { execSync } from 'child_process';
|
|
8
8
|
|
|
9
9
|
const SRC_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx']);
|
|
10
|
-
const DEFAULT_EXCLUDES = [
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
const DEFAULT_EXCLUDES = [
|
|
11
|
+
'node_modules',
|
|
12
|
+
'dist',
|
|
13
|
+
'.git',
|
|
14
|
+
'coverage',
|
|
15
|
+
'.turbo',
|
|
16
|
+
'build',
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
function collectFiles(
|
|
20
|
+
dir: string,
|
|
21
|
+
options: DocDriftOptions,
|
|
22
|
+
depth = 0
|
|
23
|
+
): string[] {
|
|
13
24
|
if (depth > 20) return [];
|
|
14
25
|
const excludes = [...DEFAULT_EXCLUDES, ...(options.exclude ?? [])];
|
|
15
26
|
let entries: string[];
|
|
16
|
-
try {
|
|
27
|
+
try {
|
|
28
|
+
entries = readdirSync(dir);
|
|
29
|
+
} catch {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
17
32
|
|
|
18
33
|
const files: string[] = [];
|
|
19
34
|
for (const entry of entries) {
|
|
20
|
-
if (excludes.some(ex => entry === ex || entry.includes(ex))) continue;
|
|
35
|
+
if (excludes.some((ex) => entry === ex || entry.includes(ex))) continue;
|
|
21
36
|
const full = join(dir, entry);
|
|
22
37
|
let stat;
|
|
23
|
-
try {
|
|
38
|
+
try {
|
|
39
|
+
stat = statSync(full);
|
|
40
|
+
} catch {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
24
43
|
if (stat.isDirectory()) {
|
|
25
44
|
files.push(...collectFiles(full, options, depth + 1));
|
|
26
45
|
} else if (stat.isFile() && SRC_EXTENSIONS.has(extname(full))) {
|
|
27
|
-
if (!options.include || options.include.some(p => full.includes(p))) {
|
|
46
|
+
if (!options.include || options.include.some((p) => full.includes(p))) {
|
|
28
47
|
files.push(full);
|
|
29
48
|
}
|
|
30
49
|
}
|
|
@@ -32,13 +51,20 @@ function collectFiles(dir: string, options: DocDriftOptions, depth = 0): string[
|
|
|
32
51
|
return files;
|
|
33
52
|
}
|
|
34
53
|
|
|
35
|
-
function getLineRangeLastModified(
|
|
54
|
+
function getLineRangeLastModified(
|
|
55
|
+
file: string,
|
|
56
|
+
startLine: number,
|
|
57
|
+
endLine: number
|
|
58
|
+
): number {
|
|
36
59
|
try {
|
|
37
60
|
// format %ct is committer date, UNIX timestamp
|
|
38
|
-
const output = execSync(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
61
|
+
const output = execSync(
|
|
62
|
+
`git log -1 --format=%ct -L ${startLine},${endLine}:"${file}"`,
|
|
63
|
+
{
|
|
64
|
+
encoding: 'utf-8',
|
|
65
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
66
|
+
}
|
|
67
|
+
);
|
|
42
68
|
const match = output.trim().split('\n')[0];
|
|
43
69
|
if (match && !isNaN(parseInt(match, 10))) {
|
|
44
70
|
return parseInt(match, 10);
|
|
@@ -50,7 +76,7 @@ function getLineRangeLastModified(file: string, startLine: number, endLine: numb
|
|
|
50
76
|
}
|
|
51
77
|
|
|
52
78
|
export async function analyzeDocDrift(
|
|
53
|
-
options: DocDriftOptions
|
|
79
|
+
options: DocDriftOptions
|
|
54
80
|
): Promise<DocDriftReport> {
|
|
55
81
|
const rootDir = options.rootDir;
|
|
56
82
|
const files = collectFiles(rootDir, options);
|
|
@@ -67,7 +93,11 @@ export async function analyzeDocDrift(
|
|
|
67
93
|
|
|
68
94
|
for (const file of files) {
|
|
69
95
|
let code: string;
|
|
70
|
-
try {
|
|
96
|
+
try {
|
|
97
|
+
code = readFileSync(file, 'utf-8');
|
|
98
|
+
} catch {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
71
101
|
|
|
72
102
|
let ast: TSESTree.Program;
|
|
73
103
|
try {
|
|
@@ -76,22 +106,36 @@ export async function analyzeDocDrift(
|
|
|
76
106
|
loc: true,
|
|
77
107
|
comment: true,
|
|
78
108
|
});
|
|
79
|
-
} catch {
|
|
109
|
+
} catch {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
80
112
|
|
|
81
113
|
const comments = ast.comments || [];
|
|
82
114
|
|
|
83
115
|
for (const node of ast.body) {
|
|
84
|
-
if (
|
|
116
|
+
if (
|
|
117
|
+
node.type === 'ExportNamedDeclaration' ||
|
|
118
|
+
node.type === 'ExportDefaultDeclaration'
|
|
119
|
+
) {
|
|
85
120
|
const decl = (node as any).declaration;
|
|
86
121
|
if (!decl) continue;
|
|
87
122
|
|
|
88
123
|
// Count exports
|
|
89
|
-
if (
|
|
124
|
+
if (
|
|
125
|
+
decl.type === 'FunctionDeclaration' ||
|
|
126
|
+
decl.type === 'ClassDeclaration' ||
|
|
127
|
+
decl.type === 'VariableDeclaration'
|
|
128
|
+
) {
|
|
90
129
|
totalExports++;
|
|
91
130
|
|
|
92
131
|
// Find associated JSDoc comment (immediately preceding the export)
|
|
93
132
|
const nodeLine = node.loc.start.line;
|
|
94
|
-
const jsdocs = comments.filter(
|
|
133
|
+
const jsdocs = comments.filter(
|
|
134
|
+
(c: any) =>
|
|
135
|
+
c.type === 'Block' &&
|
|
136
|
+
c.value.startsWith('*') &&
|
|
137
|
+
c.loc.end.line === nodeLine - 1
|
|
138
|
+
);
|
|
95
139
|
|
|
96
140
|
if (jsdocs.length === 0) {
|
|
97
141
|
uncommentedExports++;
|
|
@@ -107,35 +151,52 @@ export async function analyzeDocDrift(
|
|
|
107
151
|
|
|
108
152
|
// Signature mismatch detection
|
|
109
153
|
if (decl.type === 'FunctionDeclaration') {
|
|
110
|
-
const params = decl.params
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const
|
|
154
|
+
const params = decl.params
|
|
155
|
+
.map((p: any) => p.name || (p.left && p.left.name))
|
|
156
|
+
.filter(Boolean);
|
|
157
|
+
const paramTags = Array.from(
|
|
158
|
+
jsdocText.matchAll(/@param\s+(?:\{[^}]+\}\s+)?([a-zA-Z0-9_]+)/g)
|
|
159
|
+
).map((m: any) => m[1]);
|
|
160
|
+
|
|
161
|
+
const missingParams = params.filter(
|
|
162
|
+
(p: string) => !paramTags.includes(p)
|
|
163
|
+
);
|
|
114
164
|
if (missingParams.length > 0) {
|
|
115
165
|
outdatedComments++;
|
|
116
166
|
issues.push({
|
|
117
167
|
type: 'doc-drift',
|
|
118
168
|
severity: 'major',
|
|
119
169
|
message: `JSDoc @param mismatch: function has parameters (${missingParams.join(', ')}) not documented in JSDoc.`,
|
|
120
|
-
location: { file, line: nodeLine }
|
|
170
|
+
location: { file, line: nodeLine },
|
|
121
171
|
});
|
|
122
172
|
continue; // already counted as outdated
|
|
123
173
|
}
|
|
124
174
|
}
|
|
125
175
|
|
|
126
176
|
// Timestamp comparison
|
|
127
|
-
const commentModified = getLineRangeLastModified(
|
|
128
|
-
|
|
177
|
+
const commentModified = getLineRangeLastModified(
|
|
178
|
+
file,
|
|
179
|
+
jsdoc.loc.start.line,
|
|
180
|
+
jsdoc.loc.end.line
|
|
181
|
+
);
|
|
182
|
+
const bodyModified = getLineRangeLastModified(
|
|
183
|
+
file,
|
|
184
|
+
decl.loc.start.line,
|
|
185
|
+
decl.loc.end.line
|
|
186
|
+
);
|
|
129
187
|
|
|
130
188
|
if (commentModified > 0 && bodyModified > 0) {
|
|
131
189
|
// If body was modified much later than the comment, and comment is older than staleMonths
|
|
132
|
-
if (
|
|
190
|
+
if (
|
|
191
|
+
now - commentModified > staleSeconds &&
|
|
192
|
+
bodyModified - commentModified > staleSeconds / 2
|
|
193
|
+
) {
|
|
133
194
|
outdatedComments++;
|
|
134
195
|
issues.push({
|
|
135
196
|
type: 'doc-drift',
|
|
136
197
|
severity: 'minor',
|
|
137
198
|
message: `JSDoc is significantly older than the function body implementation. Code may have drifted.`,
|
|
138
|
-
location: { file, line: jsdoc.loc.start.line }
|
|
199
|
+
location: { file, line: jsdoc.loc.start.line },
|
|
139
200
|
});
|
|
140
201
|
}
|
|
141
202
|
}
|
|
@@ -149,7 +210,7 @@ export async function analyzeDocDrift(
|
|
|
149
210
|
uncommentedExports,
|
|
150
211
|
totalExports,
|
|
151
212
|
outdatedComments,
|
|
152
|
-
undocumentedComplexity
|
|
213
|
+
undocumentedComplexity,
|
|
153
214
|
});
|
|
154
215
|
|
|
155
216
|
return {
|
package/src/cli.ts
CHANGED
|
@@ -4,10 +4,16 @@ import pc from 'picocolors';
|
|
|
4
4
|
|
|
5
5
|
export function createCommand() {
|
|
6
6
|
const program = new Command('doc-drift')
|
|
7
|
-
.description(
|
|
7
|
+
.description(
|
|
8
|
+
'Scan for documentation drift (outdated comments, mismatched signatures)'
|
|
9
|
+
)
|
|
8
10
|
.option('--include <patterns...>', 'File patterns to include')
|
|
9
11
|
.option('--exclude <patterns...>', 'File patterns to exclude')
|
|
10
|
-
.option(
|
|
12
|
+
.option(
|
|
13
|
+
'--stale-months <number>',
|
|
14
|
+
'Months before a comment is considered potentially outdated',
|
|
15
|
+
'6'
|
|
16
|
+
)
|
|
11
17
|
.action(async (options) => {
|
|
12
18
|
console.log(pc.cyan('Analyzing documentation drift...'));
|
|
13
19
|
const report = await analyzeDocDrift({
|
|
@@ -18,7 +24,9 @@ export function createCommand() {
|
|
|
18
24
|
});
|
|
19
25
|
|
|
20
26
|
console.log(pc.bold('Doc Drift Analysis Results:'));
|
|
21
|
-
console.log(
|
|
27
|
+
console.log(
|
|
28
|
+
`Rating: ${report.summary.rating.toUpperCase()} (Score: ${report.summary.score})`
|
|
29
|
+
);
|
|
22
30
|
if (report.issues.length > 0) {
|
|
23
31
|
console.log(pc.red(`\nFound ${report.issues.length} drift issues.`));
|
|
24
32
|
} else {
|
|
@@ -30,8 +38,10 @@ export function createCommand() {
|
|
|
30
38
|
}
|
|
31
39
|
|
|
32
40
|
if (require.main === module) {
|
|
33
|
-
createCommand()
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
41
|
+
createCommand()
|
|
42
|
+
.parseAsync(process.argv)
|
|
43
|
+
.catch((err) => {
|
|
44
|
+
console.error(pc.red(err.message));
|
|
45
|
+
process.exit(1);
|
|
46
|
+
});
|
|
37
47
|
}
|