@aiready/doc-drift 0.1.1

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.
@@ -0,0 +1,66 @@
1
+
2
+ 
3
+ > @aiready/doc-drift@0.1.1 build /Users/pengcao/projects/aiready/packages/doc-drift
4
+ > tsup src/index.ts src/cli.ts --format cjs,esm --dts
5
+
6
+ CLI Building entry: src/cli.ts, src/index.ts
7
+ CLI Using tsconfig: tsconfig.json
8
+ CLI tsup v8.5.1
9
+ CLI Target: es2020
10
+ CJS Build start
11
+ ESM Build start
12
+
13
+ [9:41:37 PM]  WARN  ▲ [WARNING] The condition "types" here will never be used as it comes after both "import" and "require" [package.json]
14
+
15
+ package.json:33:6:
16
+  33 │ "types": "./dist/index.d.ts"
17
+ ╵ ~~~~~~~
18
+
19
+ The "import" condition comes earlier and will be used for all "import" statements:
20
+
21
+ package.json:31:6:
22
+  31 │ "import": "./dist/index.mjs",
23
+ ╵ ~~~~~~~~
24
+
25
+ The "require" condition comes earlier and will be used for all "require" calls:
26
+
27
+ package.json:32:6:
28
+  32 │ "require": "./dist/index.js",
29
+ ╵ ~~~~~~~~~
30
+
31
+
32
+
33
+
34
+ [9:41:37 PM]  WARN  ▲ [WARNING] The condition "types" here will never be used as it comes after both "import" and "require" [package.json]
35
+
36
+ package.json:33:6:
37
+  33 │ "types": "./dist/index.d.ts"
38
+ ╵ ~~~~~~~
39
+
40
+ The "import" condition comes earlier and will be used for all "import" statements:
41
+
42
+ package.json:31:6:
43
+  31 │ "import": "./dist/index.mjs",
44
+ ╵ ~~~~~~~~
45
+
46
+ The "require" condition comes earlier and will be used for all "require" calls:
47
+
48
+ package.json:32:6:
49
+  32 │ "require": "./dist/index.js",
50
+ ╵ ~~~~~~~~~
51
+
52
+
53
+
54
+ CJS dist/cli.js 8.49 KB
55
+ CJS dist/index.js 6.50 KB
56
+ CJS ⚡️ Build success in 180ms
57
+ ESM dist/chunk-TSLAGWBV.mjs 5.71 KB
58
+ ESM dist/cli.mjs 1.36 KB
59
+ ESM dist/index.mjs 88.00 B
60
+ ESM ⚡️ Build success in 200ms
61
+ DTS Build start
62
+ DTS ⚡️ Build success in 3656ms
63
+ DTS dist/cli.d.ts 108.00 B
64
+ DTS dist/index.d.ts 950.00 B
65
+ DTS dist/cli.d.mts 108.00 B
66
+ DTS dist/index.d.mts 950.00 B
@@ -0,0 +1,16 @@
1
+
2
+ 
3
+ > @aiready/doc-drift@0.1.1 test /Users/pengcao/projects/aiready/packages/doc-drift
4
+ > vitest run
5
+
6
+
7
+  RUN  v1.6.1 /Users/pengcao/projects/aiready/packages/doc-drift
8
+
9
+ ✓ src/__tests__/analyzer.test.ts  (1 test) 19ms
10
+
11
+  Test Files  1 passed (1)
12
+  Tests  1 passed (1)
13
+  Start at  21:42:03
14
+  Duration  2.17s (transform 267ms, setup 0ms, collect 1.29s, tests 19ms, environment 0ms, prepare 239ms)
15
+
16
+ [?25h
package/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # @aiready/doc-drift
2
+
3
+ > AIReady Spoke: Tracks documentation freshness versus code churn to pinpoint outdated comments that confuse AI models.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@aiready/doc-drift.svg)](https://npmjs.com/package/@aiready/doc-drift)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ ## Overview
9
+
10
+ AI models rely heavily on inline documentation and function signatures. When code changes but comments don't, AI models often hallucinate based on the stale documentation. The **Documentation Drift** analyzer combines AST parsing with git log traversal to identify instances where comments are likely lagging behind actual implementation logic.
11
+
12
+ ## Features
13
+
14
+ - **Drift Detection**: Detects documentation older than the code it describes based on git history timestamps.
15
+ - **Signature Mismatches**: Finds missing documented `@param` tags when new arguments are added to functions.
16
+ - **Complexity Guardrails**: Identifies long or complex functions that completely lack documentation.
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ npm install -g @aiready/cli @aiready/doc-drift
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ This tool is designed to be run through the unified AIReady CLI.
27
+
28
+ ```bash
29
+ # Scan a codebase for documentation drift
30
+ aiready scan . --tools doc-drift
31
+
32
+ # Output detailed JSON report
33
+ aiready scan . --tools doc-drift --output json
34
+ ```
35
+
36
+ ## How It Works
37
+
38
+ 1. Parses your codebase into an Abstract Syntax Tree (AST).
39
+ 2. Uses `git log` to find the last modified timestamp for the code body limits vs the associated comment block.
40
+ 3. Calculates a freshness ratio. If the comment trails the code body by several months (`--stale-months`), it flags the function as having a high risk of documentary drift.
41
+
42
+ ## License
43
+
44
+ MIT
@@ -0,0 +1,165 @@
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 = ["node_modules", "dist", ".git", "coverage", ".turbo", "build"];
16
+ function collectFiles(dir, options, depth = 0) {
17
+ if (depth > 20) return [];
18
+ const excludes = [...DEFAULT_EXCLUDES, ...options.exclude ?? []];
19
+ let entries;
20
+ try {
21
+ entries = readdirSync(dir);
22
+ } catch {
23
+ return [];
24
+ }
25
+ const files = [];
26
+ for (const entry of entries) {
27
+ if (excludes.some((ex) => entry === ex || entry.includes(ex))) continue;
28
+ const full = join(dir, entry);
29
+ let stat;
30
+ try {
31
+ stat = statSync(full);
32
+ } catch {
33
+ continue;
34
+ }
35
+ if (stat.isDirectory()) {
36
+ files.push(...collectFiles(full, options, depth + 1));
37
+ } else if (stat.isFile() && SRC_EXTENSIONS.has(extname(full))) {
38
+ if (!options.include || options.include.some((p) => full.includes(p))) {
39
+ files.push(full);
40
+ }
41
+ }
42
+ }
43
+ return files;
44
+ }
45
+ function getLineRangeLastModified(file, startLine, endLine) {
46
+ try {
47
+ const output = execSync(`git log -1 --format=%ct -L ${startLine},${endLine}:"${file}"`, {
48
+ encoding: "utf-8",
49
+ stdio: ["ignore", "pipe", "ignore"]
50
+ });
51
+ const match = output.trim().split("\n")[0];
52
+ if (match && !isNaN(parseInt(match, 10))) {
53
+ return parseInt(match, 10);
54
+ }
55
+ } catch {
56
+ }
57
+ return 0;
58
+ }
59
+ async function analyzeDocDrift(options) {
60
+ const rootDir = options.rootDir;
61
+ const files = collectFiles(rootDir, options);
62
+ const staleMonths = options.staleMonths ?? 6;
63
+ const staleSeconds = staleMonths * 30 * 24 * 60 * 60;
64
+ let uncommentedExports = 0;
65
+ let totalExports = 0;
66
+ let outdatedComments = 0;
67
+ let undocumentedComplexity = 0;
68
+ const issues = [];
69
+ const now = Math.floor(Date.now() / 1e3);
70
+ for (const file of files) {
71
+ let code;
72
+ try {
73
+ code = readFileSync(file, "utf-8");
74
+ } catch {
75
+ continue;
76
+ }
77
+ let ast;
78
+ try {
79
+ ast = parse(code, {
80
+ jsx: file.endsWith(".tsx") || file.endsWith(".jsx"),
81
+ loc: true,
82
+ comment: true
83
+ });
84
+ } catch {
85
+ continue;
86
+ }
87
+ const comments = ast.comments || [];
88
+ for (const node of ast.body) {
89
+ if (node.type === "ExportNamedDeclaration" || node.type === "ExportDefaultDeclaration") {
90
+ const decl = node.declaration;
91
+ if (!decl) continue;
92
+ if (decl.type === "FunctionDeclaration" || decl.type === "ClassDeclaration" || decl.type === "VariableDeclaration") {
93
+ totalExports++;
94
+ const nodeLine = node.loc.start.line;
95
+ const jsdocs = comments.filter((c) => c.type === "Block" && c.value.startsWith("*") && c.loc.end.line === nodeLine - 1);
96
+ if (jsdocs.length === 0) {
97
+ uncommentedExports++;
98
+ if (decl.type === "FunctionDeclaration" && decl.body?.loc) {
99
+ const lines = decl.body.loc.end.line - decl.body.loc.start.line;
100
+ if (lines > 20) undocumentedComplexity++;
101
+ }
102
+ } else {
103
+ const jsdoc = jsdocs[0];
104
+ const jsdocText = jsdoc.value;
105
+ if (decl.type === "FunctionDeclaration") {
106
+ const params = decl.params.map((p) => p.name || p.left && p.left.name).filter(Boolean);
107
+ const paramTags = Array.from(jsdocText.matchAll(/@param\s+(?:\{[^}]+\}\s+)?([a-zA-Z0-9_]+)/g)).map((m) => m[1]);
108
+ const missingParams = params.filter((p) => !paramTags.includes(p));
109
+ if (missingParams.length > 0) {
110
+ outdatedComments++;
111
+ issues.push({
112
+ type: "doc-drift",
113
+ severity: "major",
114
+ message: `JSDoc @param mismatch: function has parameters (${missingParams.join(", ")}) not documented in JSDoc.`,
115
+ location: { file, line: nodeLine }
116
+ });
117
+ continue;
118
+ }
119
+ }
120
+ const commentModified = getLineRangeLastModified(file, jsdoc.loc.start.line, jsdoc.loc.end.line);
121
+ const bodyModified = getLineRangeLastModified(file, decl.loc.start.line, decl.loc.end.line);
122
+ if (commentModified > 0 && bodyModified > 0) {
123
+ if (now - commentModified > staleSeconds && bodyModified - commentModified > staleSeconds / 2) {
124
+ outdatedComments++;
125
+ issues.push({
126
+ type: "doc-drift",
127
+ severity: "minor",
128
+ message: `JSDoc is significantly older than the function body implementation. Code may have drifted.`,
129
+ location: { file, line: jsdoc.loc.start.line }
130
+ });
131
+ }
132
+ }
133
+ }
134
+ }
135
+ }
136
+ }
137
+ }
138
+ const riskResult = calculateDocDrift({
139
+ uncommentedExports,
140
+ totalExports,
141
+ outdatedComments,
142
+ undocumentedComplexity
143
+ });
144
+ return {
145
+ summary: {
146
+ filesAnalyzed: files.length,
147
+ functionsAnalyzed: totalExports,
148
+ score: riskResult.score,
149
+ rating: riskResult.rating
150
+ },
151
+ issues,
152
+ rawData: {
153
+ uncommentedExports,
154
+ totalExports,
155
+ outdatedComments,
156
+ undocumentedComplexity
157
+ },
158
+ recommendations: riskResult.recommendations
159
+ };
160
+ }
161
+
162
+ export {
163
+ __require,
164
+ analyzeDocDrift
165
+ };
package/dist/cli.d.mts ADDED
@@ -0,0 +1,5 @@
1
+ import { Command } from 'commander';
2
+
3
+ declare function createCommand(): Command;
4
+
5
+ export { createCommand };
package/dist/cli.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ import { Command } from 'commander';
2
+
3
+ declare function createCommand(): Command;
4
+
5
+ export { createCommand };
package/dist/cli.js ADDED
@@ -0,0 +1,223 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/cli.ts
31
+ var cli_exports = {};
32
+ __export(cli_exports, {
33
+ createCommand: () => createCommand
34
+ });
35
+ module.exports = __toCommonJS(cli_exports);
36
+ var import_commander = require("commander");
37
+
38
+ // src/analyzer.ts
39
+ var import_core = require("@aiready/core");
40
+ var import_fs = require("fs");
41
+ var import_path = require("path");
42
+ 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 = ["node_modules", "dist", ".git", "coverage", ".turbo", "build"];
46
+ function collectFiles(dir, options, depth = 0) {
47
+ if (depth > 20) return [];
48
+ const excludes = [...DEFAULT_EXCLUDES, ...options.exclude ?? []];
49
+ let entries;
50
+ try {
51
+ entries = (0, import_fs.readdirSync)(dir);
52
+ } catch {
53
+ return [];
54
+ }
55
+ const files = [];
56
+ for (const entry of entries) {
57
+ if (excludes.some((ex) => entry === ex || entry.includes(ex))) continue;
58
+ const full = (0, import_path.join)(dir, entry);
59
+ let stat;
60
+ try {
61
+ stat = (0, import_fs.statSync)(full);
62
+ } catch {
63
+ continue;
64
+ }
65
+ if (stat.isDirectory()) {
66
+ files.push(...collectFiles(full, options, depth + 1));
67
+ } else if (stat.isFile() && SRC_EXTENSIONS.has((0, import_path.extname)(full))) {
68
+ if (!options.include || options.include.some((p) => full.includes(p))) {
69
+ files.push(full);
70
+ }
71
+ }
72
+ }
73
+ return files;
74
+ }
75
+ function getLineRangeLastModified(file, startLine, endLine) {
76
+ 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
+ });
81
+ const match = output.trim().split("\n")[0];
82
+ if (match && !isNaN(parseInt(match, 10))) {
83
+ return parseInt(match, 10);
84
+ }
85
+ } catch {
86
+ }
87
+ return 0;
88
+ }
89
+ async function analyzeDocDrift(options) {
90
+ const rootDir = options.rootDir;
91
+ const files = collectFiles(rootDir, options);
92
+ const staleMonths = options.staleMonths ?? 6;
93
+ const staleSeconds = staleMonths * 30 * 24 * 60 * 60;
94
+ let uncommentedExports = 0;
95
+ let totalExports = 0;
96
+ let outdatedComments = 0;
97
+ let undocumentedComplexity = 0;
98
+ const issues = [];
99
+ const now = Math.floor(Date.now() / 1e3);
100
+ for (const file of files) {
101
+ let code;
102
+ try {
103
+ code = (0, import_fs.readFileSync)(file, "utf-8");
104
+ } catch {
105
+ continue;
106
+ }
107
+ let ast;
108
+ try {
109
+ ast = (0, import_typescript_estree.parse)(code, {
110
+ jsx: file.endsWith(".tsx") || file.endsWith(".jsx"),
111
+ loc: true,
112
+ comment: true
113
+ });
114
+ } catch {
115
+ continue;
116
+ }
117
+ const comments = ast.comments || [];
118
+ for (const node of ast.body) {
119
+ if (node.type === "ExportNamedDeclaration" || node.type === "ExportDefaultDeclaration") {
120
+ const decl = node.declaration;
121
+ if (!decl) continue;
122
+ if (decl.type === "FunctionDeclaration" || decl.type === "ClassDeclaration" || decl.type === "VariableDeclaration") {
123
+ totalExports++;
124
+ const nodeLine = node.loc.start.line;
125
+ const jsdocs = comments.filter((c) => c.type === "Block" && c.value.startsWith("*") && c.loc.end.line === nodeLine - 1);
126
+ if (jsdocs.length === 0) {
127
+ uncommentedExports++;
128
+ if (decl.type === "FunctionDeclaration" && decl.body?.loc) {
129
+ const lines = decl.body.loc.end.line - decl.body.loc.start.line;
130
+ if (lines > 20) undocumentedComplexity++;
131
+ }
132
+ } else {
133
+ const jsdoc = jsdocs[0];
134
+ const jsdocText = jsdoc.value;
135
+ if (decl.type === "FunctionDeclaration") {
136
+ 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));
139
+ if (missingParams.length > 0) {
140
+ outdatedComments++;
141
+ issues.push({
142
+ type: "doc-drift",
143
+ severity: "major",
144
+ message: `JSDoc @param mismatch: function has parameters (${missingParams.join(", ")}) not documented in JSDoc.`,
145
+ location: { file, line: nodeLine }
146
+ });
147
+ continue;
148
+ }
149
+ }
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);
152
+ if (commentModified > 0 && bodyModified > 0) {
153
+ if (now - commentModified > staleSeconds && bodyModified - commentModified > staleSeconds / 2) {
154
+ outdatedComments++;
155
+ issues.push({
156
+ type: "doc-drift",
157
+ severity: "minor",
158
+ message: `JSDoc is significantly older than the function body implementation. Code may have drifted.`,
159
+ location: { file, line: jsdoc.loc.start.line }
160
+ });
161
+ }
162
+ }
163
+ }
164
+ }
165
+ }
166
+ }
167
+ }
168
+ const riskResult = (0, import_core.calculateDocDrift)({
169
+ uncommentedExports,
170
+ totalExports,
171
+ outdatedComments,
172
+ undocumentedComplexity
173
+ });
174
+ return {
175
+ summary: {
176
+ filesAnalyzed: files.length,
177
+ functionsAnalyzed: totalExports,
178
+ score: riskResult.score,
179
+ rating: riskResult.rating
180
+ },
181
+ issues,
182
+ rawData: {
183
+ uncommentedExports,
184
+ totalExports,
185
+ outdatedComments,
186
+ undocumentedComplexity
187
+ },
188
+ recommendations: riskResult.recommendations
189
+ };
190
+ }
191
+
192
+ // src/cli.ts
193
+ var import_picocolors = __toESM(require("picocolors"));
194
+ 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) => {
196
+ console.log(import_picocolors.default.cyan("Analyzing documentation drift..."));
197
+ const report = await analyzeDocDrift({
198
+ rootDir: process.cwd(),
199
+ include: options.include,
200
+ exclude: options.exclude,
201
+ staleMonths: parseInt(options.staleMonths, 10)
202
+ });
203
+ console.log(import_picocolors.default.bold("Doc Drift Analysis Results:"));
204
+ console.log(`Rating: ${report.summary.rating.toUpperCase()} (Score: ${report.summary.score})`);
205
+ if (report.issues.length > 0) {
206
+ console.log(import_picocolors.default.red(`
207
+ Found ${report.issues.length} drift issues.`));
208
+ } else {
209
+ console.log(import_picocolors.default.green("\nNo documentation drift detected."));
210
+ }
211
+ });
212
+ return program;
213
+ }
214
+ if (require.main === module) {
215
+ createCommand().parseAsync(process.argv).catch((err) => {
216
+ console.error(import_picocolors.default.red(err.message));
217
+ process.exit(1);
218
+ });
219
+ }
220
+ // Annotate the CommonJS export names for ESM import in node:
221
+ 0 && (module.exports = {
222
+ createCommand
223
+ });
package/dist/cli.mjs ADDED
@@ -0,0 +1,37 @@
1
+ import {
2
+ __require,
3
+ analyzeDocDrift
4
+ } from "./chunk-TSLAGWBV.mjs";
5
+
6
+ // src/cli.ts
7
+ import { Command } from "commander";
8
+ import pc from "picocolors";
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) => {
11
+ console.log(pc.cyan("Analyzing documentation drift..."));
12
+ const report = await analyzeDocDrift({
13
+ rootDir: process.cwd(),
14
+ include: options.include,
15
+ exclude: options.exclude,
16
+ staleMonths: parseInt(options.staleMonths, 10)
17
+ });
18
+ console.log(pc.bold("Doc Drift Analysis Results:"));
19
+ console.log(`Rating: ${report.summary.rating.toUpperCase()} (Score: ${report.summary.score})`);
20
+ if (report.issues.length > 0) {
21
+ console.log(pc.red(`
22
+ Found ${report.issues.length} drift issues.`));
23
+ } else {
24
+ console.log(pc.green("\nNo documentation drift detected."));
25
+ }
26
+ });
27
+ return program;
28
+ }
29
+ if (__require.main === module) {
30
+ createCommand().parseAsync(process.argv).catch((err) => {
31
+ console.error(pc.red(err.message));
32
+ process.exit(1);
33
+ });
34
+ }
35
+ export {
36
+ createCommand
37
+ };
@@ -0,0 +1,31 @@
1
+ import { Issue, ScanOptions } from '@aiready/core';
2
+
3
+ interface DocDriftOptions extends ScanOptions {
4
+ /** Maximum commit distance to check for drift */
5
+ maxCommits?: number;
6
+ /** Consider comments older than this many months as outdated */
7
+ staleMonths?: number;
8
+ }
9
+ interface DocDriftIssue extends Issue {
10
+ type: 'doc-drift';
11
+ }
12
+ interface DocDriftReport {
13
+ summary: {
14
+ filesAnalyzed: number;
15
+ functionsAnalyzed: number;
16
+ score: number;
17
+ rating: 'minimal' | 'low' | 'moderate' | 'high' | 'severe';
18
+ };
19
+ issues: DocDriftIssue[];
20
+ rawData: {
21
+ uncommentedExports: number;
22
+ totalExports: number;
23
+ outdatedComments: number;
24
+ undocumentedComplexity: number;
25
+ };
26
+ recommendations: string[];
27
+ }
28
+
29
+ declare function analyzeDocDrift(options: DocDriftOptions): Promise<DocDriftReport>;
30
+
31
+ export { type DocDriftIssue, type DocDriftOptions, type DocDriftReport, analyzeDocDrift };
@@ -0,0 +1,31 @@
1
+ import { Issue, ScanOptions } from '@aiready/core';
2
+
3
+ interface DocDriftOptions extends ScanOptions {
4
+ /** Maximum commit distance to check for drift */
5
+ maxCommits?: number;
6
+ /** Consider comments older than this many months as outdated */
7
+ staleMonths?: number;
8
+ }
9
+ interface DocDriftIssue extends Issue {
10
+ type: 'doc-drift';
11
+ }
12
+ interface DocDriftReport {
13
+ summary: {
14
+ filesAnalyzed: number;
15
+ functionsAnalyzed: number;
16
+ score: number;
17
+ rating: 'minimal' | 'low' | 'moderate' | 'high' | 'severe';
18
+ };
19
+ issues: DocDriftIssue[];
20
+ rawData: {
21
+ uncommentedExports: number;
22
+ totalExports: number;
23
+ outdatedComments: number;
24
+ undocumentedComplexity: number;
25
+ };
26
+ recommendations: string[];
27
+ }
28
+
29
+ declare function analyzeDocDrift(options: DocDriftOptions): Promise<DocDriftReport>;
30
+
31
+ export { type DocDriftIssue, type DocDriftOptions, type DocDriftReport, analyzeDocDrift };
package/dist/index.js ADDED
@@ -0,0 +1,183 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ analyzeDocDrift: () => analyzeDocDrift
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+
27
+ // src/analyzer.ts
28
+ var import_core = require("@aiready/core");
29
+ var import_fs = require("fs");
30
+ var import_path = require("path");
31
+ 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 = ["node_modules", "dist", ".git", "coverage", ".turbo", "build"];
35
+ function collectFiles(dir, options, depth = 0) {
36
+ if (depth > 20) return [];
37
+ const excludes = [...DEFAULT_EXCLUDES, ...options.exclude ?? []];
38
+ let entries;
39
+ try {
40
+ entries = (0, import_fs.readdirSync)(dir);
41
+ } catch {
42
+ return [];
43
+ }
44
+ const files = [];
45
+ for (const entry of entries) {
46
+ if (excludes.some((ex) => entry === ex || entry.includes(ex))) continue;
47
+ const full = (0, import_path.join)(dir, entry);
48
+ let stat;
49
+ try {
50
+ stat = (0, import_fs.statSync)(full);
51
+ } catch {
52
+ continue;
53
+ }
54
+ if (stat.isDirectory()) {
55
+ files.push(...collectFiles(full, options, depth + 1));
56
+ } else if (stat.isFile() && SRC_EXTENSIONS.has((0, import_path.extname)(full))) {
57
+ if (!options.include || options.include.some((p) => full.includes(p))) {
58
+ files.push(full);
59
+ }
60
+ }
61
+ }
62
+ return files;
63
+ }
64
+ function getLineRangeLastModified(file, startLine, endLine) {
65
+ 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
+ });
70
+ const match = output.trim().split("\n")[0];
71
+ if (match && !isNaN(parseInt(match, 10))) {
72
+ return parseInt(match, 10);
73
+ }
74
+ } catch {
75
+ }
76
+ return 0;
77
+ }
78
+ async function analyzeDocDrift(options) {
79
+ const rootDir = options.rootDir;
80
+ const files = collectFiles(rootDir, options);
81
+ const staleMonths = options.staleMonths ?? 6;
82
+ const staleSeconds = staleMonths * 30 * 24 * 60 * 60;
83
+ let uncommentedExports = 0;
84
+ let totalExports = 0;
85
+ let outdatedComments = 0;
86
+ let undocumentedComplexity = 0;
87
+ const issues = [];
88
+ const now = Math.floor(Date.now() / 1e3);
89
+ for (const file of files) {
90
+ let code;
91
+ try {
92
+ code = (0, import_fs.readFileSync)(file, "utf-8");
93
+ } catch {
94
+ continue;
95
+ }
96
+ let ast;
97
+ try {
98
+ ast = (0, import_typescript_estree.parse)(code, {
99
+ jsx: file.endsWith(".tsx") || file.endsWith(".jsx"),
100
+ loc: true,
101
+ comment: true
102
+ });
103
+ } catch {
104
+ continue;
105
+ }
106
+ const comments = ast.comments || [];
107
+ for (const node of ast.body) {
108
+ if (node.type === "ExportNamedDeclaration" || node.type === "ExportDefaultDeclaration") {
109
+ const decl = node.declaration;
110
+ if (!decl) continue;
111
+ if (decl.type === "FunctionDeclaration" || decl.type === "ClassDeclaration" || decl.type === "VariableDeclaration") {
112
+ totalExports++;
113
+ const nodeLine = node.loc.start.line;
114
+ const jsdocs = comments.filter((c) => c.type === "Block" && c.value.startsWith("*") && c.loc.end.line === nodeLine - 1);
115
+ if (jsdocs.length === 0) {
116
+ uncommentedExports++;
117
+ if (decl.type === "FunctionDeclaration" && decl.body?.loc) {
118
+ const lines = decl.body.loc.end.line - decl.body.loc.start.line;
119
+ if (lines > 20) undocumentedComplexity++;
120
+ }
121
+ } else {
122
+ const jsdoc = jsdocs[0];
123
+ const jsdocText = jsdoc.value;
124
+ if (decl.type === "FunctionDeclaration") {
125
+ 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));
128
+ if (missingParams.length > 0) {
129
+ outdatedComments++;
130
+ issues.push({
131
+ type: "doc-drift",
132
+ severity: "major",
133
+ message: `JSDoc @param mismatch: function has parameters (${missingParams.join(", ")}) not documented in JSDoc.`,
134
+ location: { file, line: nodeLine }
135
+ });
136
+ continue;
137
+ }
138
+ }
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);
141
+ if (commentModified > 0 && bodyModified > 0) {
142
+ if (now - commentModified > staleSeconds && bodyModified - commentModified > staleSeconds / 2) {
143
+ outdatedComments++;
144
+ issues.push({
145
+ type: "doc-drift",
146
+ severity: "minor",
147
+ message: `JSDoc is significantly older than the function body implementation. Code may have drifted.`,
148
+ location: { file, line: jsdoc.loc.start.line }
149
+ });
150
+ }
151
+ }
152
+ }
153
+ }
154
+ }
155
+ }
156
+ }
157
+ const riskResult = (0, import_core.calculateDocDrift)({
158
+ uncommentedExports,
159
+ totalExports,
160
+ outdatedComments,
161
+ undocumentedComplexity
162
+ });
163
+ return {
164
+ summary: {
165
+ filesAnalyzed: files.length,
166
+ functionsAnalyzed: totalExports,
167
+ score: riskResult.score,
168
+ rating: riskResult.rating
169
+ },
170
+ issues,
171
+ rawData: {
172
+ uncommentedExports,
173
+ totalExports,
174
+ outdatedComments,
175
+ undocumentedComplexity
176
+ },
177
+ recommendations: riskResult.recommendations
178
+ };
179
+ }
180
+ // Annotate the CommonJS export names for ESM import in node:
181
+ 0 && (module.exports = {
182
+ analyzeDocDrift
183
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,6 @@
1
+ import {
2
+ analyzeDocDrift
3
+ } from "./chunk-TSLAGWBV.mjs";
4
+ export {
5
+ analyzeDocDrift
6
+ };
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@aiready/doc-drift",
3
+ "version": "0.1.1",
4
+ "description": "AI-Readiness: Documentation Drift Detection",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "dependencies": {
9
+ "@typescript-eslint/typescript-estree": "^7.8.0",
10
+ "commander": "^12.0.0",
11
+ "glob": "^10.3.12",
12
+ "picocolors": "^1.0.0",
13
+ "@aiready/core": "0.9.28"
14
+ },
15
+ "devDependencies": {
16
+ "@types/node": "^20.12.7",
17
+ "@typescript-eslint/types": "^7.8.0",
18
+ "tsup": "^8.0.2",
19
+ "typescript": "^5.4.5",
20
+ "vitest": "^1.6.0"
21
+ },
22
+ "exports": {
23
+ ".": {
24
+ "import": "./dist/index.mjs",
25
+ "require": "./dist/index.js",
26
+ "types": "./dist/index.d.ts"
27
+ }
28
+ },
29
+ "scripts": {
30
+ "build": "tsup src/index.ts src/cli.ts --format cjs,esm --dts",
31
+ "dev": "tsup src/index.ts src/cli.ts --format cjs,esm --watch",
32
+ "test": "vitest run",
33
+ "test:watch": "vitest",
34
+ "lint": "eslint src --ext .ts"
35
+ }
36
+ }
@@ -0,0 +1,77 @@
1
+ import { analyzeDocDrift } from '../analyzer';
2
+ import { join } from 'path';
3
+ import { writeFileSync, mkdirSync, rmSync } from 'fs';
4
+ import { tmpdir } from 'os';
5
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
6
+
7
+ describe('Doc Drift Analyzer', () => {
8
+ let tmpDir: string;
9
+
10
+ beforeAll(() => {
11
+ tmpDir = join(tmpdir(), `doc-drift-test-${Date.now()}`);
12
+ mkdirSync(tmpDir, { recursive: true });
13
+
14
+ // File with signature mismatch
15
+ const file1 = join(tmpDir, 'file1.ts');
16
+ writeFileSync(file1, `
17
+ /**
18
+ * Adds numbers.
19
+ * @param a First number
20
+ */
21
+ export function add(a: number, b: number) {
22
+ return a + b;
23
+ }
24
+ `);
25
+
26
+ // File with undocumented complexity (simulated by lines > 20)
27
+ const file2 = join(tmpDir, 'file2.ts');
28
+ writeFileSync(file2, `
29
+ export function complexFunction(data: any) {
30
+ let result = 0;
31
+ for (let i = 0; i < 10; i++) {
32
+ if (data && data.includes(i)) result += i;
33
+ }
34
+ for (let j = 0; j < 15; j++) {
35
+ result -= j;
36
+ }
37
+ for (let j = 0; j < 15; j++) {
38
+ result -= j;
39
+ }
40
+ for (let j = 0; j < 15; j++) {
41
+ result -= j;
42
+ }
43
+ for (let j = 0; j < 15; j++) {
44
+ result -= j;
45
+ }
46
+ for (let j = 0; j < 15; j++) {
47
+ result -= j;
48
+ }
49
+ for (let k = 0; k < 5; k++) {
50
+ result += k;
51
+ }
52
+ return result;
53
+ }
54
+ `);
55
+ });
56
+
57
+ afterAll(() => {
58
+ rmSync(tmpDir, { recursive: true, force: true });
59
+ });
60
+
61
+ it('detects missing param documentation and uncommented complexity', async () => {
62
+ const report = await analyzeDocDrift({
63
+ rootDir: tmpDir,
64
+ });
65
+
66
+ // totalExports = 2 (add, complexFunction)
67
+ expect(report.summary.functionsAnalyzed).toBe(2);
68
+
69
+ // "add" has a JSDoc, but missing "b" param. "complexFunction" has NO JSDoc.
70
+ expect(report.rawData.uncommentedExports).toBe(1);
71
+ expect(report.rawData.outdatedComments).toBe(1);
72
+ expect(report.rawData.undocumentedComplexity).toBe(1);
73
+
74
+ expect(report.issues.length).toBeGreaterThan(0);
75
+ expect(report.issues.some(i => i.message.includes('b'))).toBe(true);
76
+ });
77
+ });
@@ -0,0 +1,171 @@
1
+ import { calculateDocDrift } from '@aiready/core';
2
+ import type { DocDriftOptions, DocDriftReport, DocDriftIssue } from './types';
3
+ import { readdirSync, statSync, readFileSync } from 'fs';
4
+ import { join, extname } from 'path';
5
+ import { parse } from '@typescript-eslint/typescript-estree';
6
+ 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 = ['node_modules', 'dist', '.git', 'coverage', '.turbo', 'build'];
11
+
12
+ function collectFiles(dir: string, options: DocDriftOptions, depth = 0): string[] {
13
+ if (depth > 20) return [];
14
+ const excludes = [...DEFAULT_EXCLUDES, ...(options.exclude ?? [])];
15
+ let entries: string[];
16
+ try { entries = readdirSync(dir); } catch { return []; }
17
+
18
+ const files: string[] = [];
19
+ for (const entry of entries) {
20
+ if (excludes.some(ex => entry === ex || entry.includes(ex))) continue;
21
+ const full = join(dir, entry);
22
+ let stat;
23
+ try { stat = statSync(full); } catch { continue; }
24
+ if (stat.isDirectory()) {
25
+ files.push(...collectFiles(full, options, depth + 1));
26
+ } else if (stat.isFile() && SRC_EXTENSIONS.has(extname(full))) {
27
+ if (!options.include || options.include.some(p => full.includes(p))) {
28
+ files.push(full);
29
+ }
30
+ }
31
+ }
32
+ return files;
33
+ }
34
+
35
+ function getLineRangeLastModified(file: string, startLine: number, endLine: number): number {
36
+ try {
37
+ // 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
+ });
42
+ const match = output.trim().split('\n')[0];
43
+ if (match && !isNaN(parseInt(match, 10))) {
44
+ return parseInt(match, 10);
45
+ }
46
+ } catch {
47
+ // Ignore errors (file untracked, new file, etc)
48
+ }
49
+ return 0; // Unknown or not committed
50
+ }
51
+
52
+ export async function analyzeDocDrift(
53
+ options: DocDriftOptions,
54
+ ): Promise<DocDriftReport> {
55
+ const rootDir = options.rootDir;
56
+ const files = collectFiles(rootDir, options);
57
+ const staleMonths = options.staleMonths ?? 6;
58
+ const staleSeconds = staleMonths * 30 * 24 * 60 * 60;
59
+
60
+ let uncommentedExports = 0;
61
+ let totalExports = 0;
62
+ let outdatedComments = 0;
63
+ let undocumentedComplexity = 0;
64
+
65
+ const issues: DocDriftIssue[] = [];
66
+ const now = Math.floor(Date.now() / 1000);
67
+
68
+ for (const file of files) {
69
+ let code: string;
70
+ try { code = readFileSync(file, 'utf-8'); } catch { continue; }
71
+
72
+ let ast: TSESTree.Program;
73
+ try {
74
+ ast = parse(code, {
75
+ jsx: file.endsWith('.tsx') || file.endsWith('.jsx'),
76
+ loc: true,
77
+ comment: true,
78
+ });
79
+ } catch { continue; }
80
+
81
+ const comments = ast.comments || [];
82
+
83
+ for (const node of ast.body) {
84
+ if (node.type === 'ExportNamedDeclaration' || node.type === 'ExportDefaultDeclaration') {
85
+ const decl = (node as any).declaration;
86
+ if (!decl) continue;
87
+
88
+ // Count exports
89
+ if (decl.type === 'FunctionDeclaration' || decl.type === 'ClassDeclaration' || decl.type === 'VariableDeclaration') {
90
+ totalExports++;
91
+
92
+ // Find associated JSDoc comment (immediately preceding the export)
93
+ 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);
95
+
96
+ if (jsdocs.length === 0) {
97
+ uncommentedExports++;
98
+
99
+ // Check for undocumented complexity (e.g., function body > 20 lines)
100
+ if (decl.type === 'FunctionDeclaration' && decl.body?.loc) {
101
+ const lines = decl.body.loc.end.line - decl.body.loc.start.line;
102
+ if (lines > 20) undocumentedComplexity++;
103
+ }
104
+ } else {
105
+ const jsdoc = jsdocs[0];
106
+ const jsdocText = jsdoc.value;
107
+
108
+ // Signature mismatch detection
109
+ 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));
114
+ if (missingParams.length > 0) {
115
+ outdatedComments++;
116
+ issues.push({
117
+ type: 'doc-drift',
118
+ severity: 'major',
119
+ message: `JSDoc @param mismatch: function has parameters (${missingParams.join(', ')}) not documented in JSDoc.`,
120
+ location: { file, line: nodeLine }
121
+ });
122
+ continue; // already counted as outdated
123
+ }
124
+ }
125
+
126
+ // 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);
129
+
130
+ if (commentModified > 0 && bodyModified > 0) {
131
+ // If body was modified much later than the comment, and comment is older than staleMonths
132
+ if (now - commentModified > staleSeconds && bodyModified - commentModified > staleSeconds / 2) {
133
+ outdatedComments++;
134
+ issues.push({
135
+ type: 'doc-drift',
136
+ 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
+ });
140
+ }
141
+ }
142
+ }
143
+ }
144
+ }
145
+ }
146
+ }
147
+
148
+ const riskResult = calculateDocDrift({
149
+ uncommentedExports,
150
+ totalExports,
151
+ outdatedComments,
152
+ undocumentedComplexity
153
+ });
154
+
155
+ return {
156
+ summary: {
157
+ filesAnalyzed: files.length,
158
+ functionsAnalyzed: totalExports,
159
+ score: riskResult.score,
160
+ rating: riskResult.rating,
161
+ },
162
+ issues,
163
+ rawData: {
164
+ uncommentedExports,
165
+ totalExports,
166
+ outdatedComments,
167
+ undocumentedComplexity,
168
+ },
169
+ recommendations: riskResult.recommendations,
170
+ };
171
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,37 @@
1
+ import { Command } from 'commander';
2
+ import { analyzeDocDrift } from './analyzer';
3
+ import pc from 'picocolors';
4
+
5
+ export function createCommand() {
6
+ const program = new Command('doc-drift')
7
+ .description('Scan for documentation drift (outdated comments, mismatched signatures)')
8
+ .option('--include <patterns...>', 'File patterns to include')
9
+ .option('--exclude <patterns...>', 'File patterns to exclude')
10
+ .option('--stale-months <number>', 'Months before a comment is considered potentially outdated', '6')
11
+ .action(async (options) => {
12
+ console.log(pc.cyan('Analyzing documentation drift...'));
13
+ const report = await analyzeDocDrift({
14
+ rootDir: process.cwd(),
15
+ include: options.include,
16
+ exclude: options.exclude,
17
+ staleMonths: parseInt(options.staleMonths, 10),
18
+ });
19
+
20
+ console.log(pc.bold('Doc Drift Analysis Results:'));
21
+ console.log(`Rating: ${report.summary.rating.toUpperCase()} (Score: ${report.summary.score})`);
22
+ if (report.issues.length > 0) {
23
+ console.log(pc.red(`\nFound ${report.issues.length} drift issues.`));
24
+ } else {
25
+ console.log(pc.green('\nNo documentation drift detected.'));
26
+ }
27
+ });
28
+
29
+ return program;
30
+ }
31
+
32
+ if (require.main === module) {
33
+ createCommand().parseAsync(process.argv).catch(err => {
34
+ console.error(pc.red(err.message));
35
+ process.exit(1);
36
+ });
37
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './types';
2
+ export * from './analyzer';
package/src/types.ts ADDED
@@ -0,0 +1,29 @@
1
+ import type { ScanOptions, Issue } from '@aiready/core';
2
+
3
+ export interface DocDriftOptions extends ScanOptions {
4
+ /** Maximum commit distance to check for drift */
5
+ maxCommits?: number;
6
+ /** Consider comments older than this many months as outdated */
7
+ staleMonths?: number;
8
+ }
9
+
10
+ export interface DocDriftIssue extends Issue {
11
+ type: 'doc-drift';
12
+ }
13
+
14
+ export interface DocDriftReport {
15
+ summary: {
16
+ filesAnalyzed: number;
17
+ functionsAnalyzed: number;
18
+ score: number;
19
+ rating: 'minimal' | 'low' | 'moderate' | 'high' | 'severe';
20
+ };
21
+ issues: DocDriftIssue[];
22
+ rawData: {
23
+ uncommentedExports: number;
24
+ totalExports: number;
25
+ outdatedComments: number;
26
+ undocumentedComplexity: number;
27
+ };
28
+ recommendations: string[];
29
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src/**/*"]
8
+ }