@eduardbar/drift 1.0.0 → 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 (99) 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 +3 -1
  4. package/AGENTS.md +53 -11
  5. package/README.md +68 -1
  6. package/dist/analyzer.d.ts +6 -2
  7. package/dist/analyzer.js +116 -3
  8. package/dist/badge.js +40 -22
  9. package/dist/ci.js +32 -18
  10. package/dist/cli.js +83 -5
  11. package/dist/diff.d.ts +0 -7
  12. package/dist/diff.js +26 -25
  13. package/dist/fix.d.ts +4 -0
  14. package/dist/fix.js +59 -47
  15. package/dist/git/trend.js +1 -0
  16. package/dist/git.d.ts +0 -9
  17. package/dist/git.js +25 -19
  18. package/dist/index.d.ts +5 -1
  19. package/dist/index.js +3 -0
  20. package/dist/map.d.ts +3 -0
  21. package/dist/map.js +103 -0
  22. package/dist/metrics.d.ts +4 -0
  23. package/dist/metrics.js +176 -0
  24. package/dist/plugins.d.ts +6 -0
  25. package/dist/plugins.js +74 -0
  26. package/dist/printer.js +20 -0
  27. package/dist/report.js +34 -0
  28. package/dist/reporter.js +85 -2
  29. package/dist/review.d.ts +15 -0
  30. package/dist/review.js +80 -0
  31. package/dist/rules/comments.d.ts +4 -0
  32. package/dist/rules/comments.js +45 -0
  33. package/dist/rules/complexity.d.ts +4 -0
  34. package/dist/rules/complexity.js +51 -0
  35. package/dist/rules/coupling.d.ts +4 -0
  36. package/dist/rules/coupling.js +19 -0
  37. package/dist/rules/magic.d.ts +4 -0
  38. package/dist/rules/magic.js +33 -0
  39. package/dist/rules/nesting.d.ts +5 -0
  40. package/dist/rules/nesting.js +82 -0
  41. package/dist/rules/phase0-basic.js +14 -7
  42. package/dist/rules/phase1-complexity.d.ts +6 -30
  43. package/dist/rules/phase1-complexity.js +7 -276
  44. package/dist/rules/phase2-crossfile.d.ts +0 -4
  45. package/dist/rules/phase2-crossfile.js +52 -39
  46. package/dist/rules/phase3-arch.d.ts +0 -8
  47. package/dist/rules/phase3-arch.js +26 -23
  48. package/dist/rules/phase3-configurable.d.ts +6 -0
  49. package/dist/rules/phase3-configurable.js +97 -0
  50. package/dist/rules/phase8-semantic.d.ts +0 -5
  51. package/dist/rules/phase8-semantic.js +30 -29
  52. package/dist/rules/promise.d.ts +4 -0
  53. package/dist/rules/promise.js +24 -0
  54. package/dist/snapshot.d.ts +19 -0
  55. package/dist/snapshot.js +119 -0
  56. package/dist/types.d.ts +69 -0
  57. package/dist/utils.d.ts +2 -1
  58. package/dist/utils.js +1 -0
  59. package/docs/AGENTS.md +146 -0
  60. package/docs/PRD.md +208 -0
  61. package/package.json +1 -1
  62. package/packages/eslint-plugin-drift/src/index.ts +1 -1
  63. package/packages/vscode-drift/package.json +1 -1
  64. package/packages/vscode-drift/src/analyzer.ts +2 -0
  65. package/packages/vscode-drift/src/extension.ts +87 -63
  66. package/packages/vscode-drift/src/statusbar.ts +13 -5
  67. package/packages/vscode-drift/src/treeview.ts +2 -0
  68. package/src/analyzer.ts +144 -12
  69. package/src/badge.ts +38 -16
  70. package/src/ci.ts +38 -17
  71. package/src/cli.ts +96 -6
  72. package/src/diff.ts +36 -30
  73. package/src/fix.ts +77 -53
  74. package/src/git/trend.ts +3 -2
  75. package/src/git.ts +31 -22
  76. package/src/index.ts +16 -1
  77. package/src/map.ts +117 -0
  78. package/src/metrics.ts +200 -0
  79. package/src/plugins.ts +76 -0
  80. package/src/printer.ts +20 -0
  81. package/src/report.ts +35 -0
  82. package/src/reporter.ts +95 -2
  83. package/src/review.ts +98 -0
  84. package/src/rules/comments.ts +56 -0
  85. package/src/rules/complexity.ts +57 -0
  86. package/src/rules/coupling.ts +23 -0
  87. package/src/rules/magic.ts +38 -0
  88. package/src/rules/nesting.ts +88 -0
  89. package/src/rules/phase0-basic.ts +14 -7
  90. package/src/rules/phase1-complexity.ts +8 -302
  91. package/src/rules/phase2-crossfile.ts +68 -40
  92. package/src/rules/phase3-arch.ts +34 -30
  93. package/src/rules/phase3-configurable.ts +132 -0
  94. package/src/rules/phase8-semantic.ts +33 -29
  95. package/src/rules/promise.ts +29 -0
  96. package/src/snapshot.ts +175 -0
  97. package/src/types.ts +75 -1
  98. package/src/utils.ts +3 -1
  99. package/tests/new-features.test.ts +153 -0
