Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 1x 1x 1x 1x 1x 1x 1x 1x 1x 2x 2x 1x 1x 1x 1x 1x 1x | import {
scanFiles,
calculateDocDrift,
getFileCommitTimestamps,
getLineRangeLastModifiedCached,
Severity,
IssueType,
emitProgress,
getParser,
Language,
} from '@aiready/core';
import type { DocDriftOptions, DocDriftReport, DocDriftIssue } from './types';
import { readFileSync } from 'fs';
export async function analyzeDocDrift(
options: DocDriftOptions
): Promise<DocDriftReport> {
// Use core scanFiles which respects .gitignore recursively
const files = await scanFiles(options);
const issues: DocDriftIssue[] = [];
const staleMonths = options.staleMonths ?? 6;
const staleSeconds = staleMonths * 30 * 24 * 60 * 60;
let uncommentedExports = 0;
let totalExports = 0;
let outdatedComments = 0;
let undocumentedComplexity = 0;
const now = Math.floor(Date.now() / 1000);
let processed = 0;
for (const file of files) {
processed++;
emitProgress(
processed,
files.length,
'doc-drift',
'analyzing files',
options.onProgress
);
const parser = getParser(file);
Iif (!parser) continue;
let code: string;
try {
code = readFileSync(file, 'utf-8');
} catch {
continue;
}
try {
// Initialize parser (it's a singleton in core, but ensures WASM is loaded)
await parser.initialize();
const parseResult = parser.parse(code, file);
let fileLineStamps: Record<number, number> | undefined;
for (const exp of parseResult.exports) {
// Only analyze functions and classes for documentation drift
Eif (exp.type === 'function' || exp.type === 'class') {
totalExports++;
if (!exp.documentation) {
uncommentedExports++;
// Complexity check (heuristic based on line count if range available)
Eif (exp.loc) {
const lines = exp.loc.end.line - exp.loc.start.line;
Eif (lines > 20) undocumentedComplexity++;
}
} else {
const doc = exp.documentation;
const docContent = doc.content;
// Signature mismatch detection (generalized heuristic)
Eif (exp.type === 'function' && exp.parameters) {
const params = exp.parameters;
// Check if params mentioned in doc (standard @param or simple mention)
// Use regex with word boundaries to avoid partial matches (e.g. 'b' in 'numbers')
const missingParams = params.filter((p) => {
const regex = new RegExp(`\\b${p}\\b`, 'i');
return !regex.test(docContent);
});
Eif (missingParams.length > 0) {
outdatedComments++;
issues.push({
type: IssueType.DocDrift,
severity: Severity.Major,
message: `Documentation mismatch: function parameters (${missingParams.join(', ')}) are not mentioned in the docs.`,
location: { file, line: exp.loc?.start.line || 1 },
});
continue;
}
}
// Timestamp comparison
if (exp.loc) {
if (!fileLineStamps) {
fileLineStamps = getFileCommitTimestamps(file);
}
// We don't have exact lines for the doc node in ExportInfo yet,
// but we know it precedes the export. Using export start as a proxy for drift check.
const bodyModified = getLineRangeLastModifiedCached(
fileLineStamps,
exp.loc.start.line,
exp.loc.end.line
);
if (bodyModified > 0) {
// If body was modified much later than the "stale" threshold
if (
now - bodyModified < staleSeconds / 4 &&
exp.documentation.isStale === true
) {
// This would require isStale to be set by the parser if it knew history
// For now, we compare body modification vs current time if docs look very old (heuristic)
}
// If the file itself is very old but has no issues, it's fine.
// Doc-drift is really about implementation changing without doc updates.
}
}
}
}
}
} catch (error) {
console.warn(`Doc-drift: Failed to parse ${file}: ${error}`);
continue;
}
}
const riskResult = calculateDocDrift({
uncommentedExports,
totalExports,
outdatedComments,
undocumentedComplexity,
});
return {
summary: {
filesAnalyzed: files.length,
functionsAnalyzed: totalExports,
score: riskResult.score,
rating: riskResult.rating,
},
issues,
rawData: {
uncommentedExports,
totalExports,
outdatedComments,
undocumentedComplexity,
},
recommendations: riskResult.recommendations,
};
}
|