@eduardbar/drift 0.9.1 → 1.1.0

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.
Files changed (129) hide show
  1. package/.github/actions/drift-scan/README.md +61 -0
  2. package/.github/actions/drift-scan/action.yml +65 -0
  3. package/.github/workflows/publish-vscode.yml +78 -0
  4. package/AGENTS.md +83 -23
  5. package/README.md +69 -2
  6. package/ROADMAP.md +130 -98
  7. package/dist/analyzer.d.ts +8 -38
  8. package/dist/analyzer.js +181 -1526
  9. package/dist/badge.js +40 -22
  10. package/dist/ci.js +32 -18
  11. package/dist/cli.js +125 -4
  12. package/dist/config.js +1 -1
  13. package/dist/diff.d.ts +0 -7
  14. package/dist/diff.js +26 -25
  15. package/dist/fix.d.ts +17 -0
  16. package/dist/fix.js +132 -0
  17. package/dist/git/blame.d.ts +22 -0
  18. package/dist/git/blame.js +227 -0
  19. package/dist/git/helpers.d.ts +36 -0
  20. package/dist/git/helpers.js +152 -0
  21. package/dist/git/trend.d.ts +21 -0
  22. package/dist/git/trend.js +81 -0
  23. package/dist/git.d.ts +0 -13
  24. package/dist/git.js +27 -21
  25. package/dist/index.d.ts +5 -1
  26. package/dist/index.js +3 -0
  27. package/dist/map.d.ts +3 -0
  28. package/dist/map.js +103 -0
  29. package/dist/metrics.d.ts +4 -0
  30. package/dist/metrics.js +176 -0
  31. package/dist/plugins.d.ts +6 -0
  32. package/dist/plugins.js +74 -0
  33. package/dist/printer.js +20 -0
  34. package/dist/report.js +654 -293
  35. package/dist/reporter.js +85 -2
  36. package/dist/review.d.ts +15 -0
  37. package/dist/review.js +80 -0
  38. package/dist/rules/comments.d.ts +4 -0
  39. package/dist/rules/comments.js +45 -0
  40. package/dist/rules/complexity.d.ts +4 -0
  41. package/dist/rules/complexity.js +51 -0
  42. package/dist/rules/coupling.d.ts +4 -0
  43. package/dist/rules/coupling.js +19 -0
  44. package/dist/rules/magic.d.ts +4 -0
  45. package/dist/rules/magic.js +33 -0
  46. package/dist/rules/nesting.d.ts +5 -0
  47. package/dist/rules/nesting.js +82 -0
  48. package/dist/rules/phase0-basic.d.ts +11 -0
  49. package/dist/rules/phase0-basic.js +183 -0
  50. package/dist/rules/phase1-complexity.d.ts +7 -0
  51. package/dist/rules/phase1-complexity.js +8 -0
  52. package/dist/rules/phase2-crossfile.d.ts +23 -0
  53. package/dist/rules/phase2-crossfile.js +135 -0
  54. package/dist/rules/phase3-arch.d.ts +23 -0
  55. package/dist/rules/phase3-arch.js +151 -0
  56. package/dist/rules/phase3-configurable.d.ts +6 -0
  57. package/dist/rules/phase3-configurable.js +97 -0
  58. package/dist/rules/phase5-ai.d.ts +8 -0
  59. package/dist/rules/phase5-ai.js +262 -0
  60. package/dist/rules/phase8-semantic.d.ts +17 -0
  61. package/dist/rules/phase8-semantic.js +110 -0
  62. package/dist/rules/promise.d.ts +4 -0
  63. package/dist/rules/promise.js +24 -0
  64. package/dist/rules/shared.d.ts +7 -0
  65. package/dist/rules/shared.js +27 -0
  66. package/dist/snapshot.d.ts +19 -0
  67. package/dist/snapshot.js +119 -0
  68. package/dist/types.d.ts +69 -0
  69. package/dist/utils.d.ts +2 -1
  70. package/dist/utils.js +1 -0
  71. package/docs/AGENTS.md +146 -0
  72. package/docs/PRD.md +208 -0
  73. package/package.json +8 -3
  74. package/packages/eslint-plugin-drift/src/index.ts +1 -1
  75. package/packages/vscode-drift/.vscodeignore +9 -0
  76. package/packages/vscode-drift/LICENSE +21 -0
  77. package/packages/vscode-drift/README.md +64 -0
  78. package/packages/vscode-drift/images/icon.png +0 -0
  79. package/packages/vscode-drift/images/icon.svg +30 -0
  80. package/packages/vscode-drift/package-lock.json +485 -0
  81. package/packages/vscode-drift/package.json +119 -0
  82. package/packages/vscode-drift/src/analyzer.ts +40 -0
  83. package/packages/vscode-drift/src/diagnostics.ts +55 -0
  84. package/packages/vscode-drift/src/extension.ts +135 -0
  85. package/packages/vscode-drift/src/statusbar.ts +55 -0
  86. package/packages/vscode-drift/src/treeview.ts +110 -0
  87. package/packages/vscode-drift/tsconfig.json +18 -0
  88. package/packages/vscode-drift/vscode-drift-0.1.0.vsix +0 -0
  89. package/packages/vscode-drift/vscode-drift-0.1.1.vsix +0 -0
  90. package/src/analyzer.ts +248 -1765
  91. package/src/badge.ts +38 -16
  92. package/src/ci.ts +38 -17
  93. package/src/cli.ts +143 -4
  94. package/src/config.ts +1 -1
  95. package/src/diff.ts +36 -30
  96. package/src/fix.ts +178 -0
  97. package/src/git/blame.ts +279 -0
  98. package/src/git/helpers.ts +198 -0
  99. package/src/git/trend.ts +117 -0
  100. package/src/git.ts +33 -24
  101. package/src/index.ts +16 -1
  102. package/src/map.ts +117 -0
  103. package/src/metrics.ts +200 -0
  104. package/src/plugins.ts +76 -0
  105. package/src/printer.ts +20 -0
  106. package/src/report.ts +666 -296
  107. package/src/reporter.ts +95 -2
  108. package/src/review.ts +98 -0
  109. package/src/rules/comments.ts +56 -0
  110. package/src/rules/complexity.ts +57 -0
  111. package/src/rules/coupling.ts +23 -0
  112. package/src/rules/magic.ts +38 -0
  113. package/src/rules/nesting.ts +88 -0
  114. package/src/rules/phase0-basic.ts +194 -0
  115. package/src/rules/phase1-complexity.ts +8 -0
  116. package/src/rules/phase2-crossfile.ts +177 -0
  117. package/src/rules/phase3-arch.ts +183 -0
  118. package/src/rules/phase3-configurable.ts +132 -0
  119. package/src/rules/phase5-ai.ts +292 -0
  120. package/src/rules/phase8-semantic.ts +136 -0
  121. package/src/rules/promise.ts +29 -0
  122. package/src/rules/shared.ts +39 -0
  123. package/src/snapshot.ts +175 -0
  124. package/src/types.ts +75 -1
  125. package/src/utils.ts +3 -1
  126. package/tests/helpers.ts +45 -0
  127. package/tests/new-features.test.ts +153 -0
  128. package/tests/rules.test.ts +1269 -0
  129. package/vitest.config.ts +15 -0
