@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,61 @@
1
+ # drift-scan Action
2
+
3
+ Scan your TypeScript project for AI-generated technical debt in CI.
4
+
5
+ ## Usage
6
+
7
+ ```yaml
8
+ - name: Check drift score
9
+ uses: eduardbar/drift@v1
10
+ with:
11
+ path: ./src
12
+ min-score: 60
13
+ ```
14
+
15
+ ## Inputs
16
+
17
+ | Input | Description | Default |
18
+ |-------|-------------|---------|
19
+ | `path` | Path to scan | `.` |
20
+ | `min-score` | Fail if score exceeds this | `80` |
21
+ | `fail-on-threshold` | Whether to fail on threshold | `true` |
22
+ | `version` | drift version to use | `latest` |
23
+
24
+ ## Outputs
25
+
26
+ | Output | Description |
27
+ |--------|-------------|
28
+ | `score` | Project drift score (0-100) |
29
+ | `grade` | Grade: CLEAN / LOW / MODERATE / HIGH / CRITICAL |
30
+
31
+ ## Example: PR gate
32
+
33
+ ```yaml
34
+ name: Drift Check
35
+ on: [pull_request]
36
+
37
+ jobs:
38
+ drift:
39
+ runs-on: ubuntu-latest
40
+ steps:
41
+ - uses: actions/checkout@v4
42
+ - uses: eduardbar/drift@v1
43
+ with:
44
+ path: ./src
45
+ min-score: 60
46
+ fail-on-threshold: true
47
+ ```
48
+
49
+ ## Example: capture outputs
50
+
51
+ ```yaml
52
+ - name: Scan drift
53
+ id: drift
54
+ uses: eduardbar/drift@v1
55
+ with:
56
+ path: ./src
57
+ fail-on-threshold: false
58
+
59
+ - name: Print results
60
+ run: echo "Score ${{ steps.drift.outputs.score }}/100 — ${{ steps.drift.outputs.grade }}"
61
+ ```
@@ -0,0 +1,65 @@
1
+ name: 'Drift — Technical Debt Scanner'
2
+ description: 'Scan TypeScript projects for AI-generated technical debt and enforce score thresholds'
3
+ author: 'eduardbar'
4
+
5
+ branding:
6
+ icon: 'activity'
7
+ color: 'purple'
8
+
9
+ inputs:
10
+ path:
11
+ description: 'Path to scan'
12
+ required: false
13
+ default: '.'
14
+ min-score:
15
+ description: 'Fail if project score exceeds this threshold (0-100)'
16
+ required: false
17
+ default: '80'
18
+ fail-on-threshold:
19
+ description: 'Whether to fail the action if threshold is exceeded'
20
+ required: false
21
+ default: 'true'
22
+ version:
23
+ description: 'Version of @eduardbar/drift to use'
24
+ required: false
25
+ default: 'latest'
26
+
27
+ outputs:
28
+ score:
29
+ description: 'The overall drift score (0-100)'
30
+ value: ${{ steps.scan.outputs.score }}
31
+ grade:
32
+ description: 'The drift grade (CLEAN / LOW / MODERATE / HIGH / CRITICAL)'
33
+ value: ${{ steps.scan.outputs.grade }}
34
+
35
+ runs:
36
+ using: 'composite'
37
+ steps:
38
+ - name: Install drift
39
+ shell: bash
40
+ run: npm install -g @eduardbar/drift@${{ inputs.version }}
41
+
42
+ - name: Run drift scan
43
+ id: scan
44
+ shell: bash
45
+ run: |
46
+ TMPFILE=$(mktemp)
47
+
48
+ # Run scan — write JSON to tmp, progress to stderr (already separate)
49
+ drift scan ${{ inputs.path }} --ai > "$TMPFILE" 2>/dev/null || true
50
+
51
+ # Extract score and grade from AIOutput JSON
52
+ SCORE=$(node -e "const d=JSON.parse(require('fs').readFileSync('$TMPFILE','utf8')); console.log(d.summary.score)")
53
+ GRADE=$(node -e "const d=JSON.parse(require('fs').readFileSync('$TMPFILE','utf8')); console.log(d.summary.grade)")
54
+
55
+ rm "$TMPFILE"
56
+
57
+ echo "score=$SCORE" >> "$GITHUB_OUTPUT"
58
+ echo "grade=$GRADE" >> "$GITHUB_OUTPUT"
59
+
60
+ echo "Drift Score: $SCORE/100 ($GRADE)"
61
+
62
+ if [ "${{ inputs.fail-on-threshold }}" = "true" ] && [ "$SCORE" -gt "${{ inputs.min-score }}" ]; then
63
+ echo "::error::Drift score $SCORE/100 exceeds threshold of ${{ inputs.min-score }}"
64
+ exit 1
65
+ fi
@@ -3,6 +3,8 @@ name: Publish VS Code Extension
3
3
  on:
