@aiready/doc-drift 0.13.4 → 0.13.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/dist/{chunk-NWJ6PSNT.mjs → chunk-6EHZXHM7.mjs} +26 -12
- package/dist/chunk-MAPV5YQR.mjs +142 -0
- package/dist/{chunk-5BGWZWHD.mjs → chunk-P74XAVQ3.mjs} +21 -6
- package/dist/cli.js +21 -9
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +17 -1
- package/dist/index.d.ts +17 -1
- package/dist/index.js +39 -20
- package/dist/index.mjs +19 -12
- package/package.json +2 -2
- package/src/__tests__/analyzer.test.ts +1 -0
- package/src/__tests__/provider.test.ts +1 -0
- package/src/__tests__/scoring.test.ts +1 -0
- package/src/analyzer.ts +34 -20
- package/src/scoring.ts +29 -12
- package/src/types.ts +2 -0
- package/dist/chunk-5EFFNN6L.mjs +0 -145
- package/dist/chunk-BBGJNBVI.mjs +0 -189
- package/dist/chunk-CGSYYULO.mjs +0 -145
- package/dist/chunk-E3YCVHHH.mjs +0 -152
- package/dist/chunk-FMK4O4O7.mjs +0 -143
- package/dist/chunk-TSLAGWBV.mjs +0 -165
- package/dist/chunk-VLBPAYS3.mjs +0 -209
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiready/doc-drift",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.6",
|
|
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.23.
|
|
13
|
+
"@aiready/core": "0.23.7"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
16
|
"@types/node": "^24.0.0",
|
|
@@ -76,6 +76,7 @@ export function complexFunction(data: any) {
|
|
|
76
76
|
expect(report.rawData.uncommentedExports).toBe(1);
|
|
77
77
|
expect(report.rawData.outdatedComments).toBe(1);
|
|
78
78
|
expect(report.rawData.undocumentedComplexity).toBe(1);
|
|
79
|
+
expect(report.rawData.actualDrift).toBe(0);
|
|
79
80
|
|
|
80
81
|
expect(report.issues.length).toBeGreaterThan(0);
|
|
81
82
|
expect(report.issues.some((i) => i.message.includes('b'))).toBe(true);
|
package/src/analyzer.ts
CHANGED
|
@@ -11,21 +11,31 @@ import {
|
|
|
11
11
|
import type { DocDriftOptions, DocDriftReport, DocDriftIssue } from './types';
|
|
12
12
|
import { readFileSync } from 'fs';
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Analyzes documentation drift across a set of files.
|
|
16
|
+
* This tool detects:
|
|
17
|
+
* 1. Missing documentation for complex functions/classes.
|
|
18
|
+
* 2. Signature mismatches (parameters not mentioned in docs).
|
|
19
|
+
* 3. Temporal drift (logic changed after documentation was last updated).
|
|
20
|
+
*
|
|
21
|
+
* @param options - Analysis configuration including include/exclude patterns and drift thresholds.
|
|
22
|
+
* @returns A comprehensive report with drift scores and specific issues.
|
|
23
|
+
*/
|
|
14
24
|
export async function analyzeDocDrift(
|
|
15
25
|
options: DocDriftOptions
|
|
16
26
|
): Promise<DocDriftReport> {
|
|
17
27
|
// Use core scanFiles which respects .gitignore recursively
|
|
18
28
|
const files = await scanFiles(options);
|
|
19
29
|
const issues: DocDriftIssue[] = [];
|
|
20
|
-
const
|
|
21
|
-
const staleSeconds = staleMonths * 30 * 24 * 60 * 60;
|
|
30
|
+
// const staleSeconds = staleMonths * 30 * 24 * 60 * 60; // Unused, removed
|
|
22
31
|
|
|
23
32
|
let uncommentedExports = 0;
|
|
24
33
|
let totalExports = 0;
|
|
25
34
|
let outdatedComments = 0;
|
|
26
35
|
let undocumentedComplexity = 0;
|
|
36
|
+
let actualDrift = 0;
|
|
27
37
|
|
|
28
|
-
const now = Math.floor(Date.now() / 1000);
|
|
38
|
+
// const now = Math.floor(Date.now() / 1000); // Unused, removed
|
|
29
39
|
|
|
30
40
|
let processed = 0;
|
|
31
41
|
for (const file of files) {
|
|
@@ -76,7 +86,6 @@ export async function analyzeDocDrift(
|
|
|
76
86
|
if (exp.type === 'function' && exp.parameters) {
|
|
77
87
|
const params = exp.parameters;
|
|
78
88
|
// Check if params mentioned in doc (standard @param or simple mention)
|
|
79
|
-
// Use regex with word boundaries to avoid partial matches (e.g. 'b' in 'numbers')
|
|
80
89
|
const missingParams = params.filter((p) => {
|
|
81
90
|
const regex = new RegExp(`\\b${p}\\b`, 'i');
|
|
82
91
|
return !regex.test(docContent);
|
|
@@ -90,36 +99,39 @@ export async function analyzeDocDrift(
|
|
|
90
99
|
message: `Documentation mismatch: function parameters (${missingParams.join(', ')}) are not mentioned in the docs.`,
|
|
91
100
|
location: { file, line: exp.loc?.start.line || 1 },
|
|
92
101
|
});
|
|
93
|
-
continue;
|
|
94
102
|
}
|
|
95
103
|
}
|
|
96
104
|
|
|
97
|
-
// Timestamp comparison
|
|
98
|
-
if (exp.loc) {
|
|
105
|
+
// Timestamp comparison for temporal drift
|
|
106
|
+
if (exp.loc && doc.loc) {
|
|
99
107
|
if (!fileLineStamps) {
|
|
100
108
|
fileLineStamps = getFileCommitTimestamps(file);
|
|
101
109
|
}
|
|
102
110
|
|
|
103
|
-
// We don't have exact lines for the doc node in ExportInfo yet,
|
|
104
|
-
// but we know it precedes the export. Using export start as a proxy for drift check.
|
|
105
111
|
const bodyModified = getLineRangeLastModifiedCached(
|
|
106
112
|
fileLineStamps,
|
|
107
113
|
exp.loc.start.line,
|
|
108
114
|
exp.loc.end.line
|
|
109
115
|
);
|
|
110
116
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
) {
|
|
117
|
-
// This would require isStale to be set by the parser if it knew history
|
|
118
|
-
// For now, we compare body modification vs current time if docs look very old (heuristic)
|
|
119
|
-
}
|
|
117
|
+
const docModified = getLineRangeLastModifiedCached(
|
|
118
|
+
fileLineStamps,
|
|
119
|
+
doc.loc.start.line,
|
|
120
|
+
doc.loc.end.line
|
|
121
|
+
);
|
|
120
122
|
|
|
121
|
-
|
|
122
|
-
//
|
|
123
|
+
if (bodyModified > 0 && docModified > 0) {
|
|
124
|
+
// If body was modified more than 1 day AFTER the documentation
|
|
125
|
+
const DRIFT_THRESHOLD_SECONDS = 24 * 60 * 60;
|
|
126
|
+
if (bodyModified - docModified > DRIFT_THRESHOLD_SECONDS) {
|
|
127
|
+
actualDrift++;
|
|
128
|
+
issues.push({
|
|
129
|
+
type: IssueType.DocDrift,
|
|
130
|
+
severity: Severity.Major,
|
|
131
|
+
message: `Documentation drift: logic was modified on ${new Date(bodyModified * 1000).toLocaleDateString()} but documentation was last updated on ${new Date(docModified * 1000).toLocaleDateString()}.`,
|
|
132
|
+
location: { file, line: doc.loc.start.line },
|
|
133
|
+
});
|
|
134
|
+
}
|
|
123
135
|
}
|
|
124
136
|
}
|
|
125
137
|
}
|
|
@@ -136,6 +148,7 @@ export async function analyzeDocDrift(
|
|
|
136
148
|
totalExports,
|
|
137
149
|
outdatedComments,
|
|
138
150
|
undocumentedComplexity,
|
|
151
|
+
actualDrift,
|
|
139
152
|
});
|
|
140
153
|
|
|
141
154
|
return {
|
|
@@ -151,6 +164,7 @@ export async function analyzeDocDrift(
|
|
|
151
164
|
totalExports,
|
|
152
165
|
outdatedComments,
|
|
153
166
|
undocumentedComplexity,
|
|
167
|
+
actualDrift,
|
|
154
168
|
},
|
|
155
169
|
recommendations: riskResult.recommendations,
|
|
156
170
|
};
|
package/src/scoring.ts
CHANGED
|
@@ -3,7 +3,11 @@ import type { ToolScoringOutput } from '@aiready/core';
|
|
|
3
3
|
import type { DocDriftReport } from './types';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Convert doc-drift report into a ToolScoringOutput.
|
|
6
|
+
* Convert doc-drift report into a standardized ToolScoringOutput.
|
|
7
|
+
*
|
|
8
|
+
* @param report - The detailed doc-drift report including raw metrics.
|
|
9
|
+
* @returns Standardized scoring and risk factor breakdown.
|
|
10
|
+
* @lastUpdated 2026-03-18
|
|
7
11
|
*/
|
|
8
12
|
export function calculateDocDriftScore(
|
|
9
13
|
report: DocDriftReport
|
|
@@ -16,28 +20,41 @@ export function calculateDocDriftScore(
|
|
|
16
20
|
totalExports: rawData.totalExports,
|
|
17
21
|
outdatedComments: rawData.outdatedComments,
|
|
18
22
|
undocumentedComplexity: rawData.undocumentedComplexity,
|
|
23
|
+
actualDrift: rawData.actualDrift,
|
|
19
24
|
});
|
|
20
25
|
|
|
21
26
|
const factors: ToolScoringOutput['factors'] = [
|
|
22
27
|
{
|
|
23
|
-
name: '
|
|
28
|
+
name: 'Undocumented Complexity',
|
|
24
29
|
impact: -Math.min(
|
|
25
|
-
|
|
26
|
-
(rawData.
|
|
30
|
+
50,
|
|
31
|
+
(rawData.undocumentedComplexity /
|
|
32
|
+
Math.max(1, rawData.totalExports) /
|
|
33
|
+
0.2) *
|
|
27
34
|
100 *
|
|
28
|
-
0.
|
|
35
|
+
0.5
|
|
29
36
|
),
|
|
30
|
-
description: `${rawData.
|
|
37
|
+
description: `${rawData.undocumentedComplexity} complex functions lack docs (high risk)`,
|
|
31
38
|
},
|
|
32
39
|
{
|
|
33
|
-
name: 'Outdated Comments',
|
|
34
|
-
impact: -Math.min(
|
|
35
|
-
|
|
40
|
+
name: 'Outdated/Incomplete Comments',
|
|
41
|
+
impact: -Math.min(
|
|
42
|
+
30,
|
|
43
|
+
(rawData.outdatedComments / Math.max(1, rawData.totalExports) / 0.4) *
|
|
44
|
+
100 *
|
|
45
|
+
0.3
|
|
46
|
+
),
|
|
47
|
+
description: `${rawData.outdatedComments} functions with parameter-mismatch in docs`,
|
|
36
48
|
},
|
|
37
49
|
{
|
|
38
|
-
name: '
|
|
39
|
-
impact: -Math.min(
|
|
40
|
-
|
|
50
|
+
name: 'Uncommented Exports',
|
|
51
|
+
impact: -Math.min(
|
|
52
|
+
20,
|
|
53
|
+
(rawData.uncommentedExports / Math.max(1, rawData.totalExports) / 0.8) *
|
|
54
|
+
100 *
|
|
55
|
+
0.2
|
|
56
|
+
),
|
|
57
|
+
description: `${rawData.uncommentedExports} uncommented exports`,
|
|
41
58
|
},
|
|
42
59
|
];
|
|
43
60
|
|
package/src/types.ts
CHANGED
|
@@ -50,6 +50,8 @@ export interface DocDriftReport {
|
|
|
50
50
|
outdatedComments: number;
|
|
51
51
|
/** Count of complex functions without sufficient documentation */
|
|
52
52
|
undocumentedComplexity: number;
|
|
53
|
+
/** Number of functions where code changed after docs */
|
|
54
|
+
actualDrift: number;
|
|
53
55
|
};
|
|
54
56
|
/** AI-generated remediation advice */
|
|
55
57
|
recommendations: string[];
|
package/dist/chunk-5EFFNN6L.mjs
DELETED
|
@@ -1,145 +0,0 @@
|
|
|
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
|
-
};
|
package/dist/chunk-BBGJNBVI.mjs
DELETED
|
@@ -1,189 +0,0 @@
|
|
|
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
|
-
};
|