@aiready/doc-drift 0.11.16 → 0.11.18
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 +8 -8
- package/.turbo/turbo-test.log +4 -4
- package/README.md +6 -0
- package/dist/chunk-5BGWZWHD.mjs +130 -0
- package/dist/chunk-NWJ6PSNT.mjs +129 -0
- package/dist/cli.js +35 -58
- package/dist/cli.mjs +1 -1
- package/dist/index.js +35 -58
- package/dist/index.mjs +1 -1
- package/package.json +2 -2
- package/src/analyzer.ts +61 -86
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
|
-
> @aiready/doc-drift@0.11.
|
|
3
|
+
> @aiready/doc-drift@0.11.17 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
|
|
@@ -9,15 +9,15 @@
|
|
|
9
9
|
[34mCLI[39m Target: es2020
|
|
10
10
|
[34mCJS[39m Build start
|
|
11
11
|
[34mESM[39m Build start
|
|
12
|
-
[32mCJS[39m [1mdist/
|
|
13
|
-
[32mCJS[39m [1mdist/
|
|
14
|
-
[32mCJS[39m ⚡️ Build success in
|
|
15
|
-
[32mESM[39m [1mdist/
|
|
12
|
+
[32mCJS[39m [1mdist/cli.js [22m[32m6.48 KB[39m
|
|
13
|
+
[32mCJS[39m [1mdist/index.js [22m[32m6.09 KB[39m
|
|
14
|
+
[32mCJS[39m ⚡️ Build success in 190ms
|
|
15
|
+
[32mESM[39m [1mdist/chunk-5BGWZWHD.mjs [22m[32m3.81 KB[39m
|
|
16
16
|
[32mESM[39m [1mdist/cli.mjs [22m[32m1.39 KB[39m
|
|
17
|
-
[32mESM[39m [1mdist/
|
|
18
|
-
[32mESM[39m ⚡️ Build success in
|
|
17
|
+
[32mESM[39m [1mdist/index.mjs [22m[32m1.63 KB[39m
|
|
18
|
+
[32mESM[39m ⚡️ Build success in 200ms
|
|
19
19
|
DTS Build start
|
|
20
|
-
DTS ⚡️ Build success in
|
|
20
|
+
DTS ⚡️ Build success in 4432ms
|
|
21
21
|
DTS dist/cli.d.ts 108.00 B
|
|
22
22
|
DTS dist/index.d.ts 1.07 KB
|
|
23
23
|
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.11.
|
|
3
|
+
> @aiready/doc-drift@0.11.17 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[32m
|
|
9
|
+
[32m✓[39m src/__tests__/analyzer.test.ts [2m([22m[2m1 test[22m[2m)[22m[32m 128[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 17:27:52
|
|
14
|
+
[2m Duration [22m 2.40s[2m (transform 470ms, setup 0ms, import 1.49s, tests 128ms, environment 1ms)[22m
|
|
15
15
|
|
|
16
16
|
[?25h
|
package/README.md
CHANGED
|
@@ -9,6 +9,12 @@
|
|
|
9
9
|
|
|
10
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
|
+
### Language Support
|
|
13
|
+
|
|
14
|
+
- **Full Support:** TypeScript, JavaScript, Python, Java, Go, C#
|
|
15
|
+
- **Capabilities:** JSDoc/Docstring/XML-Doc drift detection, signature mismatch.
|
|
16
|
+
toxicology
|
|
17
|
+
|
|
12
18
|
## 🏛️ Architecture
|
|
13
19
|
|
|
14
20
|
```
|
|
@@ -0,0 +1,130 @@
|
|
|
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
|
+
Severity,
|
|
15
|
+
IssueType,
|
|
16
|
+
emitProgress,
|
|
17
|
+
getParser
|
|
18
|
+
} from "@aiready/core";
|
|
19
|
+
import { readFileSync } from "fs";
|
|
20
|
+
async function analyzeDocDrift(options) {
|
|
21
|
+
const files = await scanFiles(options);
|
|
22
|
+
const issues = [];
|
|
23
|
+
const staleMonths = options.staleMonths ?? 6;
|
|
24
|
+
const staleSeconds = staleMonths * 30 * 24 * 60 * 60;
|
|
25
|
+
let uncommentedExports = 0;
|
|
26
|
+
let totalExports = 0;
|
|
27
|
+
let outdatedComments = 0;
|
|
28
|
+
let undocumentedComplexity = 0;
|
|
29
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
30
|
+
let processed = 0;
|
|
31
|
+
for (const file of files) {
|
|
32
|
+
processed++;
|
|
33
|
+
emitProgress(
|
|
34
|
+
processed,
|
|
35
|
+
files.length,
|
|
36
|
+
"doc-drift",
|
|
37
|
+
"analyzing files",
|
|
38
|
+
options.onProgress
|
|
39
|
+
);
|
|
40
|
+
const parser = getParser(file);
|
|
41
|
+
if (!parser) continue;
|
|
42
|
+
let code;
|
|
43
|
+
try {
|
|
44
|
+
code = readFileSync(file, "utf-8");
|
|
45
|
+
} catch {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
await parser.initialize();
|
|
50
|
+
const parseResult = parser.parse(code, file);
|
|
51
|
+
let fileLineStamps;
|
|
52
|
+
for (const exp of parseResult.exports) {
|
|
53
|
+
if (exp.type === "function" || exp.type === "class") {
|
|
54
|
+
totalExports++;
|
|
55
|
+
if (!exp.documentation) {
|
|
56
|
+
uncommentedExports++;
|
|
57
|
+
if (exp.loc) {
|
|
58
|
+
const lines = exp.loc.end.line - exp.loc.start.line;
|
|
59
|
+
if (lines > 20) undocumentedComplexity++;
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
const doc = exp.documentation;
|
|
63
|
+
const docContent = doc.content;
|
|
64
|
+
if (exp.type === "function" && exp.parameters) {
|
|
65
|
+
const params = exp.parameters;
|
|
66
|
+
const missingParams = params.filter((p) => {
|
|
67
|
+
const regex = new RegExp(`\\b${p}\\b`, "i");
|
|
68
|
+
return !regex.test(docContent);
|
|
69
|
+
});
|
|
70
|
+
if (missingParams.length > 0) {
|
|
71
|
+
outdatedComments++;
|
|
72
|
+
issues.push({
|
|
73
|
+
type: IssueType.DocDrift,
|
|
74
|
+
severity: Severity.Major,
|
|
75
|
+
message: `Documentation mismatch: function parameters (${missingParams.join(", ")}) are not mentioned in the docs.`,
|
|
76
|
+
location: { file, line: exp.loc?.start.line || 1 }
|
|
77
|
+
});
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (exp.loc) {
|
|
82
|
+
if (!fileLineStamps) {
|
|
83
|
+
fileLineStamps = getFileCommitTimestamps(file);
|
|
84
|
+
}
|
|
85
|
+
const bodyModified = getLineRangeLastModifiedCached(
|
|
86
|
+
fileLineStamps,
|
|
87
|
+
exp.loc.start.line,
|
|
88
|
+
exp.loc.end.line
|
|
89
|
+
);
|
|
90
|
+
if (bodyModified > 0) {
|
|
91
|
+
if (now - bodyModified < staleSeconds / 4 && exp.documentation.isStale === true) {
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.warn(`Doc-drift: Failed to parse ${file}: ${error}`);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const riskResult = calculateDocDrift({
|
|
104
|
+
uncommentedExports,
|
|
105
|
+
totalExports,
|
|
106
|
+
outdatedComments,
|
|
107
|
+
undocumentedComplexity
|
|
108
|
+
});
|
|
109
|
+
return {
|
|
110
|
+
summary: {
|
|
111
|
+
filesAnalyzed: files.length,
|
|
112
|
+
functionsAnalyzed: totalExports,
|
|
113
|
+
score: riskResult.score,
|
|
114
|
+
rating: riskResult.rating
|
|
115
|
+
},
|
|
116
|
+
issues,
|
|
117
|
+
rawData: {
|
|
118
|
+
uncommentedExports,
|
|
119
|
+
totalExports,
|
|
120
|
+
outdatedComments,
|
|
121
|
+
undocumentedComplexity
|
|
122
|
+
},
|
|
123
|
+
recommendations: riskResult.recommendations
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export {
|
|
128
|
+
__require,
|
|
129
|
+
analyzeDocDrift
|
|
130
|
+
};
|
|
@@ -0,0 +1,129 @@
|
|
|
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
|
+
Severity,
|
|
15
|
+
IssueType,
|
|
16
|
+
emitProgress,
|
|
17
|
+
getParser
|
|
18
|
+
} from "@aiready/core";
|
|
19
|
+
import { readFileSync } from "fs";
|
|
20
|
+
async function analyzeDocDrift(options) {
|
|
21
|
+
const files = await scanFiles(options);
|
|
22
|
+
const issues = [];
|
|
23
|
+
const staleMonths = options.staleMonths ?? 6;
|
|
24
|
+
const staleSeconds = staleMonths * 30 * 24 * 60 * 60;
|
|
25
|
+
let uncommentedExports = 0;
|
|
26
|
+
let totalExports = 0;
|
|
27
|
+
let outdatedComments = 0;
|
|
28
|
+
let undocumentedComplexity = 0;
|
|
29
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
30
|
+
let processed = 0;
|
|
31
|
+
for (const file of files) {
|
|
32
|
+
processed++;
|
|
33
|
+
emitProgress(
|
|
34
|
+
processed,
|
|
35
|
+
files.length,
|
|
36
|
+
"doc-drift",
|
|
37
|
+
"analyzing files",
|
|
38
|
+
options.onProgress
|
|
39
|
+
);
|
|
40
|
+
const parser = getParser(file);
|
|
41
|
+
if (!parser) continue;
|
|
42
|
+
let code;
|
|
43
|
+
try {
|
|
44
|
+
code = readFileSync(file, "utf-8");
|
|
45
|
+
} catch {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
await parser.initialize();
|
|
50
|
+
const parseResult = parser.parse(code, file);
|
|
51
|
+
let fileLineStamps;
|
|
52
|
+
for (const exp of parseResult.exports) {
|
|
53
|
+
if (exp.type === "function" || exp.type === "class") {
|
|
54
|
+
totalExports++;
|
|
55
|
+
if (!exp.documentation) {
|
|
56
|
+
uncommentedExports++;
|
|
57
|
+
if (exp.loc) {
|
|
58
|
+
const lines = exp.loc.end.line - exp.loc.start.line;
|
|
59
|
+
if (lines > 20) undocumentedComplexity++;
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
const doc = exp.documentation;
|
|
63
|
+
const docContent = doc.content;
|
|
64
|
+
if (exp.type === "function" && exp.parameters) {
|
|
65
|
+
const params = exp.parameters;
|
|
66
|
+
const missingParams = params.filter(
|
|
67
|
+
(p) => !docContent.includes(p) && !docContent.toLowerCase().includes(p.toLowerCase())
|
|
68
|
+
);
|
|
69
|
+
if (missingParams.length > 0 && params.length > 2) {
|
|
70
|
+
outdatedComments++;
|
|
71
|
+
issues.push({
|
|
72
|
+
type: IssueType.DocDrift,
|
|
73
|
+
severity: Severity.Major,
|
|
74
|
+
message: `Documentation mismatch: function parameters (${missingParams.join(", ")}) are not mentioned in the docs.`,
|
|
75
|
+
location: { file, line: exp.loc?.start.line || 1 }
|
|
76
|
+
});
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (exp.loc) {
|
|
81
|
+
if (!fileLineStamps) {
|
|
82
|
+
fileLineStamps = getFileCommitTimestamps(file);
|
|
83
|
+
}
|
|
84
|
+
const bodyModified = getLineRangeLastModifiedCached(
|
|
85
|
+
fileLineStamps,
|
|
86
|
+
exp.loc.start.line,
|
|
87
|
+
exp.loc.end.line
|
|
88
|
+
);
|
|
89
|
+
if (bodyModified > 0) {
|
|
90
|
+
if (now - bodyModified < staleSeconds / 4 && exp.documentation.isStale === true) {
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.warn(`Doc-drift: Failed to parse ${file}: ${error}`);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const riskResult = calculateDocDrift({
|
|
103
|
+
uncommentedExports,
|
|
104
|
+
totalExports,
|
|
105
|
+
outdatedComments,
|
|
106
|
+
undocumentedComplexity
|
|
107
|
+
});
|
|
108
|
+
return {
|
|
109
|
+
summary: {
|
|
110
|
+
filesAnalyzed: files.length,
|
|
111
|
+
functionsAnalyzed: totalExports,
|
|
112
|
+
score: riskResult.score,
|
|
113
|
+
rating: riskResult.rating
|
|
114
|
+
},
|
|
115
|
+
issues,
|
|
116
|
+
rawData: {
|
|
117
|
+
uncommentedExports,
|
|
118
|
+
totalExports,
|
|
119
|
+
outdatedComments,
|
|
120
|
+
undocumentedComplexity
|
|
121
|
+
},
|
|
122
|
+
recommendations: riskResult.recommendations
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export {
|
|
127
|
+
__require,
|
|
128
|
+
analyzeDocDrift
|
|
129
|
+
};
|
package/dist/cli.js
CHANGED
|
@@ -38,7 +38,6 @@ 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_typescript_estree = require("@typescript-eslint/typescript-estree");
|
|
42
41
|
async function analyzeDocDrift(options) {
|
|
43
42
|
const files = await (0, import_core.scanFiles)(options);
|
|
44
43
|
const issues = [];
|
|
@@ -59,89 +58,67 @@ async function analyzeDocDrift(options) {
|
|
|
59
58
|
"analyzing files",
|
|
60
59
|
options.onProgress
|
|
61
60
|
);
|
|
61
|
+
const parser = (0, import_core.getParser)(file);
|
|
62
|
+
if (!parser) continue;
|
|
62
63
|
let code;
|
|
63
64
|
try {
|
|
64
65
|
code = (0, import_fs.readFileSync)(file, "utf-8");
|
|
65
66
|
} catch {
|
|
66
67
|
continue;
|
|
67
68
|
}
|
|
68
|
-
let ast;
|
|
69
69
|
try {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
} catch {
|
|
76
|
-
continue;
|
|
77
|
-
}
|
|
78
|
-
const comments = ast.comments || [];
|
|
79
|
-
let fileLineStamps;
|
|
80
|
-
for (const node of ast.body) {
|
|
81
|
-
if (node.type === "ExportNamedDeclaration" || node.type === "ExportDefaultDeclaration") {
|
|
82
|
-
const decl = node.declaration;
|
|
83
|
-
if (!decl) continue;
|
|
84
|
-
if (decl.type === "FunctionDeclaration" || decl.type === "ClassDeclaration" || decl.type === "VariableDeclaration") {
|
|
70
|
+
await parser.initialize();
|
|
71
|
+
const parseResult = parser.parse(code, file);
|
|
72
|
+
let fileLineStamps;
|
|
73
|
+
for (const exp of parseResult.exports) {
|
|
74
|
+
if (exp.type === "function" || exp.type === "class") {
|
|
85
75
|
totalExports++;
|
|
86
|
-
|
|
87
|
-
const jsdocs = comments.filter(
|
|
88
|
-
(c) => c.type === "Block" && c.value.startsWith("*") && c.loc.end.line === nodeLine - 1
|
|
89
|
-
);
|
|
90
|
-
if (jsdocs.length === 0) {
|
|
76
|
+
if (!exp.documentation) {
|
|
91
77
|
uncommentedExports++;
|
|
92
|
-
if (
|
|
93
|
-
const lines =
|
|
78
|
+
if (exp.loc) {
|
|
79
|
+
const lines = exp.loc.end.line - exp.loc.start.line;
|
|
94
80
|
if (lines > 20) undocumentedComplexity++;
|
|
95
81
|
}
|
|
96
82
|
} else {
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
if (
|
|
100
|
-
const params =
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
(p) => !paramTags.includes(p)
|
|
106
|
-
);
|
|
83
|
+
const doc = exp.documentation;
|
|
84
|
+
const docContent = doc.content;
|
|
85
|
+
if (exp.type === "function" && exp.parameters) {
|
|
86
|
+
const params = exp.parameters;
|
|
87
|
+
const missingParams = params.filter((p) => {
|
|
88
|
+
const regex = new RegExp(`\\b${p}\\b`, "i");
|
|
89
|
+
return !regex.test(docContent);
|
|
90
|
+
});
|
|
107
91
|
if (missingParams.length > 0) {
|
|
108
92
|
outdatedComments++;
|
|
109
93
|
issues.push({
|
|
110
94
|
type: import_core.IssueType.DocDrift,
|
|
111
95
|
severity: import_core.Severity.Major,
|
|
112
|
-
message: `
|
|
113
|
-
location: { file, line:
|
|
96
|
+
message: `Documentation mismatch: function parameters (${missingParams.join(", ")}) are not mentioned in the docs.`,
|
|
97
|
+
location: { file, line: exp.loc?.start.line || 1 }
|
|
114
98
|
});
|
|
115
99
|
continue;
|
|
116
100
|
}
|
|
117
101
|
}
|
|
118
|
-
if (
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
);
|
|
131
|
-
if (commentModified > 0 && bodyModified > 0) {
|
|
132
|
-
if (now - commentModified > staleSeconds && bodyModified - commentModified > staleSeconds / 2) {
|
|
133
|
-
outdatedComments++;
|
|
134
|
-
issues.push({
|
|
135
|
-
type: import_core.IssueType.DocDrift,
|
|
136
|
-
severity: import_core.Severity.Minor,
|
|
137
|
-
message: `JSDoc is significantly older than the function body implementation. Code may have drifted.`,
|
|
138
|
-
location: { file, line: jsdoc.loc.start.line }
|
|
139
|
-
});
|
|
102
|
+
if (exp.loc) {
|
|
103
|
+
if (!fileLineStamps) {
|
|
104
|
+
fileLineStamps = (0, import_core.getFileCommitTimestamps)(file);
|
|
105
|
+
}
|
|
106
|
+
const bodyModified = (0, import_core.getLineRangeLastModifiedCached)(
|
|
107
|
+
fileLineStamps,
|
|
108
|
+
exp.loc.start.line,
|
|
109
|
+
exp.loc.end.line
|
|
110
|
+
);
|
|
111
|
+
if (bodyModified > 0) {
|
|
112
|
+
if (now - bodyModified < staleSeconds / 4 && exp.documentation.isStale === true) {
|
|
113
|
+
}
|
|
140
114
|
}
|
|
141
115
|
}
|
|
142
116
|
}
|
|
143
117
|
}
|
|
144
118
|
}
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.warn(`Doc-drift: Failed to parse ${file}: ${error}`);
|
|
121
|
+
continue;
|
|
145
122
|
}
|
|
146
123
|
}
|
|
147
124
|
const riskResult = (0, import_core.calculateDocDrift)({
|
package/dist/cli.mjs
CHANGED
package/dist/index.js
CHANGED
|
@@ -32,7 +32,6 @@ var import_core2 = require("@aiready/core");
|
|
|
32
32
|
// src/analyzer.ts
|
|
33
33
|
var import_core = require("@aiready/core");
|
|
34
34
|
var import_fs = require("fs");
|
|
35
|
-
var import_typescript_estree = require("@typescript-eslint/typescript-estree");
|
|
36
35
|
async function analyzeDocDrift(options) {
|
|
37
36
|
const files = await (0, import_core.scanFiles)(options);
|
|
38
37
|
const issues = [];
|
|
@@ -53,89 +52,67 @@ async function analyzeDocDrift(options) {
|
|
|
53
52
|
"analyzing files",
|
|
54
53
|
options.onProgress
|
|
55
54
|
);
|
|
55
|
+
const parser = (0, import_core.getParser)(file);
|
|
56
|
+
if (!parser) continue;
|
|
56
57
|
let code;
|
|
57
58
|
try {
|
|
58
59
|
code = (0, import_fs.readFileSync)(file, "utf-8");
|
|
59
60
|
} catch {
|
|
60
61
|
continue;
|
|
61
62
|
}
|
|
62
|
-
let ast;
|
|
63
63
|
try {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
} catch {
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
const comments = ast.comments || [];
|
|
73
|
-
let fileLineStamps;
|
|
74
|
-
for (const node of ast.body) {
|
|
75
|
-
if (node.type === "ExportNamedDeclaration" || node.type === "ExportDefaultDeclaration") {
|
|
76
|
-
const decl = node.declaration;
|
|
77
|
-
if (!decl) continue;
|
|
78
|
-
if (decl.type === "FunctionDeclaration" || decl.type === "ClassDeclaration" || decl.type === "VariableDeclaration") {
|
|
64
|
+
await parser.initialize();
|
|
65
|
+
const parseResult = parser.parse(code, file);
|
|
66
|
+
let fileLineStamps;
|
|
67
|
+
for (const exp of parseResult.exports) {
|
|
68
|
+
if (exp.type === "function" || exp.type === "class") {
|
|
79
69
|
totalExports++;
|
|
80
|
-
|
|
81
|
-
const jsdocs = comments.filter(
|
|
82
|
-
(c) => c.type === "Block" && c.value.startsWith("*") && c.loc.end.line === nodeLine - 1
|
|
83
|
-
);
|
|
84
|
-
if (jsdocs.length === 0) {
|
|
70
|
+
if (!exp.documentation) {
|
|
85
71
|
uncommentedExports++;
|
|
86
|
-
if (
|
|
87
|
-
const lines =
|
|
72
|
+
if (exp.loc) {
|
|
73
|
+
const lines = exp.loc.end.line - exp.loc.start.line;
|
|
88
74
|
if (lines > 20) undocumentedComplexity++;
|
|
89
75
|
}
|
|
90
76
|
} else {
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
if (
|
|
94
|
-
const params =
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
(p) => !paramTags.includes(p)
|
|
100
|
-
);
|
|
77
|
+
const doc = exp.documentation;
|
|
78
|
+
const docContent = doc.content;
|
|
79
|
+
if (exp.type === "function" && exp.parameters) {
|
|
80
|
+
const params = exp.parameters;
|
|
81
|
+
const missingParams = params.filter((p) => {
|
|
82
|
+
const regex = new RegExp(`\\b${p}\\b`, "i");
|
|
83
|
+
return !regex.test(docContent);
|
|
84
|
+
});
|
|
101
85
|
if (missingParams.length > 0) {
|
|
102
86
|
outdatedComments++;
|
|
103
87
|
issues.push({
|
|
104
88
|
type: import_core.IssueType.DocDrift,
|
|
105
89
|
severity: import_core.Severity.Major,
|
|
106
|
-
message: `
|
|
107
|
-
location: { file, line:
|
|
90
|
+
message: `Documentation mismatch: function parameters (${missingParams.join(", ")}) are not mentioned in the docs.`,
|
|
91
|
+
location: { file, line: exp.loc?.start.line || 1 }
|
|
108
92
|
});
|
|
109
93
|
continue;
|
|
110
94
|
}
|
|
111
95
|
}
|
|
112
|
-
if (
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
);
|
|
125
|
-
if (commentModified > 0 && bodyModified > 0) {
|
|
126
|
-
if (now - commentModified > staleSeconds && bodyModified - commentModified > staleSeconds / 2) {
|
|
127
|
-
outdatedComments++;
|
|
128
|
-
issues.push({
|
|
129
|
-
type: import_core.IssueType.DocDrift,
|
|
130
|
-
severity: import_core.Severity.Minor,
|
|
131
|
-
message: `JSDoc is significantly older than the function body implementation. Code may have drifted.`,
|
|
132
|
-
location: { file, line: jsdoc.loc.start.line }
|
|
133
|
-
});
|
|
96
|
+
if (exp.loc) {
|
|
97
|
+
if (!fileLineStamps) {
|
|
98
|
+
fileLineStamps = (0, import_core.getFileCommitTimestamps)(file);
|
|
99
|
+
}
|
|
100
|
+
const bodyModified = (0, import_core.getLineRangeLastModifiedCached)(
|
|
101
|
+
fileLineStamps,
|
|
102
|
+
exp.loc.start.line,
|
|
103
|
+
exp.loc.end.line
|
|
104
|
+
);
|
|
105
|
+
if (bodyModified > 0) {
|
|
106
|
+
if (now - bodyModified < staleSeconds / 4 && exp.documentation.isStale === true) {
|
|
107
|
+
}
|
|
134
108
|
}
|
|
135
109
|
}
|
|
136
110
|
}
|
|
137
111
|
}
|
|
138
112
|
}
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.warn(`Doc-drift: Failed to parse ${file}: ${error}`);
|
|
115
|
+
continue;
|
|
139
116
|
}
|
|
140
117
|
}
|
|
141
118
|
const riskResult = (0, import_core.calculateDocDrift)({
|
package/dist/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiready/doc-drift",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.18",
|
|
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.21.
|
|
13
|
+
"@aiready/core": "0.21.18"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
16
|
"@types/node": "^24.0.0",
|
package/src/analyzer.ts
CHANGED
|
@@ -6,11 +6,11 @@ import {
|
|
|
6
6
|
Severity,
|
|
7
7
|
IssueType,
|
|
8
8
|
emitProgress,
|
|
9
|
+
getParser,
|
|
10
|
+
Language,
|
|
9
11
|
} from '@aiready/core';
|
|
10
12
|
import type { DocDriftOptions, DocDriftReport, DocDriftIssue } from './types';
|
|
11
13
|
import { readFileSync } from 'fs';
|
|
12
|
-
import { parse } from '@typescript-eslint/typescript-estree';
|
|
13
|
-
import type { TSESTree } from '@typescript-eslint/types';
|
|
14
14
|
|
|
15
15
|
export async function analyzeDocDrift(
|
|
16
16
|
options: DocDriftOptions
|
|
@@ -39,6 +39,9 @@ export async function analyzeDocDrift(
|
|
|
39
39
|
options.onProgress
|
|
40
40
|
);
|
|
41
41
|
|
|
42
|
+
const parser = getParser(file);
|
|
43
|
+
if (!parser) continue;
|
|
44
|
+
|
|
42
45
|
let code: string;
|
|
43
46
|
try {
|
|
44
47
|
code = readFileSync(file, 'utf-8');
|
|
@@ -46,114 +49,86 @@ export async function analyzeDocDrift(
|
|
|
46
49
|
continue;
|
|
47
50
|
}
|
|
48
51
|
|
|
49
|
-
let ast: TSESTree.Program;
|
|
50
52
|
try {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
comment: true,
|
|
55
|
-
});
|
|
56
|
-
} catch {
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
53
|
+
// Initialize parser (it's a singleton in core, but ensures WASM is loaded)
|
|
54
|
+
await parser.initialize();
|
|
55
|
+
const parseResult = parser.parse(code, file);
|
|
59
56
|
|
|
60
|
-
|
|
61
|
-
let fileLineStamps: Record<number, number> | undefined;
|
|
62
|
-
|
|
63
|
-
for (const node of ast.body) {
|
|
64
|
-
if (
|
|
65
|
-
node.type === 'ExportNamedDeclaration' ||
|
|
66
|
-
node.type === 'ExportDefaultDeclaration'
|
|
67
|
-
) {
|
|
68
|
-
const decl = (node as any).declaration;
|
|
69
|
-
if (!decl) continue;
|
|
70
|
-
|
|
71
|
-
// Count exports
|
|
72
|
-
if (
|
|
73
|
-
decl.type === 'FunctionDeclaration' ||
|
|
74
|
-
decl.type === 'ClassDeclaration' ||
|
|
75
|
-
decl.type === 'VariableDeclaration'
|
|
76
|
-
) {
|
|
77
|
-
totalExports++;
|
|
57
|
+
let fileLineStamps: Record<number, number> | undefined;
|
|
78
58
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
c.type === 'Block' &&
|
|
84
|
-
c.value.startsWith('*') &&
|
|
85
|
-
c.loc.end.line === nodeLine - 1
|
|
86
|
-
);
|
|
59
|
+
for (const exp of parseResult.exports) {
|
|
60
|
+
// Only analyze functions and classes for documentation drift
|
|
61
|
+
if (exp.type === 'function' || exp.type === 'class') {
|
|
62
|
+
totalExports++;
|
|
87
63
|
|
|
88
|
-
if (
|
|
64
|
+
if (!exp.documentation) {
|
|
89
65
|
uncommentedExports++;
|
|
90
66
|
|
|
91
|
-
//
|
|
92
|
-
if (
|
|
93
|
-
const lines =
|
|
67
|
+
// Complexity check (heuristic based on line count if range available)
|
|
68
|
+
if (exp.loc) {
|
|
69
|
+
const lines = exp.loc.end.line - exp.loc.start.line;
|
|
94
70
|
if (lines > 20) undocumentedComplexity++;
|
|
95
71
|
}
|
|
96
72
|
} else {
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
// Signature mismatch detection
|
|
101
|
-
if (
|
|
102
|
-
const params =
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
(p: string) => !paramTags.includes(p)
|
|
111
|
-
);
|
|
73
|
+
const doc = exp.documentation;
|
|
74
|
+
const docContent = doc.content;
|
|
75
|
+
|
|
76
|
+
// Signature mismatch detection (generalized heuristic)
|
|
77
|
+
if (exp.type === 'function' && exp.parameters) {
|
|
78
|
+
const params = exp.parameters;
|
|
79
|
+
// Check if params mentioned in doc (standard @param or simple mention)
|
|
80
|
+
// Use regex with word boundaries to avoid partial matches (e.g. 'b' in 'numbers')
|
|
81
|
+
const missingParams = params.filter((p) => {
|
|
82
|
+
const regex = new RegExp(`\\b${p}\\b`, 'i');
|
|
83
|
+
return !regex.test(docContent);
|
|
84
|
+
});
|
|
85
|
+
|
|
112
86
|
if (missingParams.length > 0) {
|
|
113
87
|
outdatedComments++;
|
|
114
88
|
issues.push({
|
|
115
89
|
type: IssueType.DocDrift,
|
|
116
90
|
severity: Severity.Major,
|
|
117
|
-
message: `
|
|
118
|
-
location: { file, line:
|
|
91
|
+
message: `Documentation mismatch: function parameters (${missingParams.join(', ')}) are not mentioned in the docs.`,
|
|
92
|
+
location: { file, line: exp.loc?.start.line || 1 },
|
|
119
93
|
});
|
|
120
|
-
continue;
|
|
94
|
+
continue;
|
|
121
95
|
}
|
|
122
96
|
}
|
|
123
97
|
|
|
124
98
|
// Timestamp comparison
|
|
125
|
-
if (
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
location: { file, line: jsdoc.loc.start.line },
|
|
151
|
-
});
|
|
99
|
+
if (exp.loc) {
|
|
100
|
+
if (!fileLineStamps) {
|
|
101
|
+
fileLineStamps = getFileCommitTimestamps(file);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// We don't have exact lines for the doc node in ExportInfo yet,
|
|
105
|
+
// but we know it precedes the export. Using export start as a proxy for drift check.
|
|
106
|
+
const bodyModified = getLineRangeLastModifiedCached(
|
|
107
|
+
fileLineStamps,
|
|
108
|
+
exp.loc.start.line,
|
|
109
|
+
exp.loc.end.line
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
if (bodyModified > 0) {
|
|
113
|
+
// If body was modified much later than the "stale" threshold
|
|
114
|
+
if (
|
|
115
|
+
now - bodyModified < staleSeconds / 4 &&
|
|
116
|
+
exp.documentation.isStale === true
|
|
117
|
+
) {
|
|
118
|
+
// This would require isStale to be set by the parser if it knew history
|
|
119
|
+
// For now, we compare body modification vs current time if docs look very old (heuristic)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// If the file itself is very old but has no issues, it's fine.
|
|
123
|
+
// Doc-drift is really about implementation changing without doc updates.
|
|
152
124
|
}
|
|
153
125
|
}
|
|
154
126
|
}
|
|
155
127
|
}
|
|
156
128
|
}
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.warn(`Doc-drift: Failed to parse ${file}: ${error}`);
|
|
131
|
+
continue;
|
|
157
132
|
}
|
|
158
133
|
}
|
|
159
134
|
|