@eduardbar/drift 0.3.0 → 0.5.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/ISSUE_TEMPLATE/bug_report.md +41 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +39 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +46 -0
- package/AGENTS.md +229 -0
- package/CHANGELOG.md +114 -0
- package/CODE_OF_CONDUCT.md +30 -0
- package/CONTRIBUTING.md +125 -0
- package/LICENSE +21 -0
- package/README.md +71 -7
- package/ROADMAP.md +213 -0
- package/assets/og-v030-linkedin.png +0 -0
- package/assets/og-v030-linkedin.svg +120 -0
- package/assets/og-v030-x.png +0 -0
- package/assets/og-v030-x.svg +94 -0
- package/content-v030.txt +165 -0
- package/dist/analyzer.d.ts +2 -2
- package/dist/analyzer.js +904 -2
- package/dist/cli.js +52 -3
- package/dist/config.d.ts +12 -0
- package/dist/config.js +40 -0
- package/dist/diff.d.ts +10 -0
- package/dist/diff.js +58 -0
- package/dist/git.d.ts +19 -0
- package/dist/git.js +84 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/printer.d.ts +2 -1
- package/dist/printer.js +128 -0
- package/dist/types.d.ts +43 -0
- package/package.json +9 -1
- package/src/analyzer.ts +993 -3
- package/src/cli.ts +56 -3
- package/src/config.ts +45 -0
- package/src/diff.ts +74 -0
- package/src/git.ts +98 -0
- package/src/index.ts +2 -1
- package/src/printer.ts +146 -1
- package/src/types.ts +56 -0
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,10 @@ import { writeFileSync } from 'node:fs';
|
|
|
4
4
|
import { resolve } from 'node:path';
|
|
5
5
|
import { analyzeProject } from './analyzer.js';
|
|
6
6
|
import { buildReport, formatMarkdown, formatAIOutput } from './reporter.js';
|
|
7
|
-
import { printConsole } from './printer.js';
|
|
7
|
+
import { printConsole, printDiff } from './printer.js';
|
|
8
|
+
import { loadConfig } from './config.js';
|
|
9
|
+
import { extractFilesAtRef, cleanupTempDir } from './git.js';
|
|
10
|
+
import { computeDiff } from './diff.js';
|
|
8
11
|
const program = new Command();
|
|
9
12
|
program
|
|
10
13
|
.name('drift')
|
|
@@ -18,10 +21,11 @@ program
|
|
|
18
21
|
.option('--ai', 'Output AI-optimized JSON for LLM consumption')
|
|
19
22
|
.option('--fix', 'Show fix suggestions for each issue')
|
|
20
23
|
.option('--min-score <n>', 'Exit with code 1 if overall score exceeds this threshold', '0')
|
|
21
|
-
.action((targetPath, options) => {
|
|
24
|
+
.action(async (targetPath, options) => {
|
|
22
25
|
const resolvedPath = resolve(targetPath ?? '.');
|
|
23
26
|
process.stderr.write(`\nScanning ${resolvedPath}...\n`);
|
|
24
|
-
const
|
|
27
|
+
const config = await loadConfig(resolvedPath);
|
|
28
|
+
const files = analyzeProject(resolvedPath, config);
|
|
25
29
|
process.stderr.write(` Found ${files.length} TypeScript file(s)\n\n`);
|
|
26
30
|
const report = buildReport(resolvedPath, files);
|
|
27
31
|
if (options.ai) {
|
|
@@ -46,5 +50,50 @@ program
|
|
|
46
50
|
process.exit(1);
|
|
47
51
|
}
|
|
48
52
|
});
|
|
53
|
+
program
|
|
54
|
+
.command('diff [ref]')
|
|
55
|
+
.description('Compare current state against a git ref (default: HEAD~1)')
|
|
56
|
+
.option('--json', 'Output raw JSON diff')
|
|
57
|
+
.action(async (ref, options) => {
|
|
58
|
+
const baseRef = ref ?? 'HEAD~1';
|
|
59
|
+
const projectPath = resolve('.');
|
|
60
|
+
let tempDir;
|
|
61
|
+
try {
|
|
62
|
+
process.stderr.write(`\nComputing diff: HEAD vs ${baseRef}...\n\n`);
|
|
63
|
+
// Scan current state
|
|
64
|
+
const config = await loadConfig(projectPath);
|
|
65
|
+
const currentFiles = analyzeProject(projectPath, config);
|
|
66
|
+
const currentReport = buildReport(projectPath, currentFiles);
|
|
67
|
+
// Extract base state from git
|
|
68
|
+
tempDir = extractFilesAtRef(projectPath, baseRef);
|
|
69
|
+
const baseFiles = analyzeProject(tempDir, config);
|
|
70
|
+
// Remap base file paths to match current project paths
|
|
71
|
+
// (temp dir paths → project paths for accurate comparison)
|
|
72
|
+
const baseReport = buildReport(tempDir, baseFiles);
|
|
73
|
+
const remappedBase = {
|
|
74
|
+
...baseReport,
|
|
75
|
+
files: baseReport.files.map(f => ({
|
|
76
|
+
...f,
|
|
77
|
+
path: f.path.replace(tempDir, projectPath),
|
|
78
|
+
})),
|
|
79
|
+
};
|
|
80
|
+
const diff = computeDiff(remappedBase, currentReport, baseRef);
|
|
81
|
+
if (options.json) {
|
|
82
|
+
process.stdout.write(JSON.stringify(diff, null, 2) + '\n');
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
printDiff(diff);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
90
|
+
process.stderr.write(`\n Error: ${message}\n\n`);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
if (tempDir)
|
|
95
|
+
cleanupTempDir(tempDir);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
49
98
|
program.parse();
|
|
50
99
|
//# sourceMappingURL=cli.js.map
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { DriftConfig } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Load drift.config.ts / .js / .json from the given project root.
|
|
4
|
+
* Returns undefined if no config file is found.
|
|
5
|
+
*
|
|
6
|
+
* Search order (first match wins):
|
|
7
|
+
* 1. drift.config.ts
|
|
8
|
+
* 2. drift.config.js
|
|
9
|
+
* 3. drift.config.json
|
|
10
|
+
*/
|
|
11
|
+
export declare function loadConfig(projectRoot: string): Promise<DriftConfig | undefined>;
|
|
12
|
+
//# sourceMappingURL=config.d.ts.map
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
/**
|
|
5
|
+
* Load drift.config.ts / .js / .json from the given project root.
|
|
6
|
+
* Returns undefined if no config file is found.
|
|
7
|
+
*
|
|
8
|
+
* Search order (first match wins):
|
|
9
|
+
* 1. drift.config.ts
|
|
10
|
+
* 2. drift.config.js
|
|
11
|
+
* 3. drift.config.json
|
|
12
|
+
*/
|
|
13
|
+
export async function loadConfig(projectRoot) {
|
|
14
|
+
const candidates = [
|
|
15
|
+
join(projectRoot, 'drift.config.ts'),
|
|
16
|
+
join(projectRoot, 'drift.config.js'),
|
|
17
|
+
join(projectRoot, 'drift.config.json'),
|
|
18
|
+
];
|
|
19
|
+
for (const candidate of candidates) {
|
|
20
|
+
if (!existsSync(candidate))
|
|
21
|
+
continue;
|
|
22
|
+
try {
|
|
23
|
+
const ext = candidate.split('.').pop();
|
|
24
|
+
if (ext === 'json') {
|
|
25
|
+
const { readFileSync } = await import('node:fs');
|
|
26
|
+
return JSON.parse(readFileSync(candidate, 'utf-8'));
|
|
27
|
+
}
|
|
28
|
+
// .ts / .js — dynamic import via file URL
|
|
29
|
+
const fileUrl = pathToFileURL(resolve(candidate)).href;
|
|
30
|
+
const mod = await import(fileUrl);
|
|
31
|
+
const config = mod.default ?? mod;
|
|
32
|
+
return config;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// drift-ignore: catch-swallow — config is optional; load failure is non-fatal
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=config.js.map
|
package/dist/diff.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { DriftReport, DriftDiff } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Compute the diff between two DriftReports.
|
|
4
|
+
*
|
|
5
|
+
* Issues are matched by (rule + line + column) as a unique key within a file.
|
|
6
|
+
* A "new" issue exists in `current` but not in `base`.
|
|
7
|
+
* A "resolved" issue exists in `base` but not in `current`.
|
|
8
|
+
*/
|
|
9
|
+
export declare function computeDiff(base: DriftReport, current: DriftReport, baseRef: string): DriftDiff;
|
|
10
|
+
//# sourceMappingURL=diff.d.ts.map
|
package/dist/diff.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compute the diff between two DriftReports.
|
|
3
|
+
*
|
|
4
|
+
* Issues are matched by (rule + line + column) as a unique key within a file.
|
|
5
|
+
* A "new" issue exists in `current` but not in `base`.
|
|
6
|
+
* A "resolved" issue exists in `base` but not in `current`.
|
|
7
|
+
*/
|
|
8
|
+
export function computeDiff(base, current, baseRef) {
|
|
9
|
+
const fileDiffs = [];
|
|
10
|
+
// Build a map of base files by path for O(1) lookup
|
|
11
|
+
const baseByPath = new Map(base.files.map(f => [f.path, f]));
|
|
12
|
+
const currentByPath = new Map(current.files.map(f => [f.path, f]));
|
|
13
|
+
// All unique paths across both reports
|
|
14
|
+
const allPaths = new Set([
|
|
15
|
+
...base.files.map(f => f.path),
|
|
16
|
+
...current.files.map(f => f.path),
|
|
17
|
+
]);
|
|
18
|
+
for (const filePath of allPaths) {
|
|
19
|
+
const baseFile = baseByPath.get(filePath);
|
|
20
|
+
const currentFile = currentByPath.get(filePath);
|
|
21
|
+
const scoreBefore = baseFile?.score ?? 0;
|
|
22
|
+
const scoreAfter = currentFile?.score ?? 0;
|
|
23
|
+
const scoreDelta = scoreAfter - scoreBefore;
|
|
24
|
+
const baseIssues = baseFile?.issues ?? [];
|
|
25
|
+
const currentIssues = currentFile?.issues ?? [];
|
|
26
|
+
// Issue identity key: rule + line + column
|
|
27
|
+
const issueKey = (i) => `${i.rule}:${i.line}:${i.column}`;
|
|
28
|
+
const baseKeys = new Set(baseIssues.map(issueKey));
|
|
29
|
+
const currentKeys = new Set(currentIssues.map(issueKey));
|
|
30
|
+
const newIssues = currentIssues.filter(i => !baseKeys.has(issueKey(i)));
|
|
31
|
+
const resolvedIssues = baseIssues.filter(i => !currentKeys.has(issueKey(i)));
|
|
32
|
+
// Only include files that have actual changes
|
|
33
|
+
if (scoreDelta !== 0 || newIssues.length > 0 || resolvedIssues.length > 0) {
|
|
34
|
+
fileDiffs.push({
|
|
35
|
+
path: filePath,
|
|
36
|
+
scoreBefore,
|
|
37
|
+
scoreAfter,
|
|
38
|
+
scoreDelta,
|
|
39
|
+
newIssues,
|
|
40
|
+
resolvedIssues,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Sort: most regressed first, then most improved last
|
|
45
|
+
fileDiffs.sort((a, b) => b.scoreDelta - a.scoreDelta);
|
|
46
|
+
return {
|
|
47
|
+
baseRef,
|
|
48
|
+
projectPath: current.targetPath,
|
|
49
|
+
scannedAt: new Date().toISOString(),
|
|
50
|
+
files: fileDiffs,
|
|
51
|
+
totalScoreBefore: base.totalScore,
|
|
52
|
+
totalScoreAfter: current.totalScore,
|
|
53
|
+
totalDelta: current.totalScore - base.totalScore,
|
|
54
|
+
newIssuesCount: fileDiffs.reduce((sum, f) => sum + f.newIssues.length, 0),
|
|
55
|
+
resolvedIssuesCount: fileDiffs.reduce((sum, f) => sum + f.resolvedIssues.length, 0),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=diff.js.map
|
package/dist/git.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract all TypeScript files from the project at a given git ref into a
|
|
3
|
+
* temporary directory. Returns the temp directory path.
|
|
4
|
+
*
|
|
5
|
+
* Uses `git ls-tree` to list files and `git show <ref>:<path>` to read each
|
|
6
|
+
* file — no checkout, no stash, no repo state mutation.
|
|
7
|
+
*
|
|
8
|
+
* Throws if the directory is not a git repo or the ref is invalid.
|
|
9
|
+
*/
|
|
10
|
+
export declare function extractFilesAtRef(projectPath: string, ref: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* Clean up a temporary directory created by extractFilesAtRef.
|
|
13
|
+
*/
|
|
14
|
+
export declare function cleanupTempDir(tempDir: string): void;
|
|
15
|
+
/**
|
|
16
|
+
* Get the short hash of a git ref for display purposes.
|
|
17
|
+
*/
|
|
18
|
+
export declare function resolveRefHash(projectPath: string, ref: string): string;
|
|
19
|
+
//# sourceMappingURL=git.d.ts.map
|
package/dist/git.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { mkdirSync, writeFileSync, rmSync, existsSync } from 'node:fs';
|
|
3
|
+
import { join, sep } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { randomUUID } from 'node:crypto';
|
|
6
|
+
/**
|
|
7
|
+
* Extract all TypeScript files from the project at a given git ref into a
|
|
8
|
+
* temporary directory. Returns the temp directory path.
|
|
9
|
+
*
|
|
10
|
+
* Uses `git ls-tree` to list files and `git show <ref>:<path>` to read each
|
|
11
|
+
* file — no checkout, no stash, no repo state mutation.
|
|
12
|
+
*
|
|
13
|
+
* Throws if the directory is not a git repo or the ref is invalid.
|
|
14
|
+
*/
|
|
15
|
+
export function extractFilesAtRef(projectPath, ref) {
|
|
16
|
+
// Verify git repo
|
|
17
|
+
try {
|
|
18
|
+
execSync('git rev-parse --git-dir', { cwd: projectPath, stdio: 'pipe' });
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
throw new Error(`Not a git repository: ${projectPath}`);
|
|
22
|
+
}
|
|
23
|
+
// Verify ref exists
|
|
24
|
+
try {
|
|
25
|
+
execSync(`git rev-parse --verify ${ref}`, { cwd: projectPath, stdio: 'pipe' });
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
throw new Error(`Invalid git ref: '${ref}'. Run 'git log --oneline' to see available commits.`);
|
|
29
|
+
}
|
|
30
|
+
// List all .ts files tracked at this ref (excluding .d.ts)
|
|
31
|
+
let fileList;
|
|
32
|
+
try {
|
|
33
|
+
fileList = execSync(`git ls-tree -r --name-only ${ref}`, { cwd: projectPath, encoding: 'utf-8', stdio: 'pipe' });
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
throw new Error(`Failed to list files at ref '${ref}'`);
|
|
37
|
+
}
|
|
38
|
+
const tsFiles = fileList
|
|
39
|
+
.split('\n')
|
|
40
|
+
.map(f => f.trim())
|
|
41
|
+
.filter(f => f.endsWith('.ts') && !f.endsWith('.d.ts'));
|
|
42
|
+
if (tsFiles.length === 0) {
|
|
43
|
+
throw new Error(`No TypeScript files found at ref '${ref}'`);
|
|
44
|
+
}
|
|
45
|
+
// Create temp directory
|
|
46
|
+
const tempDir = join(tmpdir(), `drift-diff-${randomUUID()}`);
|
|
47
|
+
mkdirSync(tempDir, { recursive: true });
|
|
48
|
+
// Extract each file
|
|
49
|
+
for (const filePath of tsFiles) {
|
|
50
|
+
let content;
|
|
51
|
+
try {
|
|
52
|
+
content = execSync(`git show ${ref}:${filePath}`, { cwd: projectPath, encoding: 'utf-8', stdio: 'pipe' });
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// File may not exist at this ref — skip
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const destPath = join(tempDir, filePath.split('/').join(sep));
|
|
59
|
+
const destDir = destPath.substring(0, destPath.lastIndexOf(sep));
|
|
60
|
+
mkdirSync(destDir, { recursive: true });
|
|
61
|
+
writeFileSync(destPath, content, 'utf-8');
|
|
62
|
+
}
|
|
63
|
+
return tempDir;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Clean up a temporary directory created by extractFilesAtRef.
|
|
67
|
+
*/
|
|
68
|
+
export function cleanupTempDir(tempDir) {
|
|
69
|
+
if (existsSync(tempDir)) {
|
|
70
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Get the short hash of a git ref for display purposes.
|
|
75
|
+
*/
|
|
76
|
+
export function resolveRefHash(projectPath, ref) {
|
|
77
|
+
try {
|
|
78
|
+
return execSync(`git rev-parse --short ${ref}`, { cwd: projectPath, encoding: 'utf-8', stdio: 'pipe' }).trim();
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return ref;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=git.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { analyzeProject, analyzeFile } from './analyzer.js';
|
|
2
2
|
export { buildReport, formatMarkdown } from './reporter.js';
|
|
3
|
-
export
|
|
3
|
+
export { computeDiff } from './diff.js';
|
|
4
|
+
export type { DriftReport, FileReport, DriftIssue, DriftDiff, FileDiff } from './types.js';
|
|
4
5
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
package/dist/printer.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { DriftReport } from './types.js';
|
|
1
|
+
import type { DriftReport, DriftDiff } from './types.js';
|
|
2
2
|
export declare function printConsole(report: DriftReport, options?: {
|
|
3
3
|
showFix?: boolean;
|
|
4
4
|
}): void;
|
|
5
|
+
export declare function printDiff(diff: DriftDiff): void;
|
|
5
6
|
//# sourceMappingURL=printer.d.ts.map
|
package/dist/printer.js
CHANGED
|
@@ -33,6 +33,84 @@ function formatFixSuggestion(issue) {
|
|
|
33
33
|
'Consolidate with existing function',
|
|
34
34
|
'Or rename to clarify different behavior',
|
|
35
35
|
],
|
|
36
|
+
'high-complexity': [
|
|
37
|
+
'Extract each branch into a named function',
|
|
38
|
+
'Use early returns to reduce nesting and branching',
|
|
39
|
+
'Consider a strategy pattern or lookup table for switch-heavy logic',
|
|
40
|
+
],
|
|
41
|
+
'deep-nesting': [
|
|
42
|
+
'Invert conditions and return early instead of nesting',
|
|
43
|
+
'Extract inner blocks into separate functions',
|
|
44
|
+
'Flatten promise chains with async/await',
|
|
45
|
+
],
|
|
46
|
+
'too-many-params': [
|
|
47
|
+
'Group related params into an options object: foo({ a, b, c, d, e })',
|
|
48
|
+
'Consider if this function is doing too many things',
|
|
49
|
+
],
|
|
50
|
+
'high-coupling': [
|
|
51
|
+
'Group related imports into a single module',
|
|
52
|
+
'Consider if this file has too many responsibilities',
|
|
53
|
+
'Extract a sub-module that encapsulates some of these dependencies',
|
|
54
|
+
],
|
|
55
|
+
'promise-style-mix': [
|
|
56
|
+
'Pick one style and use it consistently: async/await is preferred',
|
|
57
|
+
'Convert .then()/.catch() chains to async/await',
|
|
58
|
+
],
|
|
59
|
+
'magic-number': [
|
|
60
|
+
'Extract to a named constant: const MAX_RETRIES = 3',
|
|
61
|
+
'Use an enum for related numeric values',
|
|
62
|
+
],
|
|
63
|
+
'comment-contradiction': [
|
|
64
|
+
'Remove the comment — the code already says what it does',
|
|
65
|
+
'Replace with a comment explaining WHY, not what: // retry because upstream is flaky',
|
|
66
|
+
],
|
|
67
|
+
'unused-export': [
|
|
68
|
+
"Remove the export keyword if it's only used internally",
|
|
69
|
+
'Or delete the declaration entirely if it serves no purpose',
|
|
70
|
+
],
|
|
71
|
+
'dead-file': [
|
|
72
|
+
'Delete the file if it is no longer needed',
|
|
73
|
+
'Or import it from an entry point if it should be active',
|
|
74
|
+
],
|
|
75
|
+
'unused-dependency': [
|
|
76
|
+
'Remove it from package.json: npm uninstall <pkg>',
|
|
77
|
+
'Or verify it is used transitively and document why it is kept',
|
|
78
|
+
],
|
|
79
|
+
'circular-dependency': [
|
|
80
|
+
'Introduce an abstraction (interface or shared module) that both files depend on',
|
|
81
|
+
'Move shared logic to a third file that neither of the cyclic modules imports',
|
|
82
|
+
'Use dependency injection to break the compile-time dependency',
|
|
83
|
+
],
|
|
84
|
+
'layer-violation': [
|
|
85
|
+
'Move the import to a layer that is allowed to access this dependency',
|
|
86
|
+
'Introduce a port/interface in the domain layer to invert the dependency (Dependency Inversion Principle)',
|
|
87
|
+
'Or adjust the layer rules in drift.config.ts if this import is intentional',
|
|
88
|
+
],
|
|
89
|
+
'cross-boundary-import': [
|
|
90
|
+
"Import from the module's public API barrel (index.ts) instead of internal paths",
|
|
91
|
+
'Or add the module to allowedExternalImports in drift.config.ts if this is intentional',
|
|
92
|
+
'Consider using dependency injection or an event bus to decouple the modules',
|
|
93
|
+
],
|
|
94
|
+
'over-commented': [
|
|
95
|
+
'Remove comments that restate what the code already expresses clearly',
|
|
96
|
+
'Keep only comments that explain WHY, not WHAT — prefer self-documenting names',
|
|
97
|
+
],
|
|
98
|
+
'hardcoded-config': [
|
|
99
|
+
'Move the value to an environment variable: process.env.YOUR_VAR',
|
|
100
|
+
'Or extract it to a config file / constants module imported at the top',
|
|
101
|
+
],
|
|
102
|
+
'inconsistent-error-handling': [
|
|
103
|
+
'Pick one style (async/await + try/catch is preferred) and apply it consistently',
|
|
104
|
+
'Avoid mixing .then()/.catch() with await in the same file',
|
|
105
|
+
],
|
|
106
|
+
'unnecessary-abstraction': [
|
|
107
|
+
'Inline the abstraction if it has only one implementation and is never reused',
|
|
108
|
+
'Or document why the extension point exists (e.g., future plugin system)',
|
|
109
|
+
],
|
|
110
|
+
'naming-inconsistency': [
|
|
111
|
+
'Pick one naming convention (camelCase for variables/functions, PascalCase for types)',
|
|
112
|
+
'Rename snake_case identifiers to camelCase to match TypeScript conventions',
|
|
113
|
+
],
|
|
36
114
|
};
|
|
37
115
|
return suggestions[issue.rule] ?? ['Review and fix manually'];
|
|
38
116
|
}
|
|
@@ -104,4 +182,54 @@ export function printConsole(report, options) {
|
|
|
104
182
|
console.log();
|
|
105
183
|
}
|
|
106
184
|
}
|
|
185
|
+
export function printDiff(diff) {
|
|
186
|
+
const { totalDelta, totalScoreBefore, totalScoreAfter, newIssuesCount, resolvedIssuesCount } = diff;
|
|
187
|
+
const deltaSign = totalDelta > 0 ? '+' : '';
|
|
188
|
+
const deltaColor = totalDelta > 0 ? kleur.red : totalDelta < 0 ? kleur.green : kleur.white;
|
|
189
|
+
const baseGrade = scoreToGrade(totalScoreBefore);
|
|
190
|
+
const headGrade = scoreToGrade(totalScoreAfter);
|
|
191
|
+
console.log();
|
|
192
|
+
console.log(kleur.bold(' drift diff') + kleur.gray(` — comparing HEAD vs ${diff.baseRef}`));
|
|
193
|
+
console.log(' ' + '─'.repeat(50));
|
|
194
|
+
console.log();
|
|
195
|
+
console.log(` Score ${kleur.bold(String(totalScoreBefore))} ${baseGrade.badge} → ` +
|
|
196
|
+
`${kleur.bold(String(totalScoreAfter))} ${headGrade.badge} ` +
|
|
197
|
+
deltaColor(`(${deltaSign}${totalDelta})`));
|
|
198
|
+
console.log();
|
|
199
|
+
if (newIssuesCount > 0) {
|
|
200
|
+
console.log(` ${kleur.red(`▲ ${newIssuesCount} new issue${newIssuesCount !== 1 ? 's' : ''} introduced`)}`);
|
|
201
|
+
}
|
|
202
|
+
if (resolvedIssuesCount > 0) {
|
|
203
|
+
console.log(` ${kleur.green(`▼ ${resolvedIssuesCount} issue${resolvedIssuesCount !== 1 ? 's' : ''} resolved`)}`);
|
|
204
|
+
}
|
|
205
|
+
if (newIssuesCount === 0 && resolvedIssuesCount === 0) {
|
|
206
|
+
console.log(` ${kleur.gray('No issue changes detected')}`);
|
|
207
|
+
}
|
|
208
|
+
if (diff.files.length === 0) {
|
|
209
|
+
console.log();
|
|
210
|
+
console.log(` ${kleur.gray('No file-level changes detected')}`);
|
|
211
|
+
console.log();
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
console.log();
|
|
215
|
+
console.log(' ' + '─'.repeat(50));
|
|
216
|
+
console.log();
|
|
217
|
+
for (const file of diff.files) {
|
|
218
|
+
const rel = file.path.replace(/\\/g, '/').split('/').pop() ?? file.path;
|
|
219
|
+
const fileDeltaSign = file.scoreDelta > 0 ? '+' : '';
|
|
220
|
+
const fileDeltaColor = file.scoreDelta > 0 ? kleur.red : kleur.green;
|
|
221
|
+
console.log(` ${kleur.bold(rel)}` +
|
|
222
|
+
` ${kleur.gray(`${file.scoreBefore} → ${file.scoreAfter}`)}` +
|
|
223
|
+
` ${fileDeltaColor(`${fileDeltaSign}${file.scoreDelta}`)}`);
|
|
224
|
+
for (const issue of file.newIssues) {
|
|
225
|
+
console.log(` ${kleur.red('+')} ${severityIcon(issue.severity)} ` +
|
|
226
|
+
`${kleur.yellow(issue.rule)} ${kleur.gray(`L${issue.line}`)} ${issue.message}`);
|
|
227
|
+
}
|
|
228
|
+
for (const issue of file.resolvedIssues) {
|
|
229
|
+
console.log(` ${kleur.green('-')} ${severityIcon(issue.severity)} ` +
|
|
230
|
+
`${kleur.yellow(issue.rule)} ${kleur.gray(`L${issue.line}`)} ${issue.message}`);
|
|
231
|
+
}
|
|
232
|
+
console.log();
|
|
233
|
+
}
|
|
234
|
+
}
|
|
107
235
|
//# sourceMappingURL=printer.js.map
|
package/dist/types.d.ts
CHANGED
|
@@ -52,4 +52,47 @@ export interface AIIssue {
|
|
|
52
52
|
fix_suggestion: string;
|
|
53
53
|
effort: 'low' | 'medium' | 'high';
|
|
54
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Layer definition for architectural boundary enforcement.
|
|
57
|
+
*/
|
|
58
|
+
export interface LayerDefinition {
|
|
59
|
+
name: string;
|
|
60
|
+
patterns: string[];
|
|
61
|
+
canImportFrom: string[];
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Module boundary definition for cross-boundary enforcement.
|
|
65
|
+
*/
|
|
66
|
+
export interface ModuleBoundary {
|
|
67
|
+
name: string;
|
|
68
|
+
root: string;
|
|
69
|
+
allowedExternalImports?: string[];
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Optional project-level configuration for drift.
|
|
73
|
+
* Place in drift.config.ts (or .js / .json) at the project root.
|
|
74
|
+
*/
|
|
75
|
+
export interface DriftConfig {
|
|
76
|
+
layers?: LayerDefinition[];
|
|
77
|
+
modules?: ModuleBoundary[];
|
|
78
|
+
}
|
|
79
|
+
export interface FileDiff {
|
|
80
|
+
path: string;
|
|
81
|
+
scoreBefore: number;
|
|
82
|
+
scoreAfter: number;
|
|
83
|
+
scoreDelta: number;
|
|
84
|
+
newIssues: DriftIssue[];
|
|
85
|
+
resolvedIssues: DriftIssue[];
|
|
86
|
+
}
|
|
87
|
+
export interface DriftDiff {
|
|
88
|
+
baseRef: string;
|
|
89
|
+
projectPath: string;
|
|
90
|
+
scannedAt: string;
|
|
91
|
+
files: FileDiff[];
|
|
92
|
+
totalScoreBefore: number;
|
|
93
|
+
totalScoreAfter: number;
|
|
94
|
+
totalDelta: number;
|
|
95
|
+
newIssuesCount: number;
|
|
96
|
+
resolvedIssuesCount: number;
|
|
97
|
+
}
|
|
55
98
|
//# sourceMappingURL=types.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eduardbar/drift",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Detect silent technical debt left by AI-generated code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -23,6 +23,14 @@
|
|
|
23
23
|
],
|
|
24
24
|
"author": "eduardbar",
|
|
25
25
|
"license": "MIT",
|
|
26
|
+
"homepage": "https://github.com/eduardbar/drift#readme",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/eduardbar/drift.git"
|
|
30
|
+
},
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/eduardbar/drift/issues"
|
|
33
|
+
},
|
|
26
34
|
"dependencies": {
|
|
27
35
|
"commander": "^14.0.3",
|
|
28
36
|
"kleur": "^4.1.5",
|