@aiready/doc-drift 0.1.6 → 0.1.10

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.6 build /Users/pengcao/projects/aiready/packages/doc-drift
3
+ > @aiready/doc-drift@0.1.10 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
- [10:18:49 PM]  WARN  ▲ [WARNING] The condition "types" here will never be used as it comes after both "import" and "require" [package.json]
13
+ [1:59:55 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
- [10:18:49 PM]  WARN  ▲ [WARNING] The condition "types" here will never be used as it comes after both "import" and "require" [package.json]
34
+ [1:59:55 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"
@@ -52,14 +52,14 @@
52
52
 
53
53
 
54
54
  ESM dist/cli.mjs 1.39 KB
55
- ESM dist/chunk-BBGJNBVI.mjs 5.95 KB
55
+ ESM dist/chunk-FMK4O4O7.mjs 4.79 KB
56
56
  ESM dist/index.mjs 88.00 B
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
57
+ ESM ⚡️ Build success in 163ms
58
+ CJS dist/index.js 5.50 KB
59
+ CJS dist/cli.js 7.52 KB
60
+ CJS ⚡️ Build success in 163ms
61
61
  DTS Build start
62
- DTS ⚡️ Build success in 4363ms
62
+ DTS ⚡️ Build success in 4161ms
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
@@ -0,0 +1,5 @@
1
+
2
+ 
3
+ > @aiready/doc-drift@0.1.6 lint /Users/pengcao/projects/aiready/packages/doc-drift
4
+ > eslint src --ext .ts
5
+
@@ -1,16 +1,17 @@
1
1
 
2
2
  
3
- > @aiready/doc-drift@0.1.6 test /Users/pengcao/projects/aiready/packages/doc-drift
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
   RUN  v4.0.18 /Users/pengcao/projects/aiready/packages/doc-drift
8
8
 
9
- ✓ src/__tests__/analyzer.test.ts (1 test) 19ms
9
+ ✓ src/__tests__/analyzer.test.ts (1 test) 364ms
10
+ ✓ detects missing param documentation and uncommented complexity  361ms
10
11
 
11
12
   Test Files  1 passed (1)
12
13
   Tests  1 passed (1)
13
-  Start at  22:19:16
14
-  Duration  1.48s (transform 208ms, setup 0ms, import 1.21s, tests 19ms, environment 0ms)
14
+  Start at  00:56:24
15
+  Duration  5.43s (transform 957ms, setup 0ms, import 3.81s, tests 364ms, environment 0ms)
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,143 @@
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 files = await scanFiles(options);
19
+ const issues = [];
20
+ const staleMonths = options.staleMonths ?? 6;
21
+ const staleSeconds = staleMonths * 30 * 24 * 60 * 60;
22
+ let uncommentedExports = 0;
23
+ let totalExports = 0;
24
+ let outdatedComments = 0;
25
+ let undocumentedComplexity = 0;
26
+ const now = Math.floor(Date.now() / 1e3);
27
+ let processed = 0;
28
+ for (const file of files) {
29
+ processed++;
30
+ options.onProgress?.(processed, files.length, `doc-drift: analyzing files`);
31
+ let code;
32
+ try {
33
+ code = readFileSync(file, "utf-8");
34
+ } catch {
35
+ continue;
36
+ }
37
+ let ast;
38
+ try {
39
+ ast = parse(code, {
40
+ jsx: file.endsWith(".tsx") || file.endsWith(".jsx"),
41
+ loc: true,
42
+ comment: true
43
+ });
44
+ } catch {
45
+ continue;
46
+ }
47
+ const comments = ast.comments || [];
48
+ let fileLineStamps;
49
+ for (const node of ast.body) {
50
+ if (node.type === "ExportNamedDeclaration" || node.type === "ExportDefaultDeclaration") {
51
+ const decl = node.declaration;
52
+ if (!decl) continue;
53
+ if (decl.type === "FunctionDeclaration" || decl.type === "ClassDeclaration" || decl.type === "VariableDeclaration") {
54
+ totalExports++;
55
+ const nodeLine = node.loc.start.line;
56
+ const jsdocs = comments.filter(
57
+ (c) => c.type === "Block" && c.value.startsWith("*") && c.loc.end.line === nodeLine - 1
58
+ );
59
+ if (jsdocs.length === 0) {
60
+ uncommentedExports++;
61
+ if (decl.type === "FunctionDeclaration" && decl.body?.loc) {
62
+ const lines = decl.body.loc.end.line - decl.body.loc.start.line;
63
+ if (lines > 20) undocumentedComplexity++;
64
+ }
65
+ } else {
66
+ const jsdoc = jsdocs[0];
67
+ const jsdocText = jsdoc.value;
68
+ if (decl.type === "FunctionDeclaration") {
69
+ const params = decl.params.map((p) => p.name || p.left && p.left.name).filter(Boolean);
70
+ const paramTags = Array.from(
71
+ jsdocText.matchAll(/@param\s+(?:\{[^}]+\}\s+)?([a-zA-Z0-9_]+)/g)
72
+ ).map((m) => m[1]);
73
+ const missingParams = params.filter(
74
+ (p) => !paramTags.includes(p)
75
+ );
76
+ if (missingParams.length > 0) {
77
+ outdatedComments++;
78
+ issues.push({
79
+ type: "doc-drift",
80
+ severity: "major",
81
+ message: `JSDoc @param mismatch: function has parameters (${missingParams.join(", ")}) not documented in JSDoc.`,
82
+ location: { file, line: nodeLine }
83
+ });
84
+ continue;
85
+ }
86
+ }
87
+ if (!fileLineStamps) {
88
+ fileLineStamps = getFileCommitTimestamps(file);
89
+ }
90
+ const commentModified = getLineRangeLastModifiedCached(
91
+ fileLineStamps,
92
+ jsdoc.loc.start.line,
93
+ jsdoc.loc.end.line
94
+ );
95
+ const bodyModified = getLineRangeLastModifiedCached(
96
+ fileLineStamps,
97
+ decl.loc.start.line,
98
+ decl.loc.end.line
99
+ );
100
+ if (commentModified > 0 && bodyModified > 0) {
101
+ if (now - commentModified > staleSeconds && bodyModified - commentModified > staleSeconds / 2) {
102
+ outdatedComments++;
103
+ issues.push({
104
+ type: "doc-drift",
105
+ severity: "minor",
106
+ message: `JSDoc is significantly older than the function body implementation. Code may have drifted.`,
107
+ location: { file, line: jsdoc.loc.start.line }
108
+ });
109
+ }
110
+ }
111
+ }
112
+ }
113
+ }
114
+ }
115
+ }
116
+ const riskResult = calculateDocDrift({
117
+ uncommentedExports,
118
+ totalExports,
119
+ outdatedComments,
120
+ undocumentedComplexity
121
+ });
122
+ return {
123
+ summary: {
124
+ filesAnalyzed: files.length,
125
+ functionsAnalyzed: totalExports,
126
+ score: riskResult.score,
127
+ rating: riskResult.rating
128
+ },
129
+ issues,
130
+ rawData: {
131
+ uncommentedExports,
132
+ totalExports,
133
+ outdatedComments,
134
+ undocumentedComplexity
135
+ },
136
+ recommendations: riskResult.recommendations
137
+ };
138
+ }
139
+
140
+ export {
141
+ __require,
142
+ analyzeDocDrift
143
+ };
@@ -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,21 @@ 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
- const rootDir = options.rootDir;
101
- const files = collectFiles(rootDir, options);
43
+ const files = await (0, import_core.scanFiles)(options);
44
+ const issues = [];
102
45
  const staleMonths = options.staleMonths ?? 6;