4
4
  release:
5
5
  types: [published]
6
+ tags:
7
+ - 'vscode-v*'
6
8
  workflow_dispatch:
7
9
  inputs:
8
10
  version:
@@ -39,7 +41,7 @@ jobs:
39
41
  if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
40
42
  TAG_VERSION="${{ inputs.version }}"
41
43
  else
42
- TAG_VERSION="${GITHUB_REF#refs/tags/v}"
44
+ TAG_VERSION="${GITHUB_REF#refs/tags/vscode-v}"
43
45
  fi
44
46
  PKG_VERSION=$(node -p "require('./package.json').version")
45
47
  if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then
package/AGENTS.md CHANGED
@@ -20,6 +20,7 @@ Publicado en npm como `@eduardbar/drift`. MIT.
20
20
  | `kleur ^4` | Colores en consola (sin dependencias) |
21
21
  | `typescript ^5.9` | Dev — compilación |
22
22
  | `@types/node ^25` | Dev — tipos Node.js |
23
+ | `vitest ^4` | Testing |
23
24
 
24
25
  **Runtime:** Node.js 18+, ES Modules (`"type": "module"`).
25
26
 
@@ -32,18 +33,47 @@ drift/
32
33
  ├── bin/
33
34
  │ └── drift.js ← wrapper cross-platform (Windows npx fix)
34
35
  ├── src/
35
- │ ├── types.ts interfaces: DriftIssue, FileReport, DriftReport, AIOutput
36
- │ ├── analyzer.ts motor AST + 10 reglas de detección + drift-ignore
37
- │ ├── reporter.ts ← buildReport(), formatMarkdown(), formatAIOutput()
38
- │ ├── printer.ts ← salida consola con colores y score bar ASCII
39
- │ ├── utils.ts ← scoreToGrade, severityIcon, scoreBar
40
- │ ├── index.ts ← re-exports públicos (librería)
41
- └── cli.ts ← entry point Commander.js
42
- ├── assets/
43
- │ ├── og.svg / og.pngimagen OG original
44
- │ ├── og-v030-linkedin.svg/png imagen post LinkedIn v0.3.0
45
- └── og-v030-x.svg/png imagen hilo X v0.3.0
36
+ │ ├── analyzer.ts motor AST + 26 reglas + drift-ignore
37
+ │ ├── types.ts interfaces: DriftIssue, FileReport, DriftReport, AIOutput
38
+ │ ├── reporter.ts ← buildReport(), formatMarkdown(), formatAIOutput()
39
+ │ ├── printer.ts ← salida consola con colores y score bar ASCII
40
+ │ ├── utils.ts ← scoreToGrade, severityIcon, scoreBar
41
+ │ ├── index.ts ← re-exports públicos (librería)
42
+ ├── cli.ts ← entry point Commander.js
43
+ ├── config.ts ← drift.config.ts support
44
+ │ ├── fix.tsdrift fix command
45
+ │ ├── ci.ts drift ci command
46
+ ├── diff.ts drift diff command
47
+ │ ├── report.ts ← drift report command
48
+ │ ├── badge.ts ← drift badge command
49
+ │ ├── snapshot.ts ← drift snapshot command
50
+ │ ├── git.ts ← re-exports git analyzers
51
+ │ ├── git/
52
+ │ │ ├── trend.ts ← drift trend (historial de scores)
53
+ │ │ ├── blame.ts ← drift blame (atribución de deuda)
54
+ │ │ └── helpers.ts
55
+ │ └── rules/ ← reglas modularizadas por fase
56
+ │ ├── phase0-basic.ts
57
+ │ ├── phase1-complexity.ts
58
+ │ ├── phase2-crossfile.ts ← dead-file, unused-export, unused-dependency
59
+ │ ├── phase3-arch.ts ← circular-dependency, layer-violation
60
+ │ ├── phase5-ai.ts
61
+ │ ├── phase8-semantic.ts ← semantic-duplication
62
+ │ ├── complexity.ts
63
+ │ ├── coupling.ts
64
+ │ ├── nesting.ts
65
+ │ ├── promise.ts
66
+ │ ├── magic.ts
67
+ │ ├── comments.ts
68
+ │ └── shared.ts
69
+ ├── packages/
70
+ │ ├── eslint-plugin-drift/ ← ESLint plugin oficial
71
+ │ └── vscode-drift/ ← VS Code extension
46
72
  ├── dist/ ← output tsc (no editar a mano)
