@aiready/doc-drift 0.1.6 → 0.1.8
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-lint.log +5 -0
- package/.turbo/turbo-test.log +5 -4
- package/dist/chunk-5EFFNN6L.mjs +145 -0
- package/dist/chunk-VLBPAYS3.mjs +209 -0
- package/dist/cli.js +14 -63
- package/dist/cli.mjs +1 -1
- package/dist/index.js +14 -63
- package/dist/index.mjs +1 -1
- package/package.json +2 -2
- package/src/analyzer.ts +24 -79
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.8 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[[90m12:55:25 AM[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[[90m12:55:25 AM[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
|
-
[
|
|
54
|
+
[32mCJS[39m [1mdist/cli.js [22m[32m7.58 KB[39m
|
|
55
|
+
[32mCJS[39m [1mdist/index.js [22m[32m5.55 KB[39m
|
|
56
|
+
[32mCJS[39m ⚡️ Build success in 511ms
|
|
57
|
+
[32mESM[39m [1mdist/chunk-5EFFNN6L.mjs [22m[32m4.85 KB[39m
|
|
56
58
|
[32mESM[39m [1mdist/index.mjs [22m[32m88.00 B[39m
|
|
57
|
-
[32mESM[39m
|
|
58
|
-
[
|
|
59
|
-
[32mCJS[39m [1mdist/index.js [22m[32m6.73 KB[39m
|
|
60
|
-
[32mCJS[39m ⚡️ Build success in 51ms
|
|
59
|
+
[32mESM[39m [1mdist/cli.mjs [22m[32m1.39 KB[39m
|
|
60
|
+
[32mESM[39m ⚡️ Build success in 495ms
|
|
61
61
|
DTS Build start
|
|
62
|
-
DTS ⚡️ Build success in
|
|
62
|
+
DTS ⚡️ Build success in 10279ms
|
|
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,17 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
|
-
> @aiready/doc-drift@0.1.
|
|
3
|
+
> @aiready/doc-drift@0.1.8 test /Users/pengcao/projects/aiready/packages/doc-drift
|
|
4
4
|
> vitest run
|
|
5
5
|
|
|
6
6
|
[?25l
|
|
7
7
|
[1m[46m RUN [49m[22m [36mv4.0.18 [39m[90m/Users/pengcao/projects/aiready/packages/doc-drift[39m
|
|
8
8
|
|
|
9
|
-
[32m✓[39m src/__tests__/analyzer.test.ts [2m([22m[2m1 test[22m[2m)[22m[
|
|
9
|
+
[32m✓[39m src/__tests__/analyzer.test.ts [2m([22m[2m1 test[22m[2m)[22m[33m 364[2mms[22m[39m
|
|
10
|
+
[33m[2m✓[22m[39m detects missing param documentation and uncommented complexity [33m 361[2mms[22m[39m
|
|
10
11
|
|
|
11
12
|
[2m Test Files [22m [1m[32m1 passed[39m[22m[90m (1)[39m
|
|
12
13
|
[2m Tests [22m [1m[32m1 passed[39m[22m[90m (1)[39m
|
|
13
|
-
[2m Start at [22m
|
|
14
|
-
[2m Duration [22m
|
|
14
|
+
[2m Start at [22m 00:56:24
|
|
15
|
+
[2m Duration [22m 5.43s[2m (transform 957ms, setup 0ms, import 3.81s, tests 364ms, environment 0ms)[22m
|
|
15
16
|
|
|
16
17
|
[?25h
|
|
@@ -0,0 +1,145 @@
|
|
|
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 {
|
|
10
|
+
scanFiles,
|
|
11
|
+
calculateDocDrift,
|
|
12
|
+
getFileCommitTimestamps,
|
|
13
|
+
getLineRangeLastModifiedCached
|
|
14
|
+
} from "@aiready/core";
|
|
15
|
+
import { readFileSync } from "fs";
|
|
16
|
+
import { parse } from "@typescript-eslint/typescript-estree";
|
|
17
|
+
async function analyzeDocDrift(options) {
|
|
18
|
+
const rootDir = options.rootDir;
|
|
19
|
+
const files = await scanFiles(options);
|
|
20
|
+
const issues = [];
|
|
21
|
+
const results = [];
|
|
22
|
+
const staleMonths = options.staleMonths ?? 6;
|
|
23
|
+
const staleSeconds = staleMonths * 30 * 24 * 60 * 60;
|
|
24
|
+
let uncommentedExports = 0;
|
|
25
|
+
let totalExports = 0;
|
|
26
|
+
let outdatedComments = 0;
|
|
27
|
+
let undocumentedComplexity = 0;
|
|
28
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
29
|
+
let processed = 0;
|
|
30
|
+
for (const file of files) {
|
|
31
|
+
processed++;
|
|
32
|
+
options.onProgress?.(processed, files.length, `doc-drift: analyzing files`);
|
|
33
|
+
let code;
|
|
34
|
+
try {
|
|
35
|
+
code = readFileSync(file, "utf-8");
|
|
36
|
+
} catch {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
let ast;
|
|
40
|
+
try {
|
|
41
|
+
ast = parse(code, {
|
|
42
|
+
jsx: file.endsWith(".tsx") || file.endsWith(".jsx"),
|
|
43
|
+
loc: true,
|
|
44
|
+
comment: true
|
|
45
|
+
});
|
|
46
|
+
} catch {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const comments = ast.comments || [];
|
|
50
|
+
let fileLineStamps;
|
|
51
|
+
for (const node of ast.body) {
|
|
52
|
+
if (node.type === "ExportNamedDeclaration" || node.type === "ExportDefaultDeclaration") {
|
|
53
|
+
const decl = node.declaration;
|
|
54
|
+
if (!decl) continue;
|
|
55
|
+
if (decl.type === "FunctionDeclaration" || decl.type === "ClassDeclaration" || decl.type === "VariableDeclaration") {
|
|
56
|
+
totalExports++;
|
|
57
|
+
const nodeLine = node.loc.start.line;
|
|
58
|
+
const jsdocs = comments.filter(
|
|
59
|
+
(c) => c.type === "Block" && c.value.startsWith("*") && c.loc.end.line === nodeLine - 1
|
|
60
|
+
);
|
|
61
|
+
if (jsdocs.length === 0) {
|
|
62
|
+
uncommentedExports++;
|
|
63
|
+
if (decl.type === "FunctionDeclaration" && decl.body?.loc) {
|
|
64
|
+
const lines = decl.body.loc.end.line - decl.body.loc.start.line;
|
|
65
|
+
if (lines > 20) undocumentedComplexity++;
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
const jsdoc = jsdocs[0];
|
|
69
|
+
const jsdocText = jsdoc.value;
|
|
70
|
+
if (decl.type === "FunctionDeclaration") {
|
|
71
|
+
const params = decl.params.map((p) => p.name || p.left && p.left.name).filter(Boolean);
|
|
72
|
+
const paramTags = Array.from(
|
|
73
|
+
jsdocText.matchAll(/@param\s+(?:\{[^}]+\}\s+)?([a-zA-Z0-9_]+)/g)
|
|
74
|
+
).map((m) => m[1]);
|
|
75
|
+
const missingParams = params.filter(
|
|
76
|
+
(p) => !paramTags.includes(p)
|
|
77
|
+
);
|
|
78
|
+
if (missingParams.length > 0) {
|
|
79
|
+
outdatedComments++;
|
|
80
|
+
issues.push({
|
|
81
|
+
type: "doc-drift",
|
|
82
|
+
severity: "major",
|
|
83
|
+
message: `JSDoc @param mismatch: function has parameters (${missingParams.join(", ")}) not documented in JSDoc.`,
|
|
84
|
+
location: { file, line: nodeLine }
|
|
85
|
+
});
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (!fileLineStamps) {
|
|
90
|
+
fileLineStamps = getFileCommitTimestamps(file);
|
|
91
|
+
}
|
|
92
|
+
const commentModified = getLineRangeLastModifiedCached(
|
|
93
|
+
fileLineStamps,
|
|
94
|
+
jsdoc.loc.start.line,
|
|
95
|
+
jsdoc.loc.end.line
|
|
96
|
+
);
|
|
97
|
+
const bodyModified = getLineRangeLastModifiedCached(
|
|
98
|
+
fileLineStamps,
|
|
99
|
+
decl.loc.start.line,
|
|
100
|
+
decl.loc.end.line
|
|
101
|
+
);
|
|
102
|
+
if (commentModified > 0 && bodyModified > 0) {
|
|
103
|
+
if (now - commentModified > staleSeconds && bodyModified - commentModified > staleSeconds / 2) {
|
|
104
|
+
outdatedComments++;
|
|
105
|
+
issues.push({
|
|
106
|
+
type: "doc-drift",
|
|
107
|
+
severity: "minor",
|
|
108
|
+
message: `JSDoc is significantly older than the function body implementation. Code may have drifted.`,
|
|
109
|
+
location: { file, line: jsdoc.loc.start.line }
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const riskResult = calculateDocDrift({
|
|
119
|
+
uncommentedExports,
|
|
120
|
+
totalExports,
|
|
121
|
+
outdatedComments,
|
|
122
|
+
undocumentedComplexity
|
|
123
|
+
});
|
|
124
|
+
return {
|
|
125
|
+
summary: {
|
|
126
|
+
filesAnalyzed: files.length,
|
|
127
|
+
functionsAnalyzed: totalExports,
|
|
128
|
+
score: riskResult.score,
|
|
129
|
+
rating: riskResult.rating
|
|
130
|
+
},
|
|
131
|
+
issues,
|
|
132
|
+
rawData: {
|
|
133
|
+
uncommentedExports,
|
|
134
|
+
totalExports,
|
|
135
|
+
outdatedComments,
|
|
136
|
+
undocumentedComplexity
|
|
137
|
+
},
|
|
138
|
+
recommendations: riskResult.recommendations
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export {
|
|
143
|
+
__require,
|
|
144
|
+
analyzeDocDrift
|
|
145
|
+
};
|
|
@@ -0,0 +1,209 @@
|
|
|
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 getFileCommitTimestamps(file) {
|
|
53
|
+
const lineStamps = {};
|
|
54
|
+
try {
|
|
55
|
+
const output = execSync(`git blame -t "${file}"`, {
|
|
56
|
+
encoding: "utf-8",
|
|
57
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
58
|
+
});
|
|
59
|
+
const lines = output.split("\n");
|
|
60
|
+
for (const line of lines) {
|
|
61
|
+
if (!line) continue;
|
|
62
|
+
const match = line.match(/^\S+\s+\(.*?(\d{10,})\s+[-+]\d+\s+(\d+)\)/);
|
|
63
|
+
if (match) {
|
|
64
|
+
const ts = parseInt(match[1], 10);
|
|
65
|
+
const ln = parseInt(match[2], 10);
|
|
66
|
+
lineStamps[ln] = ts;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
}
|
|
71
|
+
return lineStamps;
|
|
72
|
+
}
|
|
73
|
+
function getLineRangeLastModifiedCached(lineStamps, startLine, endLine) {
|
|
74
|
+
let latest = 0;
|
|
75
|
+
for (let i = startLine; i <= endLine; i++) {
|
|
76
|
+
if (lineStamps[i] && lineStamps[i] > latest) {
|
|
77
|
+
latest = lineStamps[i];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return latest;
|
|
81
|
+
}
|
|
82
|
+
async function analyzeDocDrift(options) {
|
|
83
|
+
const rootDir = options.rootDir;
|
|
84
|
+
const files = collectFiles(rootDir, options);
|
|
85
|
+
const staleMonths = options.staleMonths ?? 6;
|
|
86
|
+
const staleSeconds = staleMonths * 30 * 24 * 60 * 60;
|
|
87
|
+
let uncommentedExports = 0;
|
|
88
|
+
let totalExports = 0;
|
|
89
|
+
let outdatedComments = 0;
|
|
90
|
+
let undocumentedComplexity = 0;
|
|
91
|
+
const issues = [];
|
|
92
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
93
|
+
let processed = 0;
|
|
94
|
+
for (const file of files) {
|
|
95
|
+
processed++;
|
|
96
|
+
options.onProgress?.(processed, files.length, `doc-drift: analyzing ${file.substring(rootDir.length + 1)}`);
|
|
97
|
+
let code;
|
|
98
|
+
try {
|
|
99
|
+
code = readFileSync(file, "utf-8");
|
|
100
|
+
} catch {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
let ast;
|
|
104
|
+
try {
|
|
105
|
+
ast = parse(code, {
|
|
106
|
+
jsx: file.endsWith(".tsx") || file.endsWith(".jsx"),
|
|
107
|
+
loc: true,
|
|
108
|
+
comment: true
|
|
109
|
+
});
|
|
110
|
+
} catch {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const comments = ast.comments || [];
|
|
114
|
+
let fileLineStamps;
|
|
115
|
+
for (const node of ast.body) {
|
|
116
|
+
if (node.type === "ExportNamedDeclaration" || node.type === "ExportDefaultDeclaration") {
|
|
117
|
+
const decl = node.declaration;
|
|
118
|
+
if (!decl) continue;
|
|
119
|
+
if (decl.type === "FunctionDeclaration" || decl.type === "ClassDeclaration" || decl.type === "VariableDeclaration") {
|
|
120
|
+
totalExports++;
|
|
121
|
+
const nodeLine = node.loc.start.line;
|
|
122
|
+
const jsdocs = comments.filter(
|
|
123
|
+
(c) => c.type === "Block" && c.value.startsWith("*") && c.loc.end.line === nodeLine - 1
|
|
124
|
+
);
|
|
125
|
+
if (jsdocs.length === 0) {
|
|
126
|
+
uncommentedExports++;
|
|
127
|
+
if (decl.type === "FunctionDeclaration" && decl.body?.loc) {
|
|
128
|
+
const lines = decl.body.loc.end.line - decl.body.loc.start.line;
|
|
129
|
+
if (lines > 20) undocumentedComplexity++;
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
const jsdoc = jsdocs[0];
|
|
133
|
+
const jsdocText = jsdoc.value;
|
|
134
|
+
if (decl.type === "FunctionDeclaration") {
|
|
135
|
+
const params = decl.params.map((p) => p.name || p.left && p.left.name).filter(Boolean);
|
|
136
|
+
const paramTags = Array.from(
|
|
137
|
+
jsdocText.matchAll(/@param\s+(?:\{[^}]+\}\s+)?([a-zA-Z0-9_]+)/g)
|
|
138
|
+
).map((m) => m[1]);
|
|
139
|
+
const missingParams = params.filter(
|
|
140
|
+
(p) => !paramTags.includes(p)
|
|
141
|
+
);
|
|
142
|
+
if (missingParams.length > 0) {
|
|
143
|
+
outdatedComments++;
|
|
144
|
+
issues.push({
|
|
145
|
+
type: "doc-drift",
|
|
146
|
+
severity: "major",
|
|
147
|
+
message: `JSDoc @param mismatch: function has parameters (${missingParams.join(", ")}) not documented in JSDoc.`,
|
|
148
|
+
location: { file, line: nodeLine }
|
|
149
|
+
});
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (!fileLineStamps) {
|
|
154
|
+
fileLineStamps = getFileCommitTimestamps(file);
|
|
155
|
+
}
|
|
156
|
+
const commentModified = getLineRangeLastModifiedCached(
|
|
157
|
+
fileLineStamps,
|
|
158
|
+
jsdoc.loc.start.line,
|
|
159
|
+
jsdoc.loc.end.line
|
|
160
|
+
);
|
|
161
|
+
const bodyModified = getLineRangeLastModifiedCached(
|
|
162
|
+
fileLineStamps,
|
|
163
|
+
decl.loc.start.line,
|
|
164
|
+
decl.loc.end.line
|
|
165
|
+
);
|
|
166
|
+
if (commentModified > 0 && bodyModified > 0) {
|
|
167
|
+
if (now - commentModified > staleSeconds && bodyModified - commentModified > staleSeconds / 2) {
|
|
168
|
+
outdatedComments++;
|
|
169
|
+
issues.push({
|
|
170
|
+
type: "doc-drift",
|
|
171
|
+
severity: "minor",
|
|
172
|
+
message: `JSDoc is significantly older than the function body implementation. Code may have drifted.`,
|
|
173
|
+
location: { file, line: jsdoc.loc.start.line }
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const riskResult = calculateDocDrift({
|
|
183
|
+
uncommentedExports,
|
|
184
|
+
totalExports,
|
|
185
|
+
outdatedComments,
|
|
186
|
+
undocumentedComplexity
|
|
187
|
+
});
|
|
188
|
+
return {
|
|
189
|
+
summary: {
|
|
190
|
+
filesAnalyzed: files.length,
|
|
191
|
+
functionsAnalyzed: totalExports,
|
|
192
|
+
score: riskResult.score,
|
|
193
|
+
rating: riskResult.rating
|
|
194
|
+
},
|
|
195
|
+
issues,
|
|
196
|
+
rawData: {
|
|
197
|
+
uncommentedExports,
|
|
198
|
+
totalExports,
|
|
199
|
+
outdatedComments,
|
|
200
|
+
undocumentedComplexity
|
|
201
|
+
},
|
|
202
|
+
recommendations: riskResult.recommendations
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export {
|
|
207
|
+
__require,
|
|
208
|
+
analyzeDocDrift
|
|
209
|
+
};
|
package/dist/cli.js
CHANGED
|
@@ -38,76 +38,23 @@ var import_commander = require("commander");
|
|
|
38
38
|
// src/analyzer.ts
|
|
39
39
|
var import_core = require("@aiready/core");
|
|
40
40
|
var import_fs = require("fs");
|
|
41
|
-
var import_path = require("path");
|
|
42
41
|
var import_typescript_estree = require("@typescript-eslint/typescript-estree");
|
|
43
|
-
var import_child_process = require("child_process");
|
|
44
|
-
var SRC_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
45
|
-
var DEFAULT_EXCLUDES = [
|
|
46
|
-
"node_modules",
|
|
47
|
-
"dist",
|
|
48
|
-
".git",
|
|
49
|
-
"coverage",
|
|
50
|
-
".turbo",
|
|
51
|
-
"build"
|
|
52
|
-
];
|
|
53
|
-
function collectFiles(dir, options, depth = 0) {
|
|
54
|
-
if (depth > 20) return [];
|
|
55
|
-
const excludes = [...DEFAULT_EXCLUDES, ...options.exclude ?? []];
|
|
56
|
-
let entries;
|
|
57
|
-
try {
|
|
58
|
-
entries = (0, import_fs.readdirSync)(dir);
|
|
59
|
-
} catch {
|
|
60
|
-
return [];
|
|
61
|
-
}
|
|
62
|
-
const files = [];
|
|
63
|
-
for (const entry of entries) {
|
|
64
|
-
if (excludes.some((ex) => entry === ex || entry.includes(ex))) continue;
|
|
65
|
-
const full = (0, import_path.join)(dir, entry);
|
|
66
|
-
let stat;
|
|
67
|
-
try {
|
|
68
|
-
stat = (0, import_fs.statSync)(full);
|
|
69
|
-
} catch {
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
if (stat.isDirectory()) {
|
|
73
|
-
files.push(...collectFiles(full, options, depth + 1));
|
|
74
|
-
} else if (stat.isFile() && SRC_EXTENSIONS.has((0, import_path.extname)(full))) {
|
|
75
|
-
if (!options.include || options.include.some((p) => full.includes(p))) {
|
|
76
|
-
files.push(full);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
return files;
|
|
81
|
-
}
|
|
82
|
-
function getLineRangeLastModified(file, startLine, endLine) {
|
|
83
|
-
try {
|
|
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
|
-
);
|
|
91
|
-
const match = output.trim().split("\n")[0];
|
|
92
|
-
if (match && !isNaN(parseInt(match, 10))) {
|
|
93
|
-
return parseInt(match, 10);
|
|
94
|
-
}
|
|
95
|
-
} catch {
|
|
96
|
-
}
|
|
97
|
-
return 0;
|
|
98
|
-
}
|
|
99
42
|
async function analyzeDocDrift(options) {
|
|
100
43
|
const rootDir = options.rootDir;
|
|
101
|
-
const files =
|
|
44
|
+
const files = await (0, import_core.scanFiles)(options);
|
|
45
|
+
const issues = [];
|
|
46
|
+
const results = [];
|
|
102
47
|
const staleMonths = options.staleMonths ?? 6;
|
|
103
48
|
const staleSeconds = staleMonths * 30 * 24 * 60 * 60;
|
|
104
49
|
let uncommentedExports = 0;
|
|
105
50
|
let totalExports = 0;
|
|
106
51
|
let outdatedComments = 0;
|
|
107
52
|
let undocumentedComplexity = 0;
|
|
108
|
-
const issues = [];
|
|
109
53
|
const now = Math.floor(Date.now() / 1e3);
|
|
54
|
+
let processed = 0;
|
|
110
55
|
for (const file of files) {
|
|
56
|
+
processed++;
|
|
57
|
+
options.onProgress?.(processed, files.length, `doc-drift: analyzing files`);
|
|
111
58
|
let code;
|
|
112
59
|
try {
|
|
113
60
|
code = (0, import_fs.readFileSync)(file, "utf-8");
|
|
@@ -125,6 +72,7 @@ async function analyzeDocDrift(options) {
|
|
|
125
72
|
continue;
|
|
126
73
|
}
|
|
127
74
|
const comments = ast.comments || [];
|
|
75
|
+
let fileLineStamps;
|
|
128
76
|
for (const node of ast.body) {
|
|
129
77
|
if (node.type === "ExportNamedDeclaration" || node.type === "ExportDefaultDeclaration") {
|
|
130
78
|
const decl = node.declaration;
|
|
@@ -163,13 +111,16 @@ async function analyzeDocDrift(options) {
|
|
|
163
111
|
continue;
|
|
164
112
|
}
|
|
165
113
|
}
|
|
166
|
-
|
|
167
|
-
file
|
|
114
|
+
if (!fileLineStamps) {
|
|
115
|
+
fileLineStamps = (0, import_core.getFileCommitTimestamps)(file);
|
|
116
|
+
}
|
|
117
|
+
const commentModified = (0, import_core.getLineRangeLastModifiedCached)(
|
|
118
|
+
fileLineStamps,
|
|
168
119
|
jsdoc.loc.start.line,
|
|
169
120
|
jsdoc.loc.end.line
|
|
170
121
|
);
|
|
171
|
-
const bodyModified =
|
|
172
|
-
|
|
122
|
+
const bodyModified = (0, import_core.getLineRangeLastModifiedCached)(
|
|
123
|
+
fileLineStamps,
|
|
173
124
|
decl.loc.start.line,
|
|
174
125
|
decl.loc.end.line
|
|
175
126
|
);
|
package/dist/cli.mjs
CHANGED
package/dist/index.js
CHANGED
|
@@ -27,76 +27,23 @@ module.exports = __toCommonJS(index_exports);
|
|
|
27
27
|
// src/analyzer.ts
|
|
28
28
|
var import_core = require("@aiready/core");
|
|
29
29
|
var import_fs = require("fs");
|
|
30
|
-
var import_path = require("path");
|
|
31
30
|
var import_typescript_estree = require("@typescript-eslint/typescript-estree");
|
|
32
|
-
var import_child_process = require("child_process");
|
|
33
|
-
var SRC_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
34
|
-
var DEFAULT_EXCLUDES = [
|
|
35
|
-
"node_modules",
|
|
36
|
-
"dist",
|
|
37
|
-
".git",
|
|
38
|
-
"coverage",
|
|
39
|
-
".turbo",
|
|
40
|
-
"build"
|
|
41
|
-
];
|
|
42
|
-
function collectFiles(dir, options, depth = 0) {
|
|
43
|
-
if (depth > 20) return [];
|
|
44
|
-
const excludes = [...DEFAULT_EXCLUDES, ...options.exclude ?? []];
|
|
45
|
-
let entries;
|
|
46
|
-
try {
|
|
47
|
-
entries = (0, import_fs.readdirSync)(dir);
|
|
48
|
-
} catch {
|
|
49
|
-
return [];
|
|
50
|
-
}
|
|
51
|
-
const files = [];
|
|
52
|
-
for (const entry of entries) {
|
|
53
|
-
if (excludes.some((ex) => entry === ex || entry.includes(ex))) continue;
|
|
54
|
-
const full = (0, import_path.join)(dir, entry);
|
|
55
|
-
let stat;
|
|
56
|
-
try {
|
|
57
|
-
stat = (0, import_fs.statSync)(full);
|
|
58
|
-
} catch {
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
if (stat.isDirectory()) {
|
|
62
|
-
files.push(...collectFiles(full, options, depth + 1));
|
|
63
|
-
} else if (stat.isFile() && SRC_EXTENSIONS.has((0, import_path.extname)(full))) {
|
|
64
|
-
if (!options.include || options.include.some((p) => full.includes(p))) {
|
|
65
|
-
files.push(full);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
return files;
|
|
70
|
-
}
|
|
71
|
-
function getLineRangeLastModified(file, startLine, endLine) {
|
|
72
|
-
try {
|
|
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
|
-
);
|
|
80
|
-
const match = output.trim().split("\n")[0];
|
|
81
|
-
if (match && !isNaN(parseInt(match, 10))) {
|
|
82
|
-
return parseInt(match, 10);
|
|
83
|
-
}
|
|
84
|
-
} catch {
|
|
85
|
-
}
|
|
86
|
-
return 0;
|
|
87
|
-
}
|
|
88
31
|
async function analyzeDocDrift(options) {
|
|
89
32
|
const rootDir = options.rootDir;
|
|
90
|
-
const files =
|
|
33
|
+
const files = await (0, import_core.scanFiles)(options);
|
|
34
|
+
const issues = [];
|
|
35
|
+
const results = [];
|
|
91
36
|
const staleMonths = options.staleMonths ?? 6;
|
|
92
37
|
const staleSeconds = staleMonths * 30 * 24 * 60 * 60;
|
|
93
38
|
let uncommentedExports = 0;
|
|
94
39
|
let totalExports = 0;
|
|
95
40
|
let outdatedComments = 0;
|
|
96
41
|
let undocumentedComplexity = 0;
|
|
97
|
-
const issues = [];
|
|
98
42
|
const now = Math.floor(Date.now() / 1e3);
|
|
43
|
+
let processed = 0;
|
|
99
44
|
for (const file of files) {
|
|
45
|
+
processed++;
|
|
46
|
+
options.onProgress?.(processed, files.length, `doc-drift: analyzing files`);
|
|
100
47
|
let code;
|
|
101
48
|
try {
|
|
102
49
|
code = (0, import_fs.readFileSync)(file, "utf-8");
|
|
@@ -114,6 +61,7 @@ async function analyzeDocDrift(options) {
|
|
|
114
61
|
continue;
|
|
115
62
|
}
|
|
116
63
|
const comments = ast.comments || [];
|
|
64
|
+
let fileLineStamps;
|
|
117
65
|
for (const node of ast.body) {
|
|
118
66
|
if (node.type === "ExportNamedDeclaration" || node.type === "ExportDefaultDeclaration") {
|
|
119
67
|
const decl = node.declaration;
|
|
@@ -152,13 +100,16 @@ async function analyzeDocDrift(options) {
|
|
|
152
100
|
continue;
|
|
153
101
|
}
|
|
154
102
|
}
|
|
155
|
-
|
|
156
|
-
file
|
|
103
|
+
if (!fileLineStamps) {
|
|
104
|
+
fileLineStamps = (0, import_core.getFileCommitTimestamps)(file);
|
|
105
|
+
}
|
|
106
|
+
const commentModified = (0, import_core.getLineRangeLastModifiedCached)(
|
|
107
|
+
fileLineStamps,
|
|
157
108
|
jsdoc.loc.start.line,
|
|
158
109
|
jsdoc.loc.end.line
|
|
159
110
|
);
|
|
160
|
-
const bodyModified =
|
|
161
|
-
|
|
111
|
+
const bodyModified = (0, import_core.getLineRangeLastModifiedCached)(
|
|
112
|
+
fileLineStamps,
|
|
162
113
|
decl.loc.start.line,
|
|
163
114
|
decl.loc.end.line
|
|
164
115
|
);
|
package/dist/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiready/doc-drift",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "AI-Readiness: Documentation Drift Detection",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"commander": "^14.0.0",
|
|
11
11
|
"glob": "^13.0.0",
|
|
12
12
|
"picocolors": "^1.0.0",
|
|
13
|
-
"@aiready/core": "0.9.
|
|
13
|
+
"@aiready/core": "0.9.35"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
16
|
"@types/node": "^24.0.0",
|
package/src/analyzer.ts
CHANGED
|
@@ -1,85 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
scanFiles,
|
|
3
|
+
readFileContent,
|
|
4
|
+
calculateDocDrift,
|
|
5
|
+
getFileCommitTimestamps,
|
|
6
|
+
getLineRangeLastModifiedCached,
|
|
7
|
+
} from '@aiready/core';
|
|
2
8
|
import type { DocDriftOptions, DocDriftReport, DocDriftIssue } from './types';
|
|
3
|
-
import {
|
|
4
|
-
import { join, extname } from 'path';
|
|
9
|
+
import { readFileSync } from 'fs';
|
|
5
10
|
import { parse } from '@typescript-eslint/typescript-estree';
|
|
6
11
|
import type { TSESTree } from '@typescript-eslint/types';
|
|
7
|
-
import { execSync } from 'child_process';
|
|
8
|
-
|
|
9
|
-
const SRC_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx']);
|
|
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[] {
|
|
24
|
-
if (depth > 20) return [];
|
|
25
|
-
const excludes = [...DEFAULT_EXCLUDES, ...(options.exclude ?? [])];
|
|
26
|
-
let entries: string[];
|
|
27
|
-
try {
|
|
28
|
-
entries = readdirSync(dir);
|
|
29
|
-
} catch {
|
|
30
|
-
return [];
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const files: string[] = [];
|
|
34
|
-
for (const entry of entries) {
|
|
35
|
-
if (excludes.some((ex) => entry === ex || entry.includes(ex))) continue;
|
|
36
|
-
const full = join(dir, entry);
|
|
37
|
-
let stat;
|
|
38
|
-
try {
|
|
39
|
-
stat = statSync(full);
|
|
40
|
-
} catch {
|
|
41
|
-
continue;
|
|
42
|
-
}
|
|
43
|
-
if (stat.isDirectory()) {
|
|
44
|
-
files.push(...collectFiles(full, options, depth + 1));
|
|
45
|
-
} else if (stat.isFile() && SRC_EXTENSIONS.has(extname(full))) {
|
|
46
|
-
if (!options.include || options.include.some((p) => full.includes(p))) {
|
|
47
|
-
files.push(full);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
return files;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function getLineRangeLastModified(
|
|
55
|
-
file: string,
|
|
56
|
-
startLine: number,
|
|
57
|
-
endLine: number
|
|
58
|
-
): number {
|
|
59
|
-
try {
|
|
60
|
-
// format %ct is committer date, UNIX timestamp
|
|
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
|
-
);
|
|
68
|
-
const match = output.trim().split('\n')[0];
|
|
69
|
-
if (match && !isNaN(parseInt(match, 10))) {
|
|
70
|
-
return parseInt(match, 10);
|
|
71
|
-
}
|
|
72
|
-
} catch {
|
|
73
|
-
// Ignore errors (file untracked, new file, etc)
|
|
74
|
-
}
|
|
75
|
-
return 0; // Unknown or not committed
|
|
76
|
-
}
|
|
77
12
|
|
|
78
13
|
export async function analyzeDocDrift(
|
|
79
14
|
options: DocDriftOptions
|
|
80
15
|
): Promise<DocDriftReport> {
|
|
81
16
|
const rootDir = options.rootDir;
|
|
82
|
-
|
|
17
|
+
// Use core scanFiles which respects .gitignore recursively
|
|
18
|
+
const files = await scanFiles(options);
|
|
19
|
+
const issues: DocDriftIssue[] = [];
|
|
20
|
+
const results: DocDriftIssue[] = [];
|
|
83
21
|
const staleMonths = options.staleMonths ?? 6;
|
|
84
22
|
const staleSeconds = staleMonths * 30 * 24 * 60 * 60;
|
|
85
23
|
|
|
@@ -88,10 +26,13 @@ export async function analyzeDocDrift(
|
|
|
88
26
|
let outdatedComments = 0;
|
|
89
27
|
let undocumentedComplexity = 0;
|
|
90
28
|
|
|
91
|
-
const issues: DocDriftIssue[] = [];
|
|
92
29
|
const now = Math.floor(Date.now() / 1000);
|
|
93
30
|
|
|
31
|
+
let processed = 0;
|
|
94
32
|
for (const file of files) {
|
|
33
|
+
processed++;
|
|
34
|
+
options.onProgress?.(processed, files.length, `doc-drift: analyzing files`);
|
|
35
|
+
|
|
95
36
|
let code: string;
|
|
96
37
|
try {
|
|
97
38
|
code = readFileSync(file, 'utf-8');
|
|
@@ -111,6 +52,7 @@ export async function analyzeDocDrift(
|
|
|
111
52
|
}
|
|
112
53
|
|
|
113
54
|
const comments = ast.comments || [];
|
|
55
|
+
let fileLineStamps: Record<number, number> | undefined;
|
|
114
56
|
|
|
115
57
|
for (const node of ast.body) {
|
|
116
58
|
if (
|
|
@@ -174,13 +116,16 @@ export async function analyzeDocDrift(
|
|
|
174
116
|
}
|
|
175
117
|
|
|
176
118
|
// Timestamp comparison
|
|
177
|
-
|
|
178
|
-
file
|
|
119
|
+
if (!fileLineStamps) {
|
|
120
|
+
fileLineStamps = getFileCommitTimestamps(file);
|
|
121
|
+
}
|
|
122
|
+
const commentModified = getLineRangeLastModifiedCached(
|
|
123
|
+
fileLineStamps,
|
|
179
124
|
jsdoc.loc.start.line,
|
|
180
125
|
jsdoc.loc.end.line
|
|
181
126
|
);
|
|
182
|
-
const bodyModified =
|
|
183
|
-
|
|
127
|
+
const bodyModified = getLineRangeLastModifiedCached(
|
|
128
|
+
fileLineStamps,
|
|
184
129
|
decl.loc.start.line,
|
|
185
130
|
decl.loc.end.line
|
|
186
131
|
);
|