103
46
  const staleSeconds = staleMonths * 30 * 24 * 60 * 60;
104
47
  let uncommentedExports = 0;
105
48
  let totalExports = 0;
106
49
  let outdatedComments = 0;
107
50
  let undocumentedComplexity = 0;
108
- const issues = [];
109
51
  const now = Math.floor(Date.now() / 1e3);
52
+ let processed = 0;
110
53
  for (const file of files) {
54
+ processed++;
55
+ options.onProgress?.(processed, files.length, `doc-drift: analyzing files`);
111
56
  let code;
112
57
  try {
113
58
  code = (0, import_fs.readFileSync)(file, "utf-8");
@@ -125,6 +70,7 @@ async function analyzeDocDrift(options) {
125
70
  continue;
126
71
  }
127
72
  const comments = ast.comments || [];
73
+ let fileLineStamps;
128
74
  for (const node of ast.body) {
129
75
  if (node.type === "ExportNamedDeclaration" || node.type === "ExportDefaultDeclaration") {
130
76
  const decl = node.declaration;
@@ -163,13 +109,16 @@ async function analyzeDocDrift(options) {
163
109
  continue;
164
110
  }
165
111
  }
166
- const commentModified = getLineRangeLastModified(
167
- file,
112
+ if (!fileLineStamps) {
113
+ fileLineStamps = (0, import_core.getFileCommitTimestamps)(file);
114
+ }
115
+ const commentModified = (0, import_core.getLineRangeLastModifiedCached)(
116
+ fileLineStamps,
168
117
  jsdoc.loc.start.line,
169
118
  jsdoc.loc.end.line
170
119
  );
171
- const bodyModified = getLineRangeLastModified(
172
- file,
120
+ const bodyModified = (0, import_core.getLineRangeLastModifiedCached)(
121
+ fileLineStamps,
173
122
  decl.loc.start.line,
174
123
  decl.loc.end.line
175
124
  );
package/dist/cli.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  __require,
3
3
  analyzeDocDrift
4
- } from "./chunk-BBGJNBVI.mjs";
4
+ } from "./chunk-FMK4O4O7.mjs";
5
5
 
6
6
  // src/cli.ts
7
7
  import { Command } from "commander";
package/dist/index.js CHANGED
@@ -27,76 +27,21 @@ 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
- const rootDir = options.rootDir;
90
- const files = collectFiles(rootDir, options);
32
+ const files = await (0, import_core.scanFiles)(options);
33
+ const issues = [];
91
34
  const staleMonths = options.staleMonths ?? 6;
92
35
  const staleSeconds = staleMonths * 30 * 24 * 60 * 60;
93
36
  let uncommentedExports = 0;
94
37
  let totalExports = 0;
95
38
  let outdatedComments = 0;
96
39
  let undocumentedComplexity = 0;
97
- const issues = [];
98
40
  const now = Math.floor(Date.now() / 1e3);
41
+ let processed = 0;
99
42
  for (const file of files) {
43
+ processed++;
44
+ options.onProgress?.(processed, files.length, `doc-drift: analyzing files`);
100
45
  let code;
101
46
  try {
102
47
  code = (0, import_fs.readFileSync)(file, "utf-8");
@@ -114,6 +59,7 @@ async function analyzeDocDrift(options) {
114
59
  continue;
115
60
  }
116
61
  const comments = ast.comments || [];
62
+ let fileLineStamps;
117
63
  for (const node of ast.body) {
118
64
  if (node.type === "ExportNamedDeclaration" || node.type === "ExportDefaultDeclaration") {
119
65
  const decl = node.declaration;
@@ -152,13 +98,16 @@ async function analyzeDocDrift(options) {
152
98
  continue;
153
99
  }
154
100
  }
155
- const commentModified = getLineRangeLastModified(
156
- file,
101
+ if (!fileLineStamps) {
102
+ fileLineStamps = (0, import_core.getFileCommitTimestamps)(file);
103
+ }
104
+ const commentModified = (0, import_core.getLineRangeLastModifiedCached)(
105
+ fileLineStamps,
157
106
  jsdoc.loc.start.line,
158
107
  jsdoc.loc.end.line
159
108
  );
160
- const bodyModified = getLineRangeLastModified(
161
- file,
109
+ const bodyModified = (0, import_core.getLineRangeLastModifiedCached)(
110
+ fileLineStamps,
162
111
  decl.loc.start.line,
163
112
  decl.loc.end.line
164
113
  );
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  analyzeDocDrift
3
- } from "./chunk-BBGJNBVI.mjs";
3
+ } from "./chunk-FMK4O4O7.mjs";
4
4
  export {
5
5
  analyzeDocDrift
6
6
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiready/doc-drift",
3
- "version": "0.1.6",
3
+ "version": "0.1.10",
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.33"
13
+ "@aiready/core": "0.9.37"
14
14
  },
15
15
  "devDependencies": {
16
16
  "@types/node": "^24.0.0",
package/src/analyzer.ts CHANGED
@@ -1,85 +1,20 @@
1
- import { calculateDocDrift } from '@aiready/core';
1
+ import {
2
+ scanFiles,
3
+ calculateDocDrift,
4
+ getFileCommitTimestamps,
5
+ getLineRangeLastModifiedCached,
6
+ } from '@aiready/core';
2
7
  import type { DocDriftOptions, DocDriftReport, DocDriftIssue } from './types';
3
- import { readdirSync, statSync, readFileSync } from 'fs';
4
- import { join, extname } from 'path';
8
+ import { readFileSync } from 'fs';
5
9
  import { parse } from '@typescript-eslint/typescript-estree';
6
10
  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
11
 
78
12
  export async function analyzeDocDrift(
79
13
  options: DocDriftOptions
80
14
  ): Promise<DocDriftReport> {
81
- const rootDir = options.rootDir;
82
- const files = collectFiles(rootDir, options);
15
+ // Use core scanFiles which respects .gitignore recursively
16
+ const files = await scanFiles(options);
17
+ const issues: DocDriftIssue[] = [];
83
18
  const staleMonths = options.staleMonths ?? 6;
84
19
  const staleSeconds = staleMonths * 30 * 24 * 60 * 60;
85
20
 
@@ -88,10 +23,13 @@ export async function analyzeDocDrift(
88
23
  let outdatedComments = 0;
89
24
  let undocumentedComplexity = 0;
90
25
 
91
- const issues: DocDriftIssue[] = [];
92
26
  const now = Math.floor(Date.now() / 1000);
93
27
 
28
+ let processed = 0;
94
29
  for (const file of files) {
30
+ processed++;
31
+ options.onProgress?.(processed, files.length, `doc-drift: analyzing files`);
32
+
95
33
  let code: string;
96
34
  try {
97
35
  code = readFileSync(file, 'utf-8');
@@ -111,6 +49,7 @@ export async function analyzeDocDrift(
111
49
  }
112
50
 
113
51
  const comments = ast.comments || [];
52
+ let fileLineStamps: Record<number, number> | undefined;
114
53
 
115
54
  for (const node of ast.body) {
116
55
  if (
@@ -174,13 +113,16 @@ export async function analyzeDocDrift(
174
113
  }
175
114
 
176
115
  // Timestamp comparison
177
- const commentModified = getLineRangeLastModified(
178
- file,
116
+ if (!fileLineStamps) {
117
+ fileLineStamps = getFileCommitTimestamps(file);
118
+ }
119
+ const commentModified = getLineRangeLastModifiedCached(
120
+ fileLineStamps,
179
121
  jsdoc.loc.start.line,
180
122
  jsdoc.loc.end.line
181
123
  );
182
- const bodyModified = getLineRangeLastModified(
183
- file,
124
+ const bodyModified = getLineRangeLastModifiedCached(
125
+ fileLineStamps,
184
126
  decl.loc.start.line,
185
127
  decl.loc.end.line
186
128
  );