@eduardbar/drift 1.0.0 → 1.2.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 (105) 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/.github/workflows/review-pr.yml +61 -0
  5. package/AGENTS.md +53 -11
  6. package/README.md +106 -1
  7. package/dist/analyzer.d.ts +6 -2
  8. package/dist/analyzer.js +116 -3
  9. package/dist/badge.js +40 -22
  10. package/dist/ci.js +32 -18
  11. package/dist/cli.js +179 -6
  12. package/dist/diff.d.ts +0 -7
  13. package/dist/diff.js +26 -25
  14. package/dist/fix.d.ts +4 -0
  15. package/dist/fix.js +59 -47
  16. package/dist/git/trend.js +1 -0
  17. package/dist/git.d.ts +0 -9
  18. package/dist/git.js +25 -19
  19. package/dist/index.d.ts +7 -1
  20. package/dist/index.js +4 -0
  21. package/dist/map.d.ts +4 -0
  22. package/dist/map.js +191 -0
  23. package/dist/metrics.d.ts +4 -0
  24. package/dist/metrics.js +176 -0
  25. package/dist/plugins.d.ts +6 -0
  26. package/dist/plugins.js +74 -0
  27. package/dist/printer.js +20 -0
  28. package/dist/report.js +34 -0
  29. package/dist/reporter.js +85 -2
  30. package/dist/review.d.ts +15 -0
  31. package/dist/review.js +80 -0
  32. package/dist/rules/comments.d.ts +4 -0
  33. package/dist/rules/comments.js +45 -0
  34. package/dist/rules/complexity.d.ts +4 -0
  35. package/dist/rules/complexity.js +51 -0
  36. package/dist/rules/coupling.d.ts +4 -0
  37. package/dist/rules/coupling.js +19 -0
  38. package/dist/rules/magic.d.ts +4 -0
  39. package/dist/rules/magic.js +33 -0
  40. package/dist/rules/nesting.d.ts +5 -0
  41. package/dist/rules/nesting.js +82 -0
  42. package/dist/rules/phase0-basic.js +14 -7
  43. package/dist/rules/phase1-complexity.d.ts +6 -30
  44. package/dist/rules/phase1-complexity.js +7 -276
  45. package/dist/rules/phase2-crossfile.d.ts +0 -4
  46. package/dist/rules/phase2-crossfile.js +52 -39
  47. package/dist/rules/phase3-arch.d.ts +0 -8
  48. package/dist/rules/phase3-arch.js +26 -23
  49. package/dist/rules/phase3-configurable.d.ts +6 -0
  50. package/dist/rules/phase3-configurable.js +97 -0
  51. package/dist/rules/phase8-semantic.d.ts +0 -5
  52. package/dist/rules/phase8-semantic.js +30 -29
  53. package/dist/rules/promise.d.ts +4 -0
  54. package/dist/rules/promise.js +24 -0
  55. package/dist/saas.d.ts +83 -0
  56. package/dist/saas.js +321 -0
  57. package/dist/snapshot.d.ts +19 -0
  58. package/dist/snapshot.js +119 -0
  59. package/dist/types.d.ts +75 -0
  60. package/dist/utils.d.ts +2 -1
  61. package/dist/utils.js +1 -0
  62. package/docs/AGENTS.md +146 -0
  63. package/docs/PRD.md +157 -0
  64. package/package.json +1 -1
  65. package/packages/eslint-plugin-drift/src/index.ts +1 -1
  66. package/packages/vscode-drift/package.json +1 -1
  67. package/packages/vscode-drift/src/analyzer.ts +2 -0
  68. package/packages/vscode-drift/src/code-actions.ts +53 -0
  69. package/packages/vscode-drift/src/extension.ts +98 -63
  70. package/packages/vscode-drift/src/statusbar.ts +13 -5
  71. package/packages/vscode-drift/src/treeview.ts +2 -0
  72. package/src/analyzer.ts +144 -12
  73. package/src/badge.ts +38 -16
  74. package/src/ci.ts +38 -17
  75. package/src/cli.ts +206 -7
  76. package/src/diff.ts +36 -30
  77. package/src/fix.ts +77 -53
  78. package/src/git/trend.ts +3 -2
  79. package/src/git.ts +31 -22
  80. package/src/index.ts +31 -1
  81. package/src/map.ts +219 -0
  82. package/src/metrics.ts +200 -0
  83. package/src/plugins.ts +76 -0
  84. package/src/printer.ts +20 -0
  85. package/src/report.ts +35 -0
  86. package/src/reporter.ts +95 -2
  87. package/src/review.ts +98 -0
  88. package/src/rules/comments.ts +56 -0
  89. package/src/rules/complexity.ts +57 -0
  90. package/src/rules/coupling.ts +23 -0
  91. package/src/rules/magic.ts +38 -0
  92. package/src/rules/nesting.ts +88 -0
  93. package/src/rules/phase0-basic.ts +14 -7
  94. package/src/rules/phase1-complexity.ts +8 -302
  95. package/src/rules/phase2-crossfile.ts +68 -40
  96. package/src/rules/phase3-arch.ts +34 -30
  97. package/src/rules/phase3-configurable.ts +132 -0
  98. package/src/rules/phase8-semantic.ts +33 -29
  99. package/src/rules/promise.ts +29 -0
  100. package/src/saas.ts +433 -0
  101. package/src/snapshot.ts +175 -0
  102. package/src/types.ts +81 -1
  103. package/src/utils.ts +3 -1
  104. package/tests/new-features.test.ts +180 -0
  105. package/tests/saas-foundation.test.ts +107 -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