73
+ ├── assets/
74
+ │ ├── og.svg / og.png
75
+ │ ├── og-v030-linkedin.svg/png
76
+ │ └── og-v030-x.svg/png
47
77
  ├── .github/workflows/publish.yml
48
78
  ├── package.json
49
79
  ├── tsconfig.json
@@ -58,6 +88,8 @@ drift/
58
88
  npm run build # tsc — compila src/ → dist/
59
89
  npm run dev # tsc --watch
60
90
  npm start # node dist/cli.js (desarrollo local)
91
+ npm test # vitest run
92
+ npm run test:watch # vitest (watch mode)
61
93
  ```
62
94
 
63
95
  **Pre-publicación:** `prepublishOnly` corre `build` automáticamente.
@@ -226,6 +258,16 @@ Sin esto, Windows no ejecuta el shebang correctamente con ES modules.
226
258
 
227
259
  ---
228
260
 
261
+ ## Estado actual (feb 2026)
262
+
263
+ - **Versión publicada:** `1.0.0`
264
+ - **Branch:** `master`, sincronizado con `origin`
265
+ - **Self-scan score:** 5/100 (LOW)
266
+ - **Top issues:** 51× magic-number, 2× deep-nesting, 2× catch-swallow
267
+ - **26 reglas activas** organizadas en fases
268
+
269
+ ---
270
+
229
271
  ## Convenciones de código
230
272
 
231
273
  - Todo en TypeScript — sin `any` explícito (drift se corre sobre sí mismo)
package/README.md CHANGED
@@ -10,7 +10,7 @@ Detect technical debt in AI-generated TypeScript code. One command. Zero config.
10
10
  ![ts-morph](https://img.shields.io/badge/powered%20by-ts--morph-6366f1.svg)
11
11
  ![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)
12
12
 
13
- [Why](#why) · [Installation](#installation) · [Commands](#commands) · [Rules](#rules) · [Score](#score) · [Configuration](#configuration) · [CI Integration](#ci-integration) · [drift-ignore](#drift-ignore) · [Contributing](#contributing)
13
+ [Why](#why) · [Installation](#installation) · [Product Docs](#product-docs) · [Commands](#commands) · [Rules](#rules) · [Score](#score) · [Configuration](#configuration) · [CI Integration](#ci-integration) · [drift-ignore](#drift-ignore) · [Contributing](#contributing)
14
14
 
15
15
  ---
16
16
 
@@ -47,6 +47,13 @@ npm install --save-dev @eduardbar/drift
47
47
 
48
48
  ---
49
49
 
50
+ ## Product Docs
51
+
52
+ - Product requirements and roadmap: [`docs/PRD.md`](./docs/PRD.md)
53
+ - Contributor/agent workflow guide: [`docs/AGENTS.md`](./docs/AGENTS.md)
54
+
55
+ ---
56
+
50
57
  ## Commands
51
58
 
52
59
  ### `drift scan [path]`
@@ -121,6 +128,42 @@ Shows score delta, issues introduced, and issues resolved since the given ref.
121
128
 
122
129
  ---
123
130
 
131
+ ### `drift review`
132
+
133
+ Review drift against a git base ref and output a PR-ready markdown comment.
134
+
135
+ ```bash
136
+ drift review --base origin/main
137
+ drift review --base main --comment
138
+ drift review --base HEAD~3 --json
139
+ drift review --base origin/main --fail-on 5
140
+ ```
141
+
142
+ | Flag | Description |
143
+ |------|-------------|
144
+ | `--base <ref>` | Git base ref to compare against (default: `origin/main`) |
145
+ | `--json` | Output structured review JSON |
146
+ | `--comment` | Print only the markdown body for PR comments |
147
+ | `--fail-on <n>` | Exit code 1 when score delta is greater than or equal to `n` |
148
+
149
+ ---
150
+
151
+ ### `drift map [path]`
152
+
153
+ Generate an `architecture.svg` map with inferred layer dependencies.
154
+
155
+ ```bash
156
+ drift map
157
+ drift map ./src
158
+ drift map ./src --output docs/architecture.svg
159
+ ```
160
+
161
+ | Flag | Description |
162
+ |------|-------------|
163
+ | `--output <file>` | Output path for the SVG file (default: `architecture.svg`) |
164
+
165
+ ---
166
+
124
167
  ### `drift report [path]`
125
168
 
126
169
  Generate a self-contained HTML report. No server required — open in any browser.
@@ -220,6 +263,24 @@ drift blame file --top 10
220
263
 
221
264
  ---
222
265
 
266
+ ### `drift fix [path]`
267
+
268
+ Auto-fix safe issues with explicit preview/write modes.
269
+
270
+ ```bash
271
+ drift fix ./src --preview
272
+ drift fix ./src --write
273
+ drift fix ./src --dry-run # alias of --preview
274
+ ```
275
+
276
+ | Flag | Description |
277
+ |------|-------------|
278
+ | `--preview` | Preview before/after without writing files |
279
+ | `--write` | Apply fixes to disk |
280
+ | `--dry-run` | Backward-compatible alias for preview mode |
281
+
282
+ ---
283
+
223
284
  ## Rules
224
285
 
225
286
  26 rules across three severity levels. All run automatically unless marked as requiring configuration.
@@ -277,6 +338,12 @@ drift runs with zero configuration. Architectural rules (`layer-violation`, `cro
277
338
  import type { DriftConfig } from '@eduardbar/drift'
