@aiready/doc-drift 0.1.5 → 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.
@@ -1,6 +1,6 @@
1
1
 
2
2
  
3
- > @aiready/doc-drift@0.1.5 build /Users/pengcao/projects/aiready/packages/doc-drift
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
  CLI Building entry: src/cli.ts, src/index.ts
@@ -10,7 +10,7 @@
10
10
  CJS Build start
11
11
  ESM Build start
12
12
 
13
- [2:33:41 PM]  WARN  ▲ [WARNING] The condition "types" here will never be used as it comes after both "import" and "require" [package.json]
13
+ [10:18:49 PM]  WARN  ▲ [WARNING] The condition "types" here will never be used as it comes after both "import" and "require" [package.json]
14
14
 
15
15
  package.json:33:6:
16
16
   33 │ "types": "./dist/index.d.ts"
@@ -31,7 +31,7 @@
31
31
 
32
32
 
33
33
 
34
- [2:33:42 PM]  WARN  ▲ [WARNING] The condition "types" here will never be used as it comes after both "import" and "require" [package.json]
34
+ [10:18:49 PM]  WARN  ▲ [WARNING] The condition "types" here will never be used as it comes after both "import" and "require" [package.json]
35
35
 
36
36
  package.json:33:6:
37
37
   33 │ "types": "./dist/index.d.ts"
@@ -51,15 +51,15 @@
51
51
 
52
52
 
53
53
 
54
- CJS dist/index.js 6.50 KB
55
- CJS dist/cli.js 8.49 KB
56
- CJS ⚡️ Build success in 2383ms
57
- ESM dist/cli.mjs 1.36 KB
54
+ ESM dist/cli.mjs 1.39 KB
55
+ ESM dist/chunk-BBGJNBVI.mjs 5.95 KB
58
56
  ESM dist/index.mjs 88.00 B
59
- ESM dist/chunk-TSLAGWBV.mjs 5.71 KB
60
- ESM ⚡️ Build success in 2526ms
57
+ ESM ⚡️ Build success in 49ms
58
+ CJS dist/cli.js 8.76 KB
59
+ CJS dist/index.js 6.73 KB
60
+ CJS ⚡️ Build success in 51ms
61
61
  DTS Build start
62
- DTS ⚡️ Build success in 72201ms
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
@@ -1,16 +1,16 @@
1
1
 
2
2
  
3
- > @aiready/doc-drift@0.1.5 test /Users/pengcao/projects/aiready/packages/doc-drift
3
+ > @aiready/doc-drift@0.1.6 test /Users/pengcao/projects/aiready/packages/doc-drift
4
4
  > vitest run
5
5
 