@@ -0,0 +1,61 @@
1
+ name: Drift PR Review
2
+
3
+ on:
4
+ pull_request:
5
+ types: [opened, synchronize, reopened]
6
+
7
+ permissions:
8
+ contents: read
9
+ pull-requests: write
10
+
11
+ jobs:
12
+ drift-review:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - name: Checkout
16
+ uses: actions/checkout@v4
17
+ with:
18
+ fetch-depth: 0
19
+
20
+ - name: Setup Node.js
21
+ uses: actions/setup-node@v4
22
+ with:
23
+ node-version: '20'
24
+ cache: 'npm'
25
+
26
+ - name: Install dependencies
27
+ run: npm ci
28
+
29
+ - name: Generate drift review markdown
30
+ run: npx @eduardbar/drift review --base "origin/${{ github.base_ref }}" --comment > drift-review.md
31
+
32
+ - name: Post or update PR comment (non-fork only)
33
+ if: github.event.pull_request.head.repo.fork == false
34
+ env:
35
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
36
+ PR_NUMBER: ${{ github.event.pull_request.number }}
37
+ REPO: ${{ github.repository }}
38
+ run: |
39
+ COMMENT_BODY="<!-- drift-review -->"
40
+ COMMENT_BODY+=$'\n'
41
+ COMMENT_BODY+="$(cat drift-review.md)"
42
+
43
+ EXISTING_ID=$(gh api "repos/$REPO/issues/$PR_NUMBER/comments" --jq '.[] | select(.user.login == "github-actions[bot]") | select(.body | contains("<!-- drift-review -->")) | .id' | sed -n '1p')
44
+
45
+ if [ -n "$EXISTING_ID" ]; then
46
+ gh api -X PATCH "repos/$REPO/issues/comments/$EXISTING_ID" -f "body=$COMMENT_BODY"
47
+ else
48
+ gh api -X POST "repos/$REPO/issues/$PR_NUMBER/comments" -f "body=$COMMENT_BODY"
49
+ fi
50
+
51
+ - name: Fallback summary for fork PRs
52
+ if: github.event.pull_request.head.repo.fork == true
53
+ run: |
54
+ {
55
+ echo "## drift review"
56
+ echo
57
+ cat drift-review.md
58
+ } >> "$GITHUB_STEP_SUMMARY"
59
+
60
+ - name: Enforce drift threshold
61
+ run: npx @eduardbar/drift review --base "origin/${{ github.base_ref }}" --fail-on 5 --comment > /dev/null
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,47 @@ 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. When layer config is present, the SVG also highlights cycle edges and layer violations.
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
+ Edge legend in SVG:
166
+ - Gray: normal dependency
167
+ - Orange: cycle edge
168
+ - Red: layer violation edge
169
+
170
+ ---
171
+
124
172
  ### `drift report [path]`