278
339
 
279
340
  export default {
341
+ plugins: ['drift-plugin-example'],
342
+ architectureRules: {
343
+ controllerNoDb: true,
344
+ serviceNoHttp: true,
345
+ maxFunctionLines: 80,
346
+ },
280
347
  layers: [
281
348
  { name: 'domain', patterns: ['src/domain/**'], canImportFrom: [] },
282
349
  { name: 'app', patterns: ['src/app/**'], canImportFrom: ['domain'] },
@@ -1,10 +1,14 @@
1
- import type { DriftIssue, FileReport, DriftConfig } from './types.js';
1
+ import type { DriftIssue, FileReport, DriftConfig, LoadedPlugin } from './types.js';
2
2
  export { TrendAnalyzer } from './git/trend.js';
3
3
  export { BlameAnalyzer } from './git/blame.js';
4
4
  export declare const RULE_WEIGHTS: Record<string, {
5
5
  severity: DriftIssue['severity'];
6
6
  weight: number;
7
7
  }>;
8
- export declare function analyzeFile(file: import('ts-morph').SourceFile): FileReport;
8
+ export declare function analyzeFile(file: import('ts-morph').SourceFile, options?: DriftConfig | {
9
+ config?: DriftConfig;
10
+ loadedPlugins?: LoadedPlugin[];
11
+ projectRoot?: string;
12
+ }): FileReport;
9
13
  export declare function analyzeProject(targetPath: string, config?: DriftConfig): FileReport[];
10
14
  //# sourceMappingURL=analyzer.d.ts.map
package/dist/analyzer.js CHANGED
@@ -4,11 +4,18 @@ import { Project } from 'ts-morph';
4
4
  // Rules
5
5
  import { isFileIgnored } from './rules/shared.js';
6
6
  import { detectLargeFile, detectLargeFunctions, detectDebugLeftovers, detectDeadCode, detectDuplicateFunctionNames, detectAnyAbuse, detectCatchSwallow, detectMissingReturnTypes, } from './rules/phase0-basic.js';
7
- import { detectHighComplexity, detectDeepNesting, detectTooManyParams, detectHighCoupling, detectPromiseStyleMix, detectMagicNumbers, detectCommentContradiction, } from './rules/phase1-complexity.js';
7
+ import { detectHighComplexity } from './rules/complexity.js';
8
+ import { detectDeepNesting, detectTooManyParams } from './rules/nesting.js';
9
+ import { detectHighCoupling } from './rules/coupling.js';
10
+ import { detectPromiseStyleMix } from './rules/promise.js';
11
+ import { detectMagicNumbers } from './rules/magic.js';
12
+ import { detectCommentContradiction } from './rules/comments.js';
8
13
  import { detectDeadFiles, detectUnusedExports, detectUnusedDependencies, } from './rules/phase2-crossfile.js';
9
14
  import { detectCircularDependencies, detectLayerViolations, detectCrossBoundaryImports, } from './rules/phase3-arch.js';
15
+ import { detectControllerNoDb, detectServiceNoHttp, detectMaxFunctionLines, } from './rules/phase3-configurable.js';
10
16
  import { detectOverCommented, detectHardcodedConfig, detectInconsistentErrorHandling, detectUnnecessaryAbstraction, detectNamingInconsistency, } from './rules/phase5-ai.js';
11
17
  import { collectFunctions, fingerprintFunction, calculateScore, } from './rules/phase8-semantic.js';
18
+ import { loadPlugins } from './plugins.js';
12
19
  // Git analyzers (re-exported as part of the public API)
13
20
  export { TrendAnalyzer } from './git/trend.js';
14
21
  export { BlameAnalyzer } from './git/blame.js';
@@ -41,19 +48,95 @@ export const RULE_WEIGHTS = {
41
48
  // Phase 3b/c: layer and module boundary enforcement (require drift.config.ts)
42
49
  'layer-violation': { severity: 'error', weight: 16 },
43
50
  'cross-boundary-import': { severity: 'warning', weight: 10 },
51
+ 'controller-no-db': { severity: 'warning', weight: 11 },
52
+ 'service-no-http': { severity: 'warning', weight: 11 },
53
+ 'max-function-lines': { severity: 'warning', weight: 9 },
44
54
  // Phase 5: AI authorship heuristics
45
55
  'over-commented': { severity: 'info', weight: 4 },
46
56
  'hardcoded-config': { severity: 'warning', weight: 10 },
47
57
  'inconsistent-error-handling': { severity: 'warning', weight: 8 },
48
58
  'unnecessary-abstraction': { severity: 'warning', weight: 7 },
49
59
  'naming-inconsistency': { severity: 'warning', weight: 6 },
60
+ 'ai-code-smell': { severity: 'warning', weight: 12 },
50
61
  // Phase 8: semantic duplication
51
62
  'semantic-duplication': { severity: 'warning', weight: 12 },
63
+ 'plugin-error': { severity: 'warning', weight: 4 },
52
64
  };
65
+ const AI_SMELL_SIGNALS = new Set([
66
+ 'over-commented',
67
+ 'hardcoded-config',
68
+ 'inconsistent-error-handling',
69
+ 'unnecessary-abstraction',
70
+ 'naming-inconsistency',
71
+ 'comment-contradiction',
72
+ 'promise-style-mix',
73
+ 'any-abuse',
74
+ ]);
75
+ function detectAICodeSmell(issues, filePath) {
76
+ const signalCounts = new Map();
77
+ for (const issue of issues) {
78
+ if (!AI_SMELL_SIGNALS.has(issue.rule))
79
+ continue;
80
+ signalCounts.set(issue.rule, (signalCounts.get(issue.rule) ?? 0) + 1);
81
+ }
82
+ const totalSignals = [...signalCounts.values()].reduce((sum, count) => sum + count, 0);
83
+ if (totalSignals < 3)
84
+ return [];
85
+ const triggers = [...signalCounts.entries()]
86
+ .sort((a, b) => b[1] - a[1])
87
+ .slice(0, 3)
88
+ .map(([rule, count]) => `${rule} x${count}`);
89
+ return [{
90
+ rule: 'ai-code-smell',
91
+ severity: 'warning',
92
+ message: `Aggregated AI smell signals detected (${totalSignals}): ${triggers.join(', ')}`,
93
+ line: 1,
94
+ column: 1,
95
+ snippet: path.basename(filePath),
96
+ }];
97
+ }
98
+ function runPluginRules(file, loadedPlugins, config, projectRoot) {
99
+ if (loadedPlugins.length === 0)
100
+ return [];
101
+ const context = {
102
+ projectRoot,
103
+ filePath: file.getFilePath(),
104
+ config,
105
+ };
106
+ const issues = [];
107
+ for (const loaded of loadedPlugins) {
108
+ for (const rule of loaded.plugin.rules) {
109
+ try {
110
+ const detected = rule.detect(file, context) ?? [];
111
+ for (const issue of detected) {
112
+ issues.push({
113
+ ...issue,
114
+ rule: issue.rule || `${loaded.plugin.name}/${rule.name}`,
115
+ severity: issue.severity ?? (rule.severity ?? 'warning'),
116
+ });
117
+ }
118
+ }
119
+ catch (error) {
120
+ issues.push({
121
+ rule: 'plugin-error',
122
+ severity: 'warning',
123
+ message: `Plugin '${loaded.id}' rule '${rule.name}' failed: ${error instanceof Error ? error.message : String(error)}`,
124
+ line: 1,
125
+ column: 1,
126
+ snippet: file.getBaseName(),
127
+ });
128
+ }
129
+ }
130
+ }
131
+ return issues;
132
+ }
53
133
  // ---------------------------------------------------------------------------
54
134
  // Per-file analysis
55
135
  // ---------------------------------------------------------------------------
56
- export function analyzeFile(file) {
136
+ export function analyzeFile(file, options) {
137
+ const normalizedOptions = (options && typeof options === 'object' && ('config' in options || 'loadedPlugins' in options || 'projectRoot' in options))
138
+ ? options
139
+ : { config: (options && typeof options === 'object' ? options : undefined) };
57
140
  if (isFileIgnored(file)) {
58
141
  return {
59
142
  path: file.getFilePath(),
@@ -84,7 +167,14 @@ export function analyzeFile(file) {
84
167
  ...detectInconsistentErrorHandling(file),
85
168
  ...detectUnnecessaryAbstraction(file),
86
169
  ...detectNamingInconsistency(file),
170
+ // Configurable architecture rules
171
+ ...detectControllerNoDb(file, normalizedOptions?.config),
172
+ ...detectServiceNoHttp(file, normalizedOptions?.config),
173
+ ...detectMaxFunctionLines(file, normalizedOptions?.config),
174
+ // Plugin rules
175
+ ...runPluginRules(file, normalizedOptions?.loadedPlugins ?? [], normalizedOptions?.config, normalizedOptions?.projectRoot ?? path.dirname(file.getFilePath())),
87
176
  ];
177
+ issues.push(...detectAICodeSmell(issues, file.getFilePath()));
88
178
  return {
89
179
  path: file.getFilePath(),
90
180
  issues,
@@ -113,8 +203,13 @@ export function analyzeProject(targetPath, config) {
113
203
  `!${targetPath}/**/*.spec.*`,
114
204
  ]);
115
205
  const sourceFiles = project.getSourceFiles();
206
+ const pluginRuntime = loadPlugins(targetPath, config?.plugins);
116
207
  // Phase 1: per-file analysis
117
- const reports = sourceFiles.map(analyzeFile);
208
+ const reports = sourceFiles.map((file) => analyzeFile(file, {
209
+ config,
210
+ loadedPlugins: pluginRuntime.plugins,
211
+ projectRoot: targetPath,
212
+ }));
118
213
  const reportByPath = new Map();
119
214
  for (const r of reports)
120
215
  reportByPath.set(r.path, r);
@@ -172,6 +267,24 @@ export function analyzeProject(targetPath, config) {
172
267
  }
173
268
  }
174
269
  }
270
+ // Plugin load failures are surfaced as synthetic report entries.
271
+ if (pluginRuntime.errors.length > 0) {
272
+ for (const err of pluginRuntime.errors) {
273
+ const pluginIssue = {
274
+ rule: 'plugin-error',
275
+ severity: 'warning',
276
+ message: `Failed to load plugin '${err.pluginId}': ${err.message}`,
277
+ line: 1,
278
+ column: 1,
279
+ snippet: err.pluginId,
280
+ };
281
+ reports.push({
282
+ path: path.join(targetPath, '.drift-plugin-errors', `${err.pluginId}.plugin`),
283
+ issues: [pluginIssue],
284
+ score: calculateScore([pluginIssue], RULE_WEIGHTS),
285
+ });
286
+ }
287
+ }
175
288
  // ── Phase 2: dead-file + unused-export + unused-dependency ─────────────────
176
289
  const deadFiles = detectDeadFiles(sourceFiles, allImportedPaths, RULE_WEIGHTS);
177
290
  for (const [sfPath, issue] of deadFiles) {
package/dist/badge.js CHANGED
@@ -1,23 +1,41 @@
1
1
  const LEFT_WIDTH = 47;
2
2
  const CHAR_WIDTH = 7;
3
3
  const PADDING = 16;
4
+ const SVG_SCALE = 10;
5
+ const GRADE_THRESHOLDS = {
6
+ LOW: 20,
7
+ MODERATE: 45,
8
+ HIGH: 70,
9
+ };
10
+ const GRADE_COLORS = {
11
+ LOW: '#4c1',
12
+ MODERATE: '#dfb317',
13
+ HIGH: '#fe7d37',
14
+ CRITICAL: '#e05d44',
15
+ };
16
+ const GRADE_LABELS = {
17
+ LOW: 'LOW',
18
+ MODERATE: 'MODERATE',
19
+ HIGH: 'HIGH',
20
+ CRITICAL: 'CRITICAL',
21
+ };
4
22
  function scoreColor(score) {
5
- if (score < 20)
6
- return '#4c1';
7
- if (score < 45)
8
- return '#dfb317';
9
- if (score < 70)
10
- return '#fe7d37';
11
- return '#e05d44';
23
+ if (score < GRADE_THRESHOLDS.LOW)
24
+ return GRADE_COLORS.LOW;
25
+ if (score < GRADE_THRESHOLDS.MODERATE)
26
+ return GRADE_COLORS.MODERATE;
27
+ if (score < GRADE_THRESHOLDS.HIGH)
28
+ return GRADE_COLORS.HIGH;
29
+ return GRADE_COLORS.CRITICAL;
12
30
  }
13
31
  function scoreLabel(score) {
14
- if (score < 20)
15
- return 'LOW';
16
- if (score < 45)
17
- return 'MODERATE';
18
- if (score < 70)
19
- return 'HIGH';
20
- return 'CRITICAL';
32
+ if (score < GRADE_THRESHOLDS.LOW)
33
+ return GRADE_LABELS.LOW;
34
+ if (score < GRADE_THRESHOLDS.MODERATE)
35
+ return GRADE_LABELS.MODERATE;
36
+ if (score < GRADE_THRESHOLDS.HIGH)
37
+ return GRADE_LABELS.HIGH;
38
+ return GRADE_LABELS.CRITICAL;
21
39
  }
22
40
  function rightWidth(text) {
23
41
  return text.length * CHAR_WIDTH + PADDING;
@@ -29,10 +47,10 @@ export function generateBadge(score) {
29
47
  const totalWidth = LEFT_WIDTH + rWidth;
30
48
  const leftCenterX = LEFT_WIDTH / 2;
31
49
  const rightCenterX = LEFT_WIDTH + rWidth / 2;
32
- // shields.io pattern: font-size="110" + scale(.1) = effective 11px
33
- // all X/Y coords are ×10
34
- const leftTextWidth = (LEFT_WIDTH - 10) * 10;
35
- const rightTextWidth = (rWidth - PADDING) * 10;
50
+ const leftTextWidth = (LEFT_WIDTH - PADDING) * SVG_SCALE;
51
+ const rightTextWidth = (rWidth - PADDING) * SVG_SCALE;
52
+ const leftCenterXScaled = leftCenterX * SVG_SCALE;
53
+ const rightCenterXScaled = rightCenterX * SVG_SCALE;
36
54
  return `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${totalWidth}" height="20">
37
55
  <linearGradient id="s" x2="0" y2="100%">
38
56
  <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
@@ -47,10 +65,10 @@ export function generateBadge(score) {
47
65
  <rect width="${totalWidth}" height="20" fill="url(#s)"/>
48
66
  </g>
49
67
  <g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="110">
50
- <text x="${leftCenterX * 10}" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="${leftTextWidth}" lengthAdjust="spacing">drift</text>
51
- <text x="${leftCenterX * 10}" y="140" transform="scale(.1)" textLength="${leftTextWidth}" lengthAdjust="spacing">drift</text>
52
- <text x="${rightCenterX * 10}" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="${rightTextWidth}" lengthAdjust="spacing">${valueText}</text>
53
- <text x="${rightCenterX * 10}" y="140" transform="scale(.1)" textLength="${rightTextWidth}" lengthAdjust="spacing">${valueText}</text>
68
+ <text x="${leftCenterXScaled}" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="${leftTextWidth}" lengthAdjust="spacing">drift</text>
69
+ <text x="${leftCenterXScaled}" y="140" transform="scale(.1)" textLength="${leftTextWidth}" lengthAdjust="spacing">drift</text>
70
+ <text x="${rightCenterXScaled}" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="${rightTextWidth}" lengthAdjust="spacing">${valueText}</text>
71
+ <text x="${rightCenterXScaled}" y="140" transform="scale(.1)" textLength="${rightTextWidth}" lengthAdjust="spacing">${valueText}</text>
54
72
  </g>
55
73
  </svg>`;
56
74
  }