@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.
- package/.github/actions/drift-scan/README.md +61 -0
- package/.github/actions/drift-scan/action.yml +65 -0
- package/.github/workflows/publish-vscode.yml +3 -1
- package/.github/workflows/review-pr.yml +61 -0
- package/AGENTS.md +53 -11
- package/README.md +106 -1
- package/dist/analyzer.d.ts +6 -2
- package/dist/analyzer.js +116 -3
- package/dist/badge.js +40 -22
- package/dist/ci.js +32 -18
- package/dist/cli.js +179 -6
- package/dist/diff.d.ts +0 -7
- package/dist/diff.js +26 -25
- package/dist/fix.d.ts +4 -0
- package/dist/fix.js +59 -47
- package/dist/git/trend.js +1 -0
- package/dist/git.d.ts +0 -9
- package/dist/git.js +25 -19
- package/dist/index.d.ts +7 -1
- package/dist/index.js +4 -0
- package/dist/map.d.ts +4 -0
- package/dist/map.js +191 -0
- package/dist/metrics.d.ts +4 -0
- package/dist/metrics.js +176 -0
- package/dist/plugins.d.ts +6 -0
- package/dist/plugins.js +74 -0
- package/dist/printer.js +20 -0
- package/dist/report.js +34 -0
- package/dist/reporter.js +85 -2
- package/dist/review.d.ts +15 -0
- package/dist/review.js +80 -0
- package/dist/rules/comments.d.ts +4 -0
- package/dist/rules/comments.js +45 -0
- package/dist/rules/complexity.d.ts +4 -0
- package/dist/rules/complexity.js +51 -0
- package/dist/rules/coupling.d.ts +4 -0
- package/dist/rules/coupling.js +19 -0
- package/dist/rules/magic.d.ts +4 -0
- package/dist/rules/magic.js +33 -0
- package/dist/rules/nesting.d.ts +5 -0
- package/dist/rules/nesting.js +82 -0
- package/dist/rules/phase0-basic.js +14 -7
- package/dist/rules/phase1-complexity.d.ts +6 -30
- package/dist/rules/phase1-complexity.js +7 -276
- package/dist/rules/phase2-crossfile.d.ts +0 -4
- package/dist/rules/phase2-crossfile.js +52 -39
- package/dist/rules/phase3-arch.d.ts +0 -8
- package/dist/rules/phase3-arch.js +26 -23
- package/dist/rules/phase3-configurable.d.ts +6 -0
- package/dist/rules/phase3-configurable.js +97 -0
- package/dist/rules/phase8-semantic.d.ts +0 -5
- package/dist/rules/phase8-semantic.js +30 -29
- package/dist/rules/promise.d.ts +4 -0
- package/dist/rules/promise.js +24 -0
- package/dist/saas.d.ts +83 -0
- package/dist/saas.js +321 -0
- package/dist/snapshot.d.ts +19 -0
- package/dist/snapshot.js +119 -0
- package/dist/types.d.ts +75 -0
- package/dist/utils.d.ts +2 -1
- package/dist/utils.js +1 -0
- package/docs/AGENTS.md +146 -0
- package/docs/PRD.md +157 -0
- package/package.json +1 -1
- package/packages/eslint-plugin-drift/src/index.ts +1 -1
- package/packages/vscode-drift/package.json +1 -1
- package/packages/vscode-drift/src/analyzer.ts +2 -0
- package/packages/vscode-drift/src/code-actions.ts +53 -0
- package/packages/vscode-drift/src/extension.ts +98 -63
- package/packages/vscode-drift/src/statusbar.ts +13 -5
- package/packages/vscode-drift/src/treeview.ts +2 -0
- package/src/analyzer.ts +144 -12
- package/src/badge.ts +38 -16
- package/src/ci.ts +38 -17
- package/src/cli.ts +206 -7
- package/src/diff.ts +36 -30
- package/src/fix.ts +77 -53
- package/src/git/trend.ts +3 -2
- package/src/git.ts +31 -22
- package/src/index.ts +31 -1
- package/src/map.ts +219 -0
- package/src/metrics.ts +200 -0
- package/src/plugins.ts +76 -0
- package/src/printer.ts +20 -0
- package/src/report.ts +35 -0
- package/src/reporter.ts +95 -2
- package/src/review.ts +98 -0
- package/src/rules/comments.ts +56 -0
- package/src/rules/complexity.ts +57 -0
- package/src/rules/coupling.ts +23 -0
- package/src/rules/magic.ts +38 -0
- package/src/rules/nesting.ts +88 -0
- package/src/rules/phase0-basic.ts +14 -7
- package/src/rules/phase1-complexity.ts +8 -302
- package/src/rules/phase2-crossfile.ts +68 -40
- package/src/rules/phase3-arch.ts +34 -30
- package/src/rules/phase3-configurable.ts +132 -0
- package/src/rules/phase8-semantic.ts +33 -29
- package/src/rules/promise.ts +29 -0
- package/src/saas.ts +433 -0
- package/src/snapshot.ts +175 -0
- package/src/types.ts +81 -1
- package/src/utils.ts +3 -1
- package/tests/new-features.test.ts +180 -0
- 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
|
-
│ ├──
|
|
36
|
-
│ ├──
|
|
37
|
-
│ ├── reporter.ts
|
|
38
|
-
│ ├── printer.ts
|
|
39
|
-
│ ├── utils.ts
|
|
40
|
-
│ ├── index.ts
|
|
41
|
-
│
|
|
42
|
-
├──
|
|
43
|
-
│ ├──
|
|
44
|
-
│ ├──
|
|
45
|
-
│
|
|
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.ts ← drift 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
|

|
|
11
11
|

|
|
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
|
package/dist/analyzer.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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) {
|