package/dist/reporter.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { scoreToGradeText, severityIcon } from './utils.js';
2
+ import { computeRepoQuality, computeMaintenanceRisk } from './metrics.js';
2
3
  const FIX_SUGGESTIONS = {
3
4
  'large-file': 'Consider splitting this file into smaller modules with single responsibility',
4
5
  'large-function': 'Extract logic into smaller functions with descriptive names',
@@ -21,6 +22,17 @@ const RULE_EFFORT = {
21
22
  };
22
23
  const SEVERITY_ORDER = { error: 0, warning: 1, info: 2 };
23
24
  const EFFORT_ORDER = { low: 0, medium: 1, high: 2 };
25
+ const AI_SIGNAL_RULES = new Set([
26
+ 'over-commented',
27
+ 'hardcoded-config',
28
+ 'inconsistent-error-handling',
29
+ 'unnecessary-abstraction',
30
+ 'naming-inconsistency',
31
+ 'comment-contradiction',
32
+ 'promise-style-mix',
33
+ 'any-abuse',
34
+ 'ai-code-smell',
35
+ ]);
24
36
  export function buildReport(targetPath, files) {
25
37
  const allIssues = files.flatMap((f) => f.issues);
26
38
  const byRule = {};
@@ -30,10 +42,11 @@ export function buildReport(targetPath, files) {
30
42
  const totalScore = files.length > 0
31
43
  ? Math.round(files.reduce((sum, f) => sum + f.score, 0) / files.length)
32
44
  : 0;
33
- return {
45
+ const sortedFiles = files.filter((f) => f.issues.length > 0).sort((a, b) => b.score - a.score);
46
+ const baseReport = {
34
47
  scannedAt: new Date().toISOString(),
35
48
  targetPath,
36
- files: files.filter((f) => f.issues.length > 0).sort((a, b) => b.score - a.score),
49
+ files: sortedFiles,
37
50
  totalIssues: allIssues.length,
38
51
  totalScore,
39
52
  totalFiles: files.length,
@@ -43,7 +56,29 @@ export function buildReport(targetPath, files) {
43
56
  infos: allIssues.filter((i) => i.severity === 'info').length,
44
57
  byRule,
45
58
  },
59
+ quality: {
60
+ overall: 100,
61
+ dimensions: {
62
+ architecture: 100,
63
+ complexity: 100,
64
+ 'ai-patterns': 100,
65
+ testing: 100,
66
+ },
67
+ },
68
+ maintenanceRisk: {
69
+ score: 0,
70
+ level: 'low',
71
+ hotspots: [],
72
+ signals: {
73
+ highComplexityFiles: 0,
74
+ filesWithoutNearbyTests: 0,
75
+ frequentChangeFiles: 0,
76
+ },
77
+ },
46
78
  };
79
+ baseReport.quality = computeRepoQuality(targetPath, files);
80
+ baseReport.maintenanceRisk = computeMaintenanceRisk(baseReport);
81
+ return baseReport;
47
82
  }
48
83
  function formatHeader(report, grade) {
49
84
  return [
@@ -146,12 +181,55 @@ function buildRecommendedAction(priorityOrder) {
146
181
  }
147
182
  return 'Start with the highest priority issue and work through them in order.';
148
183
  }
184
+ function fileAILikelihood(fileIssues) {
185
+ if (fileIssues.length === 0)
186
+ return { score: 0, triggers: [] };
187
+ const triggerCounts = new Map();
188
+ for (const issue of fileIssues) {
189
+ if (!AI_SIGNAL_RULES.has(issue.rule))
190
+ continue;
191
+ triggerCounts.set(issue.rule, (triggerCounts.get(issue.rule) ?? 0) + 1);
192
+ }
193
+ const triggerTotal = [...triggerCounts.values()].reduce((sum, count) => sum + count, 0);
194
+ const smellBoost = fileIssues.some((issue) => issue.rule === 'ai-code-smell') ? 20 : 0;
195
+ const ratioScore = Math.round((triggerTotal / Math.max(fileIssues.length, 1)) * 100);
196
+ const score = Math.max(0, Math.min(100, ratioScore + smellBoost));
197
+ const triggers = [...triggerCounts.entries()]
198
+ .sort((a, b) => b[1] - a[1])
199
+ .slice(0, 4)
200
+ .map(([rule]) => rule);
201
+ return { score, triggers };
202
+ }
203
+ function computeAILikelihood(report) {
204
+ const suspected = report.files
205
+ .map((file) => {
206
+ const likelihood = fileAILikelihood(file.issues);
207
+ return {
208
+ path: file.path,
209
+ ai_likelihood: likelihood.score,
210
+ triggers: likelihood.triggers,
211
+ };
212
+ })
213
+ .filter((entry) => entry.ai_likelihood >= 35)
214
+ .sort((a, b) => b.ai_likelihood - a.ai_likelihood);
215
+ const overall = suspected.length === 0
216
+ ? 0
217
+ : Math.round(suspected.reduce((sum, entry) => sum + entry.ai_likelihood, 0) / suspected.length);
218
+ const smellCount = report.files.flatMap((file) => file.issues).filter((issue) => issue.rule === 'ai-code-smell').length;
219
+ const smellScore = Math.min(100, smellCount * 15);
220
+ return {
221
+ overall,
222
+ files: suspected.slice(0, 10),
223
+ smellScore,
224
+ };
225
+ }
149
226
  export function formatAIOutput(report) {
150
227
  const allIssues = collectAllIssues(report);
151
228
  const sortedIssues = sortIssues(allIssues);
152
229
  const priorityOrder = sortedIssues.map((item, i) => buildAIIssue(item, i + 1));
153
230
  const rulesDetected = [...new Set(allIssues.map((i) => i.issue.rule))];
154
231
  const grade = scoreToGradeText(report.totalScore);
232
+ const aiLikelihood = computeAILikelihood(report);
155
233
  return {
156
234
  summary: {
157
235
  score: report.totalScore,
@@ -159,8 +237,13 @@ export function formatAIOutput(report) {
159
237
  total_issues: report.totalIssues,
160
238
  files_affected: report.files.length,
161
239
  files_clean: report.totalFiles - report.files.length,
240
+ ai_likelihood: aiLikelihood.overall,
241
+ ai_code_smell_score: aiLikelihood.smellScore,
162
242
  },
243
+ files_suspected: aiLikelihood.files,
163
244
  priority_order: priorityOrder,
245
+ maintenance_risk: report.maintenanceRisk,
246
+ quality: report.quality,
164
247
  context_for_ai: {
165
248
  project_type: 'typescript',
166
249
  scan_path: report.targetPath,
@@ -0,0 +1,15 @@
1
+ import type { DriftDiff } from './types.js';
2
+ export interface DriftReview {
3
+ baseRef: string;
4
+ scannedAt: string;
5
+ totalDelta: number;
6
+ newIssues: number;
7
+ resolvedIssues: number;
8
+ status: 'clean' | 'improved' | 'regressed';
9
+ summary: string;
10
+ markdown: string;
11
+ diff: DriftDiff;
12
+ }
13
+ export declare function formatReviewMarkdown(review: DriftReview): string;
14
+ export declare function generateReview(projectPath: string, baseRef: string): Promise<DriftReview>;
15
+ //# sourceMappingURL=review.d.ts.map
package/dist/review.js ADDED
@@ -0,0 +1,80 @@
1
+ import { resolve } from 'node:path';
2
+ import { analyzeProject } from './analyzer.js';
3
+ import { loadConfig } from './config.js';
4
+ import { buildReport } from './reporter.js';
5
+ import { cleanupTempDir, extractFilesAtRef } from './git.js';
6
+ import { computeDiff } from './diff.js';
7
+ export function formatReviewMarkdown(review) {
8
+ const trendIcon = review.status === 'regressed' ? '⚠️' : review.status === 'improved' ? '✅' : 'ℹ️';
9
+ const topFiles = review.diff.files
10
+ .slice(0, 8)
11
+ .map((file) => {
12
+ const sign = file.scoreDelta > 0 ? '+' : '';
13
+ return `- \`${file.path}\`: ${file.scoreBefore} -> ${file.scoreAfter} (${sign}${file.scoreDelta}), +${file.newIssues.length} new / -${file.resolvedIssues.length} resolved`;
14
+ })
15
+ .join('\n');
16
+ return [
17
+ '## drift review',
18
+ '',
19
+ `${trendIcon} ${review.summary}`,
20
+ '',
21
+ `- Base ref: \`${review.baseRef}\``,
22
+ `- Score delta: **${review.totalDelta >= 0 ? '+' : ''}${review.totalDelta}**`,
23
+ `- New issues: **${review.newIssues}**`,
24
+ `- Resolved issues: **${review.resolvedIssues}**`,
25
+ '',
26
+ '### File breakdown',
27
+ topFiles || '- No file-level deltas detected',
28
+ ].join('\n');
29
+ }
30
+ function getStatus(totalDelta, newIssues) {
31
+ if (totalDelta > 0 || newIssues > 0)
32
+ return 'regressed';
33
+ if (totalDelta < 0)
34
+ return 'improved';
35
+ return 'clean';
36
+ }
37
+ export async function generateReview(projectPath, baseRef) {
38
+ const resolvedPath = resolve(projectPath);
39
+ const config = await loadConfig(resolvedPath);
40
+ const currentFiles = analyzeProject(resolvedPath, config);
41
+ const currentReport = buildReport(resolvedPath, currentFiles);
42
+ let tempDir;
43
+ try {
44
+ tempDir = extractFilesAtRef(resolvedPath, baseRef);
45
+ const baseFiles = analyzeProject(tempDir, config);
46
+ const baseReport = buildReport(tempDir, baseFiles);
47
+ const remappedBase = {
48
+ ...baseReport,
49
+ files: baseReport.files.map((file) => ({
50
+ ...file,
51
+ path: file.path.replace(tempDir, resolvedPath),
52
+ })),
53
+ };
54
+ const diff = computeDiff(remappedBase, currentReport, baseRef);
55
+ const status = getStatus(diff.totalDelta, diff.newIssuesCount);
56
+ const summary = status === 'regressed'
57
+ ? `Drift regressed: +${diff.totalDelta} score and ${diff.newIssuesCount} new issue(s).`
58
+ : status === 'improved'
59
+ ? `Drift improved: ${diff.totalDelta} score delta and ${diff.resolvedIssuesCount} issue(s) resolved.`
60
+ : 'No drift changes detected against base ref.';
61
+ const review = {
62
+ baseRef,
63
+ scannedAt: new Date().toISOString(),
64
+ totalDelta: diff.totalDelta,
65
+ newIssues: diff.newIssuesCount,
66
+ resolvedIssues: diff.resolvedIssuesCount,
67
+ status,
68
+ summary,
69
+ markdown: '',
70
+ diff,
71
+ };
72
+ review.markdown = formatReviewMarkdown(review);
73
+ return review;
74
+ }
75
+ finally {
76
+ if (tempDir)
77
+ cleanupTempDir(tempDir);
78
+ }
79
+ }
80
+ //# sourceMappingURL=review.js.map
@@ -0,0 +1,4 @@
1
+ import { SourceFile } from 'ts-morph';
2
+ import type { DriftIssue } from '../types.js';
3
+ export declare function detectCommentContradiction(file: SourceFile): DriftIssue[];
4
+ //# sourceMappingURL=comments.d.ts.map
@@ -0,0 +1,45 @@
1
+ import { hasIgnoreComment } from './shared.js';
2
+ const TRIVIAL_COMMENT_PATTERNS = [
3
+ { comment: /\/\/\s*return\b/i, code: /^\s*return\b/ },
4
+ { comment: /\/\/\s*(increment|increase|add\s+1|plus\s+1)\b/i, code: /\+\+|(\+= ?1)\b/ },
5
+ { comment: /\/\/\s*(decrement|decrease|subtract\s+1|minus\s+1)\b/i, code: /--|(-= ?1)\b/ },
6
+ { comment: /\/\/\s*log\b/i, code: /console\.(log|warn|error)/ },
7
+ { comment: /\/\/\s*(set|assign)\b/i, code: /^\s*\w[\w.[\]]*\s*=(?!=)/ },
8
+ { comment: /\/\/\s*call\b/i, code: /^\s*\w[\w.]*\(/ },
9
+ { comment: /\/\/\s*(declare|define|create|initialize)\b/i, code: /^\s*(const|let|var)\b/ },
10
+ { comment: /\/\/\s*check\s+if\b/i, code: /^\s*if\s*\(/ },
11
+ { comment: /\/\/\s*(loop|iterate|for each|foreach)\b/i, code: /^\s*(for|while)\b/ },
12
+ { comment: /\/\/\s*import\b/i, code: /^\s*import\b/ },
13
+ ];
14
+ const SNIPPET_TRUNCATE = 60;
15
+ function checkLineForContradiction(commentLine, nextLine, lineNumber, file) {
16
+ for (const { comment, code } of TRIVIAL_COMMENT_PATTERNS) {
17
+ if (comment.test(commentLine) && code.test(nextLine)) {
18
+ if (hasIgnoreComment(file, lineNumber))
19
+ return null;
20
+ return {
21
+ rule: 'comment-contradiction',
22
+ severity: 'warning',
23
+ message: `Comment restates what the code already says. AI documents the obvious instead of the why.`,
24
+ line: lineNumber,
25
+ column: 1,
26
+ snippet: `${commentLine.slice(0, SNIPPET_TRUNCATE)}\n${nextLine.trim().slice(0, SNIPPET_TRUNCATE)}`,
27
+ };
28
+ }
29
+ }
30
+ return null;
31
+ }
32
+ export function detectCommentContradiction(file) {
33
+ const issues = [];
34
+ const lines = file.getFullText().split('\n');
35
+ for (let i = 0; i < lines.length - 1; i++) {
36
+ const commentLine = lines[i].trim();
37
+ const nextLine = lines[i + 1];
38
+ const issue = checkLineForContradiction(commentLine, nextLine, i + 1, file);
39
+ if (issue) {
40
+ issues.push(issue);
41
+ }
42
+ }
43
+ return issues;
44
+ }
45
+ //# sourceMappingURL=comments.js.map
@@ -0,0 +1,4 @@
1
+ import { SourceFile } from 'ts-morph';
2
+ import type { DriftIssue } from '../types.js';
3
+ export declare function detectHighComplexity(file: SourceFile): DriftIssue[];
4
+ //# sourceMappingURL=complexity.d.ts.map
@@ -0,0 +1,51 @@
1
+ import { SyntaxKind } from 'ts-morph';
2
+ import { hasIgnoreComment, getSnippet } from './shared.js';
3
+ const COMPLEXITY_THRESHOLD = 10;
4
+ const INCREMENT_KINDS = [
5
+ SyntaxKind.IfStatement,
6
+ SyntaxKind.ForStatement,
7
+ SyntaxKind.ForInStatement,
8
+ SyntaxKind.ForOfStatement,
9
+ SyntaxKind.WhileStatement,
10
+ SyntaxKind.DoStatement,
11
+ SyntaxKind.CaseClause,
12
+ SyntaxKind.CatchClause,
13
+ SyntaxKind.ConditionalExpression,
14
+ SyntaxKind.AmpersandAmpersandToken,
15
+ SyntaxKind.BarBarToken,
16
+ SyntaxKind.QuestionQuestionToken,
17
+ ];
18
+ function getCyclomaticComplexity(fn) {
19
+ let complexity = 1;
20
+ for (const kind of INCREMENT_KINDS) {
21
+ complexity += fn.getDescendantsOfKind(kind).length;
22
+ }
23
+ return complexity;
24
+ }
25
+ export function detectHighComplexity(file) {
26
+ const issues = [];
27
+ const fns = [
28
+ ...file.getFunctions(),
29
+ ...file.getDescendantsOfKind(SyntaxKind.ArrowFunction),
30
+ ...file.getDescendantsOfKind(SyntaxKind.FunctionExpression),
31
+ ...file.getClasses().flatMap((c) => c.getMethods()),
32
+ ];
33
+ for (const fn of fns) {
34
+ const complexity = getCyclomaticComplexity(fn);
35
+ if (complexity > COMPLEXITY_THRESHOLD) {
36
+ const startLine = fn.getStartLineNumber();
37
+ if (hasIgnoreComment(file, startLine))
38
+ continue;
39
+ issues.push({
40
+ rule: 'high-complexity',
41
+ severity: 'error',
42
+ message: `Cyclomatic complexity is ${complexity} (threshold: ${COMPLEXITY_THRESHOLD}). AI generates correct code, not simple code.`,
43
+ line: startLine,
44
+ column: fn.getStartLinePos(),
45
+ snippet: getSnippet(fn, file),
46
+ });
47
+ }
48
+ }
49
+ return issues;
50
+ }
51
+ //# sourceMappingURL=complexity.js.map
@@ -0,0 +1,4 @@
1
+ import { SourceFile } from 'ts-morph';
2
+ import type { DriftIssue } from '../types.js';
3
+ export declare function detectHighCoupling(file: SourceFile): DriftIssue[];
4
+ //# sourceMappingURL=coupling.d.ts.map
@@ -0,0 +1,19 @@
1
+ const COUPLING_THRESHOLD = 10;
2
+ export function detectHighCoupling(file) {
3
+ const imports = file.getImportDeclarations();
4
+ const sources = new Set(imports.map((i) => i.getModuleSpecifierValue()));
5
+ if (sources.size > COUPLING_THRESHOLD) {
6
+ return [
7
+ {
8
+ rule: 'high-coupling',
9
+ severity: 'warning',
10
+ message: `File imports from ${sources.size} distinct modules (threshold: ${COUPLING_THRESHOLD}). High coupling makes refactoring dangerous.`,
11
+ line: 1,
12
+ column: 1,
13
+ snippet: `// ${sources.size} import sources`,
14
+ },
15
+ ];
16
+ }
17
+ return [];
18
+ }
19
+ //# sourceMappingURL=coupling.js.map
@@ -0,0 +1,4 @@
1
+ import { SourceFile } from 'ts-morph';
2
+ import type { DriftIssue } from '../types.js';
3
+ export declare function detectMagicNumbers(file: SourceFile): DriftIssue[];
4
+ //# sourceMappingURL=magic.d.ts.map
@@ -0,0 +1,33 @@
1
+ import { SyntaxKind } from 'ts-morph';
2
+ import { hasIgnoreComment, getSnippet } from './shared.js';
3
+ const ALLOWED_NUMBERS = new Set([0, 1, -1, 2, 100]);
4
+ export function detectMagicNumbers(file) {
5
+ const issues = [];
6
+ for (const node of file.getDescendantsOfKind(SyntaxKind.NumericLiteral)) {
7
+ const value = Number(node.getLiteralValue());
8
+ if (ALLOWED_NUMBERS.has(value))
9
+ continue;
10
+ const parent = node.getParent();
11
+ if (!parent)
12
+ continue;
13
+ const parentKind = parent.getKind();
14
+ if (parentKind === SyntaxKind.VariableDeclaration ||
15
+ parentKind === SyntaxKind.PropertyAssignment ||
16
+ parentKind === SyntaxKind.EnumMember ||
17
+ parentKind === SyntaxKind.Parameter)
18
+ continue;
19
+ const line = node.getStartLineNumber();
20
+ if (hasIgnoreComment(file, line))
21
+ continue;
22
+ issues.push({
23
+ rule: 'magic-number',
24
+ severity: 'info',
25
+ message: `Magic number ${value} used directly in logic. Extract to a named constant.`,
26
+ line,
27
+ column: node.getStartLinePos(),
28
+ snippet: getSnippet(node, file),
29
+ });
30
+ }
31
+ return issues;
32
+ }
33
+ //# sourceMappingURL=magic.js.map
@@ -0,0 +1,5 @@
1
+ import { SourceFile } from 'ts-morph';
2
+ import type { DriftIssue } from '../types.js';
3
+ export declare function detectDeepNesting(file: SourceFile): DriftIssue[];
4
+ export declare function detectTooManyParams(file: SourceFile): DriftIssue[];
5
+ //# sourceMappingURL=nesting.d.ts.map
@@ -0,0 +1,82 @@
1
+ import { SyntaxKind } from 'ts-morph';
2
+ import { hasIgnoreComment, getSnippet } from './shared.js';
3
+ const NESTING_THRESHOLD = 3;
4
+ const PARAMS_THRESHOLD = 4;
5
+ const NESTING_KINDS = new Set([
6
+ SyntaxKind.IfStatement,
7
+ SyntaxKind.ForStatement,
8
+ SyntaxKind.ForInStatement,
9
+ SyntaxKind.ForOfStatement,
10
+ SyntaxKind.WhileStatement,
11
+ SyntaxKind.DoStatement,
12
+ SyntaxKind.TryStatement,
13
+ SyntaxKind.SwitchStatement,
14
+ ]);
15
+ function getMaxNestingDepth(fn) {
16
+ let maxDepth = 0;
17
+ function walk(node, depth) {
18
+ if (NESTING_KINDS.has(node.getKind())) {
19
+ depth++;
20
+ if (depth > maxDepth)
21
+ maxDepth = depth;
22
+ }
23
+ for (const child of node.getChildren()) {
24
+ walk(child, depth);
25
+ }
26
+ }
27
+ walk(fn, 0);
28
+ return maxDepth;
29
+ }
30
+ export function detectDeepNesting(file) {
31
+ const issues = [];
32
+ const fns = [
33
+ ...file.getFunctions(),
34
+ ...file.getDescendantsOfKind(SyntaxKind.ArrowFunction),
35
+ ...file.getDescendantsOfKind(SyntaxKind.FunctionExpression),
36
+ ...file.getClasses().flatMap((c) => c.getMethods()),
37
+ ];
38
+ for (const fn of fns) {
39
+ const depth = getMaxNestingDepth(fn);
40
+ if (depth > NESTING_THRESHOLD) {
41
+ const startLine = fn.getStartLineNumber();
42
+ if (hasIgnoreComment(file, startLine))
43
+ continue;
44
+ issues.push({
45
+ rule: 'deep-nesting',
46
+ severity: 'warning',
47
+ message: `Maximum nesting depth is ${depth} (threshold: ${NESTING_THRESHOLD}). Deep nesting is the #1 readability killer.`,
48
+ line: startLine,
49
+ column: fn.getStartLinePos(),
50
+ snippet: getSnippet(fn, file),
51
+ });
52
+ }
53
+ }
54
+ return issues;
55
+ }
56
+ export function detectTooManyParams(file) {
57
+ const issues = [];
58
+ const fns = [
59
+ ...file.getFunctions(),
60
+ ...file.getDescendantsOfKind(SyntaxKind.ArrowFunction),
61
+ ...file.getDescendantsOfKind(SyntaxKind.FunctionExpression),
62
+ ...file.getClasses().flatMap((c) => c.getMethods()),
63
+ ];
64
+ for (const fn of fns) {
65
+ const paramCount = fn.getParameters().length;
66
+ if (paramCount > PARAMS_THRESHOLD) {
67
+ const startLine = fn.getStartLineNumber();
68
+ if (hasIgnoreComment(file, startLine))
69
+ continue;
70
+ issues.push({
71
+ rule: 'too-many-params',
72
+ severity: 'warning',
73
+ message: `Function has ${paramCount} parameters (threshold: ${PARAMS_THRESHOLD}). AI avoids refactoring into options objects.`,
74
+ line: startLine,
75
+ column: fn.getStartLinePos(),
76
+ snippet: getSnippet(fn, file),
77
+ });
78
+ }
79
+ }
80
+ return issues;
81
+ }
82
+ //# sourceMappingURL=nesting.js.map
@@ -0,0 +1,11 @@
1
+ import { SourceFile } from 'ts-morph';
2
+ import type { DriftIssue } from '../types.js';
3
+ export declare function detectLargeFile(file: SourceFile): DriftIssue[];
4
+ export declare function detectLargeFunctions(file: SourceFile): DriftIssue[];
5
+ export declare function detectDebugLeftovers(file: SourceFile): DriftIssue[];
6
+ export declare function detectDeadCode(file: SourceFile): DriftIssue[];
7
+ export declare function detectDuplicateFunctionNames(file: SourceFile): DriftIssue[];
8
+ export declare function detectAnyAbuse(file: SourceFile): DriftIssue[];
9
+ export declare function detectCatchSwallow(file: SourceFile): DriftIssue[];
10
+ export declare function detectMissingReturnTypes(file: SourceFile): DriftIssue[];
11
+ //# sourceMappingURL=phase0-basic.d.ts.map