6
+ [?25l
7
+  RUN  v4.0.18 /Users/pengcao/projects/aiready/packages/doc-drift
6
8
 
7
-  RUN  v1.6.1 /Users/pengcao/projects/aiready/packages/doc-drift
8
-
9
- ✓ src/__tests__/analyzer.test.ts  (1 test) 777ms
9
+ ✓ src/__tests__/analyzer.test.ts (1 test) 19ms
10
10
 
11
11
   Test Files  1 passed (1)
12
12
   Tests  1 passed (1)
13
-  Start at  14:38:44
14
-  Duration  33.39s (transform 5.68s, setup 0ms, collect 23.73s, tests 777ms, environment 0ms, prepare 4.26s)
13
+  Start at  22:19:16
14
+  Duration  1.48s (transform 208ms, setup 0ms, import 1.21s, tests 19ms, environment 0ms)
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 = ["node_modules", "dist", ".git", "coverage", ".turbo", "build"];
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)(`git log -1 --format=%ct -L ${startLine},${endLine}:"${file}"`, {
78
- encoding: "utf-8",
79
- stdio: ["ignore", "pipe", "ignore"]
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((c) => c.type === "Block" && c.value.startsWith("*") && c.loc.end.line === nodeLine - 1);
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(jsdocText.matchAll(/@param\s+(?:\{[^}]+\}\s+)?([a-zA-Z0-9_]+)/g)).map((m) => m[1]);
138
- const missingParams = params.filter((p) => !paramTags.includes(p));
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(file, jsdoc.loc.start.line, jsdoc.loc.end.line);
151
- const bodyModified = getLineRangeLastModified(file, decl.loc.start.line, decl.loc.end.line);
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("Scan for documentation drift (outdated comments, mismatched signatures)").option("--include <patterns...>", "File patterns to include").option("--exclude <patterns...>", "File patterns to exclude").option("--stale-months <number>", "Months before a comment is considered potentially outdated", "6").action(async (options) => {
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(`Rating: ${report.summary.rating.toUpperCase()} (Score: ${report.summary.score})`);
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-TSLAGWBV.mjs";
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("Scan for documentation drift (outdated comments, mismatched signatures)").option("--include <patterns...>", "File patterns to include").option("--exclude <patterns...>", "File patterns to exclude").option("--stale-months <number>", "Months before a comment is considered potentially outdated", "6").action(async (options) => {
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(`Rating: ${report.summary.rating.toUpperCase()} (Score: ${report.summary.score})`);
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 = ["node_modules", "dist", ".git", "coverage", ".turbo", "build"];
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)(`git log -1 --format=%ct -L ${startLine},${endLine}:"${file}"`, {
67
- encoding: "utf-8",
68
- stdio: ["ignore", "pipe", "ignore"]
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((c) => c.type === "Block" && c.value.startsWith("*") && c.loc.end.line === nodeLine - 1);
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(jsdocText.matchAll(/@param\s+(?:\{[^}]+\}\s+)?([a-zA-Z0-9_]+)/g)).map((m) => m[1]);
127
- const missingParams = params.filter((p) => !paramTags.includes(p));
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(file, jsdoc.loc.start.line, jsdoc.loc.end.line);
140
- const bodyModified = getLineRangeLastModified(file, decl.loc.start.line, decl.loc.end.line);
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
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  analyzeDocDrift
3
- } from "./chunk-TSLAGWBV.mjs";
3
+ } from "./chunk-BBGJNBVI.mjs";
4
4
  export {
5
5
  analyzeDocDrift
6
6
  };
package/package.json CHANGED
@@ -1,23 +1,23 @@
1
1
  {
2
2
  "name": "@aiready/doc-drift",
3
- "version": "0.1.5",
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": "^7.8.0",
10
- "commander": "^12.0.0",
11
- "glob": "^10.3.12",
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.32"
13
+ "@aiready/core": "0.9.33"
14
14
  },
15
15
  "devDependencies": {
16
- "@types/node": "^20.12.7",
17
- "@typescript-eslint/types": "^7.8.0",
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": "^1.6.0"
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(file1, `
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(file2, `
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 = ['node_modules', 'dist', '.git', 'coverage', '.turbo', 'build'];
11
-
12
- function collectFiles(dir: string, options: DocDriftOptions, depth = 0): string[] {
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 { entries = readdirSync(dir); } catch { return []; }
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 { stat = statSync(full); } catch { continue; }
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(file: string, startLine: number, endLine: number): number {
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(`git log -1 --format=%ct -L ${startLine},${endLine}:"${file}"`, {
39
- encoding: 'utf-8',
40
- stdio: ['ignore', 'pipe', 'ignore']
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 { code = readFileSync(file, 'utf-8'); } catch { continue; }
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 { continue; }
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 (node.type === 'ExportNamedDeclaration' || node.type === 'ExportDefaultDeclaration') {
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 (decl.type === 'FunctionDeclaration' || decl.type === 'ClassDeclaration' || decl.type === 'VariableDeclaration') {
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((c: any) => c.type === 'Block' && c.value.startsWith('*') && c.loc.end.line === nodeLine - 1);
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.map((p: any) => p.name || (p.left && p.left.name)).filter(Boolean);
111
- const paramTags = Array.from(jsdocText.matchAll(/@param\s+(?:\{[^}]+\}\s+)?([a-zA-Z0-9_]+)/g)).map((m: any) => m[1]);
112
-
113
- const missingParams = params.filter((p: string) => !paramTags.includes(p));
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(file, jsdoc.loc.start.line, jsdoc.loc.end.line);
128
- const bodyModified = getLineRangeLastModified(file, decl.loc.start.line, decl.loc.end.line);
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 (now - commentModified > staleSeconds && bodyModified - commentModified > staleSeconds / 2) {
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('Scan for documentation drift (outdated comments, mismatched signatures)')
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('--stale-months <number>', 'Months before a comment is considered potentially outdated', '6')
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(`Rating: ${report.summary.rating.toUpperCase()} (Score: ${report.summary.score})`);
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().parseAsync(process.argv).catch(err => {
34
- console.error(pc.red(err.message));
35
- process.exit(1);
36
- });
41
+ createCommand()
42
+ .parseAsync(process.argv)
43
+ .catch((err) => {
44
+ console.error(pc.red(err.message));
45
+ process.exit(1);
46
+ });
37
47
  }