@@ -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
@@ -1,13 +1,17 @@
1
1
  import { SyntaxKind } from 'ts-morph';
2
2
  import { hasIgnoreComment, getSnippet, getFunctionLikeLines } from './shared.js';
3
+ const LARGE_FILE_THRESHOLD = 300;
4
+ const LARGE_FUNCTION_THRESHOLD = 50;
5
+ const SNIPPET_TRUNCATE_SHORT = 60;
6
+ const SNIPPET_TRUNCATE_LONG = 120;
3
7
  export function detectLargeFile(file) {
4
8
  const lineCount = file.getEndLineNumber();
5
- if (lineCount > 300) {
9
+ if (lineCount > LARGE_FILE_THRESHOLD) {
6
10
  return [
7
11
  {
8
12
  rule: 'large-file',
9
13
  severity: 'error',
10
- message: `File has ${lineCount} lines (threshold: 300). Large files are the #1 sign of AI-generated structural drift.`,
14
+ message: `File has ${lineCount} lines (threshold: ${LARGE_FILE_THRESHOLD}). Large files are the #1 sign of AI-generated structural drift.`,
11
15
  line: 1,
12
16
  column: 1,
13
17
  snippet: `// ${lineCount} lines total`,
@@ -27,13 +31,13 @@ export function detectLargeFunctions(file) {
27
31
  for (const fn of fns) {
28
32
  const lines = getFunctionLikeLines(fn);
29
33
  const startLine = fn.getStartLineNumber();
30
- if (lines > 50) {
34
+ if (lines > LARGE_FUNCTION_THRESHOLD) {
31
35
  if (hasIgnoreComment(file, startLine))
32
36
  continue;
33
37
  issues.push({
34
38
  rule: 'large-function',
35
39
  severity: 'error',
36
- message: `Function spans ${lines} lines (threshold: 50). AI tends to dump logic into single functions.`,
40
+ message: `Function spans ${lines} lines (threshold: ${LARGE_FUNCTION_THRESHOLD}). AI tends to dump logic into single functions.`,
37
41
  line: startLine,
38
42
  column: fn.getStartLinePos(),
39
43
  snippet: getSnippet(fn, file),
@@ -68,10 +72,10 @@ export function detectDebugLeftovers(file) {
68
72
  issues.push({
69
73
  rule: 'debug-leftover',
70
74
  severity: 'warning',
71
- message: `Unresolved marker found: ${lineContent.trim().slice(0, 60)}`,
75
+ message: `Unresolved marker found: ${lineContent.trim().slice(0, SNIPPET_TRUNCATE_SHORT)}`,
72
76
  line: i + 1,
73
77
  column: 1,
74
- snippet: lineContent.trim().slice(0, 120),
78
+ snippet: lineContent.trim().slice(0, SNIPPET_TRUNCATE_LONG),
75
79
  });
76
80
  }
77
81
  });
@@ -145,11 +149,14 @@ export function detectCatchSwallow(file) {
145
149
  const block = catchClause.getBlock();
146
150
  const stmts = block.getStatements();
147
151
  if (stmts.length === 0) {
152
+ const line = catchClause.getStartLineNumber();
153
+ if (hasIgnoreComment(file, line))
154
+ continue;
148
155
  issues.push({
149
156
  rule: 'catch-swallow',
150
157
  severity: 'warning',
151
158
  message: `Empty catch block silently swallows errors. Classic AI pattern to make code "not throw".`,
152
- line: catchClause.getStartLineNumber(),
159
+ line,
153
160
  column: catchClause.getStartLinePos(),
154
161
  snippet: getSnippet(catchClause, file),
155
162
  });
@@ -1,31 +1,7 @@
1
- import { SourceFile } from 'ts-morph';
2
- import type { DriftIssue } from '../types.js';
3
- export declare function detectHighComplexity(file: SourceFile): DriftIssue[];
4
- export declare function detectDeepNesting(file: SourceFile): DriftIssue[];
5
- /**
6
- * Too many parameters: functions with more than 4 parameters.
7
- * AI avoids refactoring parameters into objects/options bags.
8
- */
9
- export declare function detectTooManyParams(file: SourceFile): DriftIssue[];
10
- /**
11
- * High coupling: files with more than 10 distinct import sources.
12
- * AI imports broadly without considering module cohesion.
13
- */
14
- export declare function detectHighCoupling(file: SourceFile): DriftIssue[];
15
- /**
16
- * Promise style mix: async/await and .then()/.catch() used in the same file.
17
- * AI generates both styles without consistency.
18
- */
19
- export declare function detectPromiseStyleMix(file: SourceFile): DriftIssue[];
20
- /**
21
- * Magic numbers: numeric literals used directly in logic outside of named constants.
22
- * Excludes 0, 1, -1 (universally understood) and array indices in obvious patterns.
23
- */
24
- export declare function detectMagicNumbers(file: SourceFile): DriftIssue[];
25
- /**
26
- * Comment contradiction: comments that restate exactly what the code does.
27
- * Classic AI pattern — documents the obvious instead of the why.
28
- * Detects: "// increment counter" above counter++, "// return x" above return x, etc.
29
- */
30
- export declare function detectCommentContradiction(file: SourceFile): DriftIssue[];
1
+ export { detectHighComplexity } from './complexity.js';
2
+ export { detectDeepNesting, detectTooManyParams } from './nesting.js';
3
+ export { detectHighCoupling } from './coupling.js';
4
+ export { detectPromiseStyleMix } from './promise.js';
5
+ export { detectMagicNumbers } from './magic.js';
6
+ export { detectCommentContradiction } from './comments.js';
31
7
  //# sourceMappingURL=phase1-complexity.d.ts.map
@@ -1,277 +1,8 @@
1
- import { SyntaxKind } from 'ts-morph';
2
- import { hasIgnoreComment, getSnippet } from './shared.js';
3
- /**
4
- * Cyclomatic complexity: count decision points in a function.
5
- * Each if/else if/ternary/?:/for/while/do/case/catch/&&/|| adds 1.
6
- * Threshold: > 10 is considered high complexity.
7
- */
8
- function getCyclomaticComplexity(fn) {
9
- let complexity = 1; // base path
10
- const incrementKinds = [
11
- SyntaxKind.IfStatement,
12
- SyntaxKind.ForStatement,
13
- SyntaxKind.ForInStatement,
14
- SyntaxKind.ForOfStatement,
15
- SyntaxKind.WhileStatement,
16
- SyntaxKind.DoStatement,
17
- SyntaxKind.CaseClause,
18
- SyntaxKind.CatchClause,
19
- SyntaxKind.ConditionalExpression, // ternary
20
- SyntaxKind.AmpersandAmpersandToken,
21
- SyntaxKind.BarBarToken,
22
- SyntaxKind.QuestionQuestionToken, // ??
23
- ];
24
- for (const kind of incrementKinds) {
25
- complexity += fn.getDescendantsOfKind(kind).length;
26
- }
27
- return complexity;
28
- }
29
- export function detectHighComplexity(file) {
30
- const issues = [];
31
- const fns = [
32
- ...file.getFunctions(),
33
- ...file.getDescendantsOfKind(SyntaxKind.ArrowFunction),
34
- ...file.getDescendantsOfKind(SyntaxKind.FunctionExpression),
35
- ...file.getClasses().flatMap((c) => c.getMethods()),
36
- ];
37
- for (const fn of fns) {
38
- const complexity = getCyclomaticComplexity(fn);
39
- if (complexity > 10) {
40
- const startLine = fn.getStartLineNumber();
41
- if (hasIgnoreComment(file, startLine))
42
- continue;
43
- issues.push({
44
- rule: 'high-complexity',
45
- severity: 'error',
46
- message: `Cyclomatic complexity is ${complexity} (threshold: 10). AI generates correct code, not simple code.`,
47
- line: startLine,
48
- column: fn.getStartLinePos(),
49
- snippet: getSnippet(fn, file),
50
- });
51
- }
52
- }
53
- return issues;
54
- }
55
- /**
56
- * Deep nesting: count the maximum nesting depth of control flow inside a function.
57
- * Counts: if, for, while, do, try, switch.
58
- * Threshold: > 3 levels.
59
- */
60
- function getMaxNestingDepth(fn) {
61
- const nestingKinds = new Set([
62
- SyntaxKind.IfStatement,
63
- SyntaxKind.ForStatement,
64
- SyntaxKind.ForInStatement,
65
- SyntaxKind.ForOfStatement,
66
- SyntaxKind.WhileStatement,
67
- SyntaxKind.DoStatement,
68
- SyntaxKind.TryStatement,
69
- SyntaxKind.SwitchStatement,
70
- ]);
71
- let maxDepth = 0;
72
- function walk(node, depth) {
73
- if (nestingKinds.has(node.getKind())) {
74
- depth++;
75
- if (depth > maxDepth)
76
- maxDepth = depth;
77
- }
78
- for (const child of node.getChildren()) {
79
- walk(child, depth);
80
- }
81
- }
82
- walk(fn, 0);
83
- return maxDepth;
84
- }
85
- export function detectDeepNesting(file) {
86
- const issues = [];
87
- const fns = [
88
- ...file.getFunctions(),
89
- ...file.getDescendantsOfKind(SyntaxKind.ArrowFunction),
90
- ...file.getDescendantsOfKind(SyntaxKind.FunctionExpression),
91
- ...file.getClasses().flatMap((c) => c.getMethods()),
92
- ];
93
- for (const fn of fns) {
94
- const depth = getMaxNestingDepth(fn);
95
- if (depth > 3) {
96
- const startLine = fn.getStartLineNumber();
97
- if (hasIgnoreComment(file, startLine))
98
- continue;
99
- issues.push({
100
- rule: 'deep-nesting',
101
- severity: 'warning',
102
- message: `Maximum nesting depth is ${depth} (threshold: 3). Deep nesting is the #1 readability killer.`,
103
- line: startLine,
104
- column: fn.getStartLinePos(),
105
- snippet: getSnippet(fn, file),
106
- });
107
- }
108
- }
109
- return issues;
110
- }
111
- /**
112
- * Too many parameters: functions with more than 4 parameters.
113
- * AI avoids refactoring parameters into objects/options bags.
114
- */
115
- export function detectTooManyParams(file) {
116
- const issues = [];
117
- const fns = [
118
- ...file.getFunctions(),
119
- ...file.getDescendantsOfKind(SyntaxKind.ArrowFunction),
120
- ...file.getDescendantsOfKind(SyntaxKind.FunctionExpression),
121
- ...file.getClasses().flatMap((c) => c.getMethods()),
122
- ];
123
- for (const fn of fns) {
124
- const paramCount = fn.getParameters().length;
125
- if (paramCount > 4) {
126
- const startLine = fn.getStartLineNumber();
127
- if (hasIgnoreComment(file, startLine))
128
- continue;
129
- issues.push({
130
- rule: 'too-many-params',
131
- severity: 'warning',
132
- message: `Function has ${paramCount} parameters (threshold: 4). AI avoids refactoring into options objects.`,
133
- line: startLine,
134
- column: fn.getStartLinePos(),
135
- snippet: getSnippet(fn, file),
136
- });
137
- }
138
- }
139
- return issues;
140
- }
141
- /**
142
- * High coupling: files with more than 10 distinct import sources.
143
- * AI imports broadly without considering module cohesion.
144
- */
145
- export function detectHighCoupling(file) {
146
- const imports = file.getImportDeclarations();
147
- const sources = new Set(imports.map((i) => i.getModuleSpecifierValue()));
148
- if (sources.size > 10) {
149
- return [
150
- {
151
- rule: 'high-coupling',
152
- severity: 'warning',
153
- message: `File imports from ${sources.size} distinct modules (threshold: 10). High coupling makes refactoring dangerous.`,
154
- line: 1,
155
- column: 1,
156
- snippet: `// ${sources.size} import sources`,
157
- },
158
- ];
159
- }
160
- return [];
161
- }
162
- /**
163
- * Promise style mix: async/await and .then()/.catch() used in the same file.
164
- * AI generates both styles without consistency.
165
- */
166
- export function detectPromiseStyleMix(file) {
167
- const text = file.getFullText();
168
- // detect .then( or .catch( calls (property access on a promise)
169
- const hasThen = file.getDescendantsOfKind(SyntaxKind.PropertyAccessExpression).some((node) => {
170
- const name = node.getName();
171
- return name === 'then' || name === 'catch';
172
- });
173
- // detect async keyword usage
174
- const hasAsync = file.getDescendantsOfKind(SyntaxKind.AsyncKeyword).length > 0 ||
175
- /\bawait\b/.test(text);
176
- if (hasThen && hasAsync) {
177
- return [
178
- {
179
- rule: 'promise-style-mix',
180
- severity: 'warning',
181
- message: `File mixes async/await with .then()/.catch(). AI generates both styles without picking one.`,
182
- line: 1,
183
- column: 1,
184
- snippet: `// mixed promise styles detected`,
185
- },
186
- ];
187
- }
188
- return [];
189
- }
190
- /**
191
- * Magic numbers: numeric literals used directly in logic outside of named constants.
192
- * Excludes 0, 1, -1 (universally understood) and array indices in obvious patterns.
193
- */
194
- export function detectMagicNumbers(file) {
195
- const issues = [];
196
- const ALLOWED = new Set([0, 1, -1, 2, 100]);
197
- for (const node of file.getDescendantsOfKind(SyntaxKind.NumericLiteral)) {
198
- const value = Number(node.getLiteralValue());
199
- if (ALLOWED.has(value))
200
- continue;
201
- // Skip: variable/const initializers at top level (those ARE the named constants)
202
- const parent = node.getParent();
203
- if (!parent)
204
- continue;
205
- const parentKind = parent.getKind();
206
- if (parentKind === SyntaxKind.VariableDeclaration ||
207
- parentKind === SyntaxKind.PropertyAssignment ||
208
- parentKind === SyntaxKind.EnumMember ||
209
- parentKind === SyntaxKind.Parameter)
210
- continue;
211
- const line = node.getStartLineNumber();
212
- if (hasIgnoreComment(file, line))
213
- continue;
214
- issues.push({
215
- rule: 'magic-number',
216
- severity: 'info',
217
- message: `Magic number ${value} used directly in logic. Extract to a named constant.`,
218
- line,
219
- column: node.getStartLinePos(),
220
- snippet: getSnippet(node, file),
221
- });
222
- }
223
- return issues;
224
- }
225
- /**
226
- * Comment contradiction: comments that restate exactly what the code does.
227
- * Classic AI pattern — documents the obvious instead of the why.
228
- * Detects: "// increment counter" above counter++, "// return x" above return x, etc.
229
- */
230
- export function detectCommentContradiction(file) {
231
- const issues = [];
232
- const lines = file.getFullText().split('\n');
233
- // Patterns: comment that is a near-literal restatement of the next line
234
- const trivialCommentPatterns = [
235
- // "// return ..." above a return statement
236
- { comment: /\/\/\s*return\b/i, code: /^\s*return\b/ },
237
- // "// increment ..." or "// increase ..." above x++ or x += 1
238
- { comment: /\/\/\s*(increment|increase|add\s+1|plus\s+1)\b/i, code: /\+\+|(\+= ?1)\b/ },
239
- // "// decrement ..." above x-- or x -= 1
240
- { comment: /\/\/\s*(decrement|decrease|subtract\s+1|minus\s+1)\b/i, code: /--|(-= ?1)\b/ },
241
- // "// log ..." above console.log
242
- { comment: /\/\/\s*log\b/i, code: /console\.(log|warn|error)/ },
243
- // "// set ... to ..." or "// assign ..." above assignment
244
- { comment: /\/\/\s*(set|assign)\b/i, code: /^\s*\w[\w.[\]]*\s*=(?!=)/ },
245
- // "// call ..." above a function call
246
- { comment: /\/\/\s*call\b/i, code: /^\s*\w[\w.]*\(/ },
247
- // "// declare ..." or "// define ..." or "// create ..." above const/let/var
248
- { comment: /\/\/\s*(declare|define|create|initialize)\b/i, code: /^\s*(const|let|var)\b/ },
249
- // "// check if ..." above an if statement
250
- { comment: /\/\/\s*check\s+if\b/i, code: /^\s*if\s*\(/ },
251
- // "// loop ..." or "// iterate ..." above for/while
252
- { comment: /\/\/\s*(loop|iterate|for each|foreach)\b/i, code: /^\s*(for|while)\b/ },
253
- // "// import ..." above an import
254
- { comment: /\/\/\s*import\b/i, code: /^\s*import\b/ },
255
- ];
256
- for (let i = 0; i < lines.length - 1; i++) {
257
- const commentLine = lines[i].trim();
258
- const nextLine = lines[i + 1];
259
- for (const { comment, code } of trivialCommentPatterns) {
260
- if (comment.test(commentLine) && code.test(nextLine)) {
261
- if (hasIgnoreComment(file, i + 1))
262
- continue;
263
- issues.push({
264
- rule: 'comment-contradiction',
265
- severity: 'warning',
266
- message: `Comment restates what the code already says. AI documents the obvious instead of the why.`,
267
- line: i + 1,
268
- column: 1,
269
- snippet: `${commentLine.slice(0, 60)}\n${nextLine.trim().slice(0, 60)}`,
270
- });
271
- break; // one issue per comment line max
272
- }
273
- }
274
- }
275
- return issues;
276
- }
1
+ // drift-ignore-file
2
+ export { detectHighComplexity } from './complexity.js';
3
+ export { detectDeepNesting, detectTooManyParams } from './nesting.js';
4
+ export { detectHighCoupling } from './coupling.js';
5
+ export { detectPromiseStyleMix } from './promise.js';
6
+ export { detectMagicNumbers } from './magic.js';
7
+ export { detectCommentContradiction } from './comments.js';
277
8
  //# sourceMappingURL=phase1-complexity.js.map
@@ -8,10 +8,6 @@ export declare function detectDeadFiles(sourceFiles: SourceFile[], allImportedPa
8
8
  severity: DriftIssue['severity'];
9
9
  weight: number;
10
10
  }>): Map<string, DriftIssue>;
11
- /**
12
- * Detect named exports that are never imported by any other file.
13
- * Barrel files (index.*) are excluded since their entire surface is the public API.
14
- */
15
11
  export declare function detectUnusedExports(sourceFiles: SourceFile[], allImportedNames: Map<string, Set<string>>, ruleWeights: Record<string, {
16
12
  severity: DriftIssue['severity'];
17
13
  weight: number;