125
173
 
126
174
  Generate a self-contained HTML report. No server required — open in any browser.
@@ -220,6 +268,49 @@ drift blame file --top 10
220
268
 
221
269
  ---
222
270
 
271
+ ### `drift fix [path]`
272
+
273
+ Auto-fix safe issues with explicit preview/write modes.
274
+
275
+ ```bash
276
+ drift fix ./src --preview
277
+ drift fix ./src --write
278
+ drift fix ./src --write --yes
279
+ drift fix ./src --dry-run # alias of --preview
280
+ ```
281
+
282
+ | Flag | Description |
283
+ |------|-------------|
284
+ | `--preview` | Preview before/after without writing files |
285
+ | `--write` | Apply fixes to disk |
286
+ | `--dry-run` | Backward-compatible alias for preview mode |
287
+ | `--yes` | Skip interactive confirmation for write mode |
288
+
289
+ ---
290
+
291
+ ### `drift cloud`
292
+
293
+ Local SaaS foundations backed by `.drift-cloud/store.json`.
294
+
295
+ ```bash
296
+ drift cloud ingest ./src --workspace acme --user u-123 --repo webapp
297
+ drift cloud summary
298
+ drift cloud summary --json
299
+ drift cloud dashboard --output drift-cloud-dashboard.html
300
+ ```
301
+
302
+ **Subcommands:**
303
+
304
+ | Command | Description |
305
+ |---------|-------------|
306
+ | `drift cloud ingest [path] --workspace <id> --user <id> [--repo <name>] [--store <file>]` | Scans the path and stores one SaaS snapshot |
307
+ | `drift cloud summary [--json] [--store <file>]` | Shows users/workspaces/repos usage and runs per month |
308
+ | `drift cloud dashboard [--output <file>] [--store <file>]` | Generates an HTML dashboard with trends and hotspots |
309
+
310
+ `drift cloud` ships with a free-until-7,500 strategy and configurable guardrails for the free phase: max runs per workspace per month, max repos per workspace, and retention window.
311
+
312
+ ---
313
+
223
314
  ## Rules
224
315
 
225
316
  26 rules across three severity levels. All run automatically unless marked as requiring configuration.
@@ -277,6 +368,12 @@ drift runs with zero configuration. Architectural rules (`layer-violation`, `cro
277
368
  import type { DriftConfig } from '@eduardbar/drift'
278
369
 
279
370
  export default {
371
+ plugins: ['drift-plugin-example'],
372
+ architectureRules: {
373
+ controllerNoDb: true,
374
+ serviceNoHttp: true,
375
+ maxFunctionLines: 80,
376
+ },
280
377
  layers: [
281
378
  { name: 'domain', patterns: ['src/domain/**'], canImportFrom: [] },
282
379
  { name: 'app', patterns: ['src/app/**'], canImportFrom: ['domain'] },
@@ -345,6 +442,14 @@ jobs:
345
442
 
346
443
  `drift ci` emits `::error` and `::warning` annotations that appear inline in the PR diff and writes a formatted summary to `$GITHUB_STEP_SUMMARY`. Use this when you want visibility beyond a pass/fail exit code.
347
444
 
445
+ ### Auto PR comment with `drift review`
446
+
447
+ The repository includes `.github/workflows/review-pr.yml`, which:
448
+ - generates a PR-ready markdown comment from `drift review --comment`
449
+ - updates a single sticky comment (`<!-- drift-review -->`) on non-fork PRs
450
+ - falls back to `$GITHUB_STEP_SUMMARY` for fork PRs
451
+ - enforces a score delta threshold with `--fail-on`
452
+
348
453
  ---
349
454
 
350
455
  ## drift-ignore
@@ -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) {