@vibedrift/cli 0.5.4 → 0.5.6
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/README.md +6 -3
- package/dist/index.js +410 -41
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -41,12 +41,15 @@ vibedrift . --deep # uses one of your 3 monthly deep scans
|
|
|
41
41
|
- **Dependency health** — phantom deps, missing deps, undocumented env vars
|
|
42
42
|
- **Code quality** — cyclomatic complexity, dead code, TODO density, long functions, unclear naming
|
|
43
43
|
|
|
44
|
-
### Layer 1.5 — Cross-File Drift Detection (
|
|
45
|
-
- **Architectural consistency** —
|
|
44
|
+
### Layer 1.5 — Cross-File Drift Detection (8 detectors)
|
|
45
|
+
- **Architectural consistency** — error handling, data access, DI, and config patterns across all source files
|
|
46
46
|
- **Convention oscillation** — naming conventions, file naming across the whole project
|
|
47
47
|
- **Security consistency** — auth middleware, input validation, rate limiting across routes
|
|
48
48
|
- **Semantic duplication** — functions that do the same thing under different names in different files
|
|
49
|
-
- **Phantom scaffolding** — CRUD endpoints
|
|
49
|
+
- **Phantom scaffolding** — exported types and CRUD endpoints that are never used
|
|
50
|
+
- **Import style** — relative paths vs path aliases, default vs named imports
|
|
51
|
+
- **Export style** — default exports vs named-only exports per file
|
|
52
|
+
- **Async patterns** — async/await vs .then() chains consistency
|
|
50
53
|
|
|
51
54
|
### Layer 1.7 — Code DNA Engine (5 modules, runs locally)
|
|
52
55
|
- **Semantic fingerprinting** — normalizes function bodies and hashes them. Identical hashes = exact semantic duplicates at confidence 1.0
|
package/dist/index.js
CHANGED
|
@@ -5178,11 +5178,14 @@ init_esm_shims();
|
|
|
5178
5178
|
// src/drift/types.ts
|
|
5179
5179
|
init_esm_shims();
|
|
5180
5180
|
var DRIFT_WEIGHTS = {
|
|
5181
|
-
architectural_consistency:
|
|
5182
|
-
security_posture:
|
|
5183
|
-
semantic_duplication:
|
|
5184
|
-
naming_conventions:
|
|
5185
|
-
phantom_scaffolding:
|
|
5181
|
+
architectural_consistency: 16,
|
|
5182
|
+
security_posture: 14,
|
|
5183
|
+
semantic_duplication: 14,
|
|
5184
|
+
naming_conventions: 12,
|
|
5185
|
+
phantom_scaffolding: 12,
|
|
5186
|
+
import_style: 12,
|
|
5187
|
+
export_style: 10,
|
|
5188
|
+
async_patterns: 10
|
|
5186
5189
|
};
|
|
5187
5190
|
|
|
5188
5191
|
// src/drift/architectural-contradiction.ts
|
|
@@ -5200,9 +5203,6 @@ function extractEvidence(content, pattern, maxResults = 3) {
|
|
|
5200
5203
|
}
|
|
5201
5204
|
return evidence;
|
|
5202
5205
|
}
|
|
5203
|
-
function isHandlerOrController(path2) {
|
|
5204
|
-
return /(?:handler|controller|route|endpoint|view|api|resource|page|action)/i.test(path2) && !/(?:repository|repo|store|dal|model|query|migration|test|spec|mock)/i.test(path2);
|
|
5205
|
-
}
|
|
5206
5206
|
function detectDataAccess(file) {
|
|
5207
5207
|
const results = [];
|
|
5208
5208
|
const c = file.content;
|
|
@@ -5220,7 +5220,7 @@ function detectDataAccess(file) {
|
|
|
5220
5220
|
results.push({ pattern: "direct_db", evidence: dbEvidence });
|
|
5221
5221
|
}
|
|
5222
5222
|
const httpEvidence = extractEvidence(c, /\b(?:fetch|axios|http\.(?:Get|Post|Put)|requests\.(?:get|post))\s*\(/g);
|
|
5223
|
-
if (httpEvidence.length > 0
|
|
5223
|
+
if (httpEvidence.length > 0) {
|
|
5224
5224
|
results.push({ pattern: "http_client", evidence: httpEvidence });
|
|
5225
5225
|
}
|
|
5226
5226
|
return results;
|
|
@@ -5280,13 +5280,19 @@ function detectDIPattern(file) {
|
|
|
5280
5280
|
}
|
|
5281
5281
|
return results;
|
|
5282
5282
|
}
|
|
5283
|
+
function isAnalyzableSource(path2) {
|
|
5284
|
+
if (/(?:test|spec|mock|fixture|__test__|__mocks__|\.test\.|\.spec\.)/i.test(path2)) return false;
|
|
5285
|
+
if (/(?:\.config\.|\.d\.ts$|node_modules|dist\/|build\/)/i.test(path2)) return false;
|
|
5286
|
+
return true;
|
|
5287
|
+
}
|
|
5283
5288
|
function buildProfile(file) {
|
|
5284
|
-
if (!file.language
|
|
5289
|
+
if (!file.language) return null;
|
|
5290
|
+
if (!isAnalyzableSource(file.path)) return null;
|
|
5285
5291
|
const dataAccess = detectDataAccess(file);
|
|
5286
5292
|
const errorHandling = detectErrorHandling(file);
|
|
5287
5293
|
const config = detectConfigPattern(file);
|
|
5288
5294
|
const di = detectDIPattern(file);
|
|
5289
|
-
if (dataAccess.length === 0 && errorHandling.length === 0) return null;
|
|
5295
|
+
if (dataAccess.length === 0 && errorHandling.length === 0 && config.length === 0) return null;
|
|
5290
5296
|
return { file: file.path, language: file.language, dataAccess, errorHandling, config, di };
|
|
5291
5297
|
}
|
|
5292
5298
|
function detectFilePattern(patterns) {
|
|
@@ -6285,6 +6291,341 @@ var phantomScaffolding = {
|
|
|
6285
6291
|
}
|
|
6286
6292
|
};
|
|
6287
6293
|
|
|
6294
|
+
// src/drift/import-consistency.ts
|
|
6295
|
+
init_esm_shims();
|
|
6296
|
+
function isSourceFile(path2) {
|
|
6297
|
+
if (/(?:test|spec|mock|fixture|__test__|__mocks__|\.test\.|\.spec\.)/i.test(path2)) return false;
|
|
6298
|
+
if (/(?:\.config\.|\.d\.ts$|node_modules|dist\/|build\/)/i.test(path2)) return false;
|
|
6299
|
+
return true;
|
|
6300
|
+
}
|
|
6301
|
+
function analyzeImports(file) {
|
|
6302
|
+
if (!file.language || !["javascript", "typescript"].includes(file.language)) return null;
|
|
6303
|
+
if (!isSourceFile(file.path)) return null;
|
|
6304
|
+
const lines = file.content.split("\n");
|
|
6305
|
+
let relativeCount = 0;
|
|
6306
|
+
let aliasCount = 0;
|
|
6307
|
+
let defaultCount = 0;
|
|
6308
|
+
let namedCount = 0;
|
|
6309
|
+
const evidence = [];
|
|
6310
|
+
for (let i = 0; i < lines.length; i++) {
|
|
6311
|
+
const line = lines[i].trim();
|
|
6312
|
+
const importMatch = line.match(/^import\s+(?!type\s)/);
|
|
6313
|
+
if (!importMatch) continue;
|
|
6314
|
+
const fromMatch = line.match(/from\s+["']([^"']+)["']/);
|
|
6315
|
+
if (!fromMatch) continue;
|
|
6316
|
+
const importPath = fromMatch[1];
|
|
6317
|
+
if (!importPath.startsWith(".") && !importPath.startsWith("@/") && !importPath.startsWith("~/")) continue;
|
|
6318
|
+
if (importPath.startsWith("./") || importPath.startsWith("../")) {
|
|
6319
|
+
relativeCount++;
|
|
6320
|
+
} else if (importPath.startsWith("@/") || importPath.startsWith("~/")) {
|
|
6321
|
+
aliasCount++;
|
|
6322
|
+
}
|
|
6323
|
+
if (/^import\s+\{/.test(line)) {
|
|
6324
|
+
namedCount++;
|
|
6325
|
+
} else if (/^import\s+\w+\s+from/.test(line) || /^import\s+\w+,\s*\{/.test(line)) {
|
|
6326
|
+
defaultCount++;
|
|
6327
|
+
}
|
|
6328
|
+
if (evidence.length < 3) {
|
|
6329
|
+
evidence.push({ line: i + 1, code: line });
|
|
6330
|
+
}
|
|
6331
|
+
}
|
|
6332
|
+
const totalLocalImports = relativeCount + aliasCount;
|
|
6333
|
+
if (totalLocalImports < 3) return null;
|
|
6334
|
+
const pathStyle = aliasCount === 0 ? "relative" : relativeCount === 0 ? "alias" : relativeCount >= aliasCount ? "relative" : "alias";
|
|
6335
|
+
const totalBindings = defaultCount + namedCount;
|
|
6336
|
+
let bindingStyle = null;
|
|
6337
|
+
if (totalBindings >= 3) {
|
|
6338
|
+
const defaultRatio = defaultCount / totalBindings;
|
|
6339
|
+
bindingStyle = defaultRatio > 0.6 ? "default_heavy" : defaultRatio < 0.4 ? "named_heavy" : "mixed";
|
|
6340
|
+
}
|
|
6341
|
+
return {
|
|
6342
|
+
file: file.path,
|
|
6343
|
+
pathStyle,
|
|
6344
|
+
bindingStyle,
|
|
6345
|
+
relativeCount,
|
|
6346
|
+
aliasCount,
|
|
6347
|
+
defaultCount,
|
|
6348
|
+
namedCount,
|
|
6349
|
+
evidence
|
|
6350
|
+
};
|
|
6351
|
+
}
|
|
6352
|
+
var PATH_STYLE_NAMES = {
|
|
6353
|
+
relative: "relative paths (./)",
|
|
6354
|
+
alias: "path aliases (@/)"
|
|
6355
|
+
};
|
|
6356
|
+
function buildFinding(profiles, getStyle, styleNames, subCategory, label) {
|
|
6357
|
+
const withStyle = profiles.filter((p) => getStyle(p) !== null);
|
|
6358
|
+
if (withStyle.length < 5) return null;
|
|
6359
|
+
const counts = /* @__PURE__ */ new Map();
|
|
6360
|
+
for (const p of withStyle) {
|
|
6361
|
+
const style = getStyle(p);
|
|
6362
|
+
if (!counts.has(style)) counts.set(style, { count: 0, files: [] });
|
|
6363
|
+
const entry = counts.get(style);
|
|
6364
|
+
entry.count++;
|
|
6365
|
+
entry.files.push(p.file);
|
|
6366
|
+
}
|
|
6367
|
+
if (counts.size < 2) return null;
|
|
6368
|
+
let dominant = null;
|
|
6369
|
+
let dominantCount = 0;
|
|
6370
|
+
for (const [style, data] of counts) {
|
|
6371
|
+
if (data.count > dominantCount) {
|
|
6372
|
+
dominantCount = data.count;
|
|
6373
|
+
dominant = style;
|
|
6374
|
+
}
|
|
6375
|
+
}
|
|
6376
|
+
if (!dominant) return null;
|
|
6377
|
+
const totalFiles = withStyle.length;
|
|
6378
|
+
const consistencyScore = Math.round(dominantCount / totalFiles * 100);
|
|
6379
|
+
if (consistencyScore === 100) return null;
|
|
6380
|
+
const deviating = [];
|
|
6381
|
+
for (const [style, data] of counts) {
|
|
6382
|
+
if (style === dominant) continue;
|
|
6383
|
+
for (const filePath of data.files) {
|
|
6384
|
+
const profile = withStyle.find((p) => p.file === filePath);
|
|
6385
|
+
deviating.push({
|
|
6386
|
+
path: filePath,
|
|
6387
|
+
detectedPattern: styleNames[style] ?? style,
|
|
6388
|
+
evidence: profile?.evidence.slice(0, 2) ?? []
|
|
6389
|
+
});
|
|
6390
|
+
}
|
|
6391
|
+
}
|
|
6392
|
+
return {
|
|
6393
|
+
detector: "import_style",
|
|
6394
|
+
subCategory,
|
|
6395
|
+
driftCategory: "import_style",
|
|
6396
|
+
severity: deviating.length >= 5 ? "warning" : "info",
|
|
6397
|
+
confidence: 0.8,
|
|
6398
|
+
finding: `${label}: ${dominantCount} files use ${styleNames[dominant] ?? dominant}, ${deviating.length} deviate`,
|
|
6399
|
+
dominantPattern: styleNames[dominant] ?? dominant,
|
|
6400
|
+
dominantCount,
|
|
6401
|
+
totalRelevantFiles: totalFiles,
|
|
6402
|
+
consistencyScore,
|
|
6403
|
+
deviatingFiles: deviating,
|
|
6404
|
+
recommendation: `Standardize on ${styleNames[dominant] ?? dominant} across all files for consistency.`
|
|
6405
|
+
};
|
|
6406
|
+
}
|
|
6407
|
+
var importConsistency = {
|
|
6408
|
+
id: "import-consistency",
|
|
6409
|
+
name: "Import Style Consistency",
|
|
6410
|
+
category: "import_style",
|
|
6411
|
+
detect(ctx) {
|
|
6412
|
+
const findings = [];
|
|
6413
|
+
const profiles = [];
|
|
6414
|
+
for (const file of ctx.files) {
|
|
6415
|
+
const p = analyzeImports(file);
|
|
6416
|
+
if (p) profiles.push(p);
|
|
6417
|
+
}
|
|
6418
|
+
if (profiles.length < 5) return findings;
|
|
6419
|
+
const pathFinding = buildFinding(
|
|
6420
|
+
profiles,
|
|
6421
|
+
(p) => p.pathStyle,
|
|
6422
|
+
PATH_STYLE_NAMES,
|
|
6423
|
+
"path_style",
|
|
6424
|
+
"Import path style"
|
|
6425
|
+
);
|
|
6426
|
+
if (pathFinding) findings.push(pathFinding);
|
|
6427
|
+
return findings;
|
|
6428
|
+
}
|
|
6429
|
+
};
|
|
6430
|
+
|
|
6431
|
+
// src/drift/export-consistency.ts
|
|
6432
|
+
init_esm_shims();
|
|
6433
|
+
function isSourceFile2(path2) {
|
|
6434
|
+
if (/(?:test|spec|mock|fixture|__test__|__mocks__|\.test\.|\.spec\.)/i.test(path2)) return false;
|
|
6435
|
+
if (/(?:\.config\.|\.d\.ts$|node_modules|dist\/|build\/|index\.)/i.test(path2)) return false;
|
|
6436
|
+
return true;
|
|
6437
|
+
}
|
|
6438
|
+
function analyzeExports(file) {
|
|
6439
|
+
if (!file.language || !["javascript", "typescript"].includes(file.language)) return null;
|
|
6440
|
+
if (!isSourceFile2(file.path)) return null;
|
|
6441
|
+
const lines = file.content.split("\n");
|
|
6442
|
+
let hasDefaultExport = false;
|
|
6443
|
+
let hasNamedExport = false;
|
|
6444
|
+
const evidence = [];
|
|
6445
|
+
for (let i = 0; i < lines.length; i++) {
|
|
6446
|
+
const line = lines[i].trim();
|
|
6447
|
+
if (/^export\s+default\b/.test(line) || /\bmodule\.exports\s*=/.test(line)) {
|
|
6448
|
+
hasDefaultExport = true;
|
|
6449
|
+
if (evidence.length < 2) evidence.push({ line: i + 1, code: line.slice(0, 100) });
|
|
6450
|
+
} else if (/^export\s+(?:function|class|const|let|var|interface|type|enum|abstract)\b/.test(line)) {
|
|
6451
|
+
hasNamedExport = true;
|
|
6452
|
+
if (evidence.length < 2) evidence.push({ line: i + 1, code: line.slice(0, 100) });
|
|
6453
|
+
} else if (/^export\s*\{/.test(line)) {
|
|
6454
|
+
hasNamedExport = true;
|
|
6455
|
+
if (evidence.length < 2) evidence.push({ line: i + 1, code: line.slice(0, 100) });
|
|
6456
|
+
}
|
|
6457
|
+
}
|
|
6458
|
+
if (!hasDefaultExport && !hasNamedExport) return null;
|
|
6459
|
+
const style = hasDefaultExport ? "default_export" : "named_only";
|
|
6460
|
+
return { file: file.path, style, evidence };
|
|
6461
|
+
}
|
|
6462
|
+
var STYLE_NAMES = {
|
|
6463
|
+
default_export: "default exports",
|
|
6464
|
+
named_only: "named exports only"
|
|
6465
|
+
};
|
|
6466
|
+
var exportConsistency = {
|
|
6467
|
+
id: "export-consistency",
|
|
6468
|
+
name: "Export Style Consistency",
|
|
6469
|
+
category: "export_style",
|
|
6470
|
+
detect(ctx) {
|
|
6471
|
+
const profiles = [];
|
|
6472
|
+
for (const file of ctx.files) {
|
|
6473
|
+
const p = analyzeExports(file);
|
|
6474
|
+
if (p) profiles.push(p);
|
|
6475
|
+
}
|
|
6476
|
+
if (profiles.length < 5) return [];
|
|
6477
|
+
const counts = /* @__PURE__ */ new Map();
|
|
6478
|
+
for (const p of profiles) {
|
|
6479
|
+
if (!counts.has(p.style)) counts.set(p.style, { count: 0, files: [] });
|
|
6480
|
+
const entry = counts.get(p.style);
|
|
6481
|
+
entry.count++;
|
|
6482
|
+
entry.files.push(p.file);
|
|
6483
|
+
}
|
|
6484
|
+
if (counts.size < 2) return [];
|
|
6485
|
+
let dominant = null;
|
|
6486
|
+
let dominantCount = 0;
|
|
6487
|
+
for (const [style, data] of counts) {
|
|
6488
|
+
if (data.count > dominantCount) {
|
|
6489
|
+
dominantCount = data.count;
|
|
6490
|
+
dominant = style;
|
|
6491
|
+
}
|
|
6492
|
+
}
|
|
6493
|
+
if (!dominant) return [];
|
|
6494
|
+
const totalFiles = profiles.length;
|
|
6495
|
+
const consistencyScore = Math.round(dominantCount / totalFiles * 100);
|
|
6496
|
+
if (consistencyScore === 100) return [];
|
|
6497
|
+
const deviating = [];
|
|
6498
|
+
for (const [style, data] of counts) {
|
|
6499
|
+
if (style === dominant) continue;
|
|
6500
|
+
for (const filePath of data.files) {
|
|
6501
|
+
const profile = profiles.find((p) => p.file === filePath);
|
|
6502
|
+
deviating.push({
|
|
6503
|
+
path: filePath,
|
|
6504
|
+
detectedPattern: STYLE_NAMES[style],
|
|
6505
|
+
evidence: profile?.evidence.slice(0, 2) ?? []
|
|
6506
|
+
});
|
|
6507
|
+
}
|
|
6508
|
+
}
|
|
6509
|
+
return [{
|
|
6510
|
+
detector: "export_style",
|
|
6511
|
+
subCategory: "export_style",
|
|
6512
|
+
driftCategory: "export_style",
|
|
6513
|
+
severity: deviating.length >= 5 ? "warning" : "info",
|
|
6514
|
+
confidence: 0.75,
|
|
6515
|
+
finding: `Export style: ${dominantCount} files use ${STYLE_NAMES[dominant]}, ${deviating.length} use ${STYLE_NAMES[dominant === "default_export" ? "named_only" : "default_export"]}`,
|
|
6516
|
+
dominantPattern: STYLE_NAMES[dominant],
|
|
6517
|
+
dominantCount,
|
|
6518
|
+
totalRelevantFiles: totalFiles,
|
|
6519
|
+
consistencyScore,
|
|
6520
|
+
deviatingFiles: deviating,
|
|
6521
|
+
recommendation: `Standardize on ${STYLE_NAMES[dominant]}. ${dominant === "named_only" ? "Named exports enable tree-shaking and explicit imports." : "Default exports simplify imports but disable tree-shaking."}`
|
|
6522
|
+
}];
|
|
6523
|
+
}
|
|
6524
|
+
};
|
|
6525
|
+
|
|
6526
|
+
// src/drift/async-consistency.ts
|
|
6527
|
+
init_esm_shims();
|
|
6528
|
+
function isSourceFile3(path2) {
|
|
6529
|
+
if (/(?:test|spec|mock|fixture|__test__|__mocks__|\.test\.|\.spec\.)/i.test(path2)) return false;
|
|
6530
|
+
if (/(?:\.config\.|\.d\.ts$|node_modules|dist\/|build\/)/i.test(path2)) return false;
|
|
6531
|
+
return true;
|
|
6532
|
+
}
|
|
6533
|
+
function analyzeAsync(file) {
|
|
6534
|
+
if (!file.language || !["javascript", "typescript"].includes(file.language)) return null;
|
|
6535
|
+
if (!isSourceFile3(file.path)) return null;
|
|
6536
|
+
const lines = file.content.split("\n");
|
|
6537
|
+
let awaitCount = 0;
|
|
6538
|
+
let thenCount = 0;
|
|
6539
|
+
const evidence = [];
|
|
6540
|
+
for (let i = 0; i < lines.length; i++) {
|
|
6541
|
+
const line = lines[i];
|
|
6542
|
+
if (/\bawait\s+/.test(line) && !line.trim().startsWith("//") && !line.trim().startsWith("*")) {
|
|
6543
|
+
awaitCount++;
|
|
6544
|
+
if (evidence.length < 3) {
|
|
6545
|
+
evidence.push({ line: i + 1, code: line.trim().slice(0, 100) });
|
|
6546
|
+
}
|
|
6547
|
+
}
|
|
6548
|
+
if (/\.\s*then\s*\(/.test(line) && !line.trim().startsWith("//") && !line.trim().startsWith("*") && !/type\s|interface\s/.test(line)) {
|
|
6549
|
+
thenCount++;
|
|
6550
|
+
if (evidence.length < 3) {
|
|
6551
|
+
evidence.push({ line: i + 1, code: line.trim().slice(0, 100) });
|
|
6552
|
+
}
|
|
6553
|
+
}
|
|
6554
|
+
}
|
|
6555
|
+
const total = awaitCount + thenCount;
|
|
6556
|
+
if (total < 2) return null;
|
|
6557
|
+
let style;
|
|
6558
|
+
const awaitRatio = awaitCount / total;
|
|
6559
|
+
if (awaitRatio > 0.7) style = "async_await";
|
|
6560
|
+
else if (awaitRatio < 0.3) style = "then_chains";
|
|
6561
|
+
else style = "mixed";
|
|
6562
|
+
return { file: file.path, style, awaitCount, thenCount, evidence };
|
|
6563
|
+
}
|
|
6564
|
+
var STYLE_NAMES2 = {
|
|
6565
|
+
async_await: "async/await",
|
|
6566
|
+
then_chains: ".then() chains",
|
|
6567
|
+
mixed: "mixed async patterns"
|
|
6568
|
+
};
|
|
6569
|
+
var asyncConsistency = {
|
|
6570
|
+
id: "async-consistency",
|
|
6571
|
+
name: "Async Pattern Consistency",
|
|
6572
|
+
category: "async_patterns",
|
|
6573
|
+
detect(ctx) {
|
|
6574
|
+
const profiles = [];
|
|
6575
|
+
for (const file of ctx.files) {
|
|
6576
|
+
const p = analyzeAsync(file);
|
|
6577
|
+
if (p) profiles.push(p);
|
|
6578
|
+
}
|
|
6579
|
+
if (profiles.length < 5) return [];
|
|
6580
|
+
const counts = /* @__PURE__ */ new Map();
|
|
6581
|
+
for (const p of profiles) {
|
|
6582
|
+
if (!counts.has(p.style)) counts.set(p.style, { count: 0, files: [] });
|
|
6583
|
+
const entry = counts.get(p.style);
|
|
6584
|
+
entry.count++;
|
|
6585
|
+
entry.files.push(p.file);
|
|
6586
|
+
}
|
|
6587
|
+
if (counts.size < 2) return [];
|
|
6588
|
+
let dominant = null;
|
|
6589
|
+
let dominantCount = 0;
|
|
6590
|
+
for (const [style, data] of counts) {
|
|
6591
|
+
if (data.count > dominantCount) {
|
|
6592
|
+
dominantCount = data.count;
|
|
6593
|
+
dominant = style;
|
|
6594
|
+
}
|
|
6595
|
+
}
|
|
6596
|
+
if (!dominant) return [];
|
|
6597
|
+
const totalFiles = profiles.length;
|
|
6598
|
+
const consistencyScore = Math.round(dominantCount / totalFiles * 100);
|
|
6599
|
+
if (consistencyScore === 100) return [];
|
|
6600
|
+
const deviating = [];
|
|
6601
|
+
for (const [style, data] of counts) {
|
|
6602
|
+
if (style === dominant) continue;
|
|
6603
|
+
for (const filePath of data.files) {
|
|
6604
|
+
const profile = profiles.find((p) => p.file === filePath);
|
|
6605
|
+
deviating.push({
|
|
6606
|
+
path: filePath,
|
|
6607
|
+
detectedPattern: STYLE_NAMES2[style],
|
|
6608
|
+
evidence: profile?.evidence.slice(0, 2) ?? []
|
|
6609
|
+
});
|
|
6610
|
+
}
|
|
6611
|
+
}
|
|
6612
|
+
return [{
|
|
6613
|
+
detector: "async_patterns",
|
|
6614
|
+
subCategory: "async_style",
|
|
6615
|
+
driftCategory: "async_patterns",
|
|
6616
|
+
severity: deviating.length >= 5 ? "warning" : "info",
|
|
6617
|
+
confidence: 0.8,
|
|
6618
|
+
finding: `Async style: ${dominantCount} files use ${STYLE_NAMES2[dominant]}, ${deviating.length} use different patterns`,
|
|
6619
|
+
dominantPattern: STYLE_NAMES2[dominant],
|
|
6620
|
+
dominantCount,
|
|
6621
|
+
totalRelevantFiles: totalFiles,
|
|
6622
|
+
consistencyScore,
|
|
6623
|
+
deviatingFiles: deviating,
|
|
6624
|
+
recommendation: `Standardize on ${STYLE_NAMES2[dominant]}. ${dominant === "async_await" ? "async/await is more readable and has clearer error handling with try/catch." : "Consider migrating to async/await for consistency and readability."}`
|
|
6625
|
+
}];
|
|
6626
|
+
}
|
|
6627
|
+
};
|
|
6628
|
+
|
|
6288
6629
|
// src/drift/index.ts
|
|
6289
6630
|
function createDriftDetectors() {
|
|
6290
6631
|
return [
|
|
@@ -6292,7 +6633,10 @@ function createDriftDetectors() {
|
|
|
6292
6633
|
conventionOscillation,
|
|
6293
6634
|
securityConsistency,
|
|
6294
6635
|
semanticDuplication,
|
|
6295
|
-
phantomScaffolding
|
|
6636
|
+
phantomScaffolding,
|
|
6637
|
+
importConsistency,
|
|
6638
|
+
exportConsistency,
|
|
6639
|
+
asyncConsistency
|
|
6296
6640
|
];
|
|
6297
6641
|
}
|
|
6298
6642
|
function buildDriftContext(ctx) {
|
|
@@ -6325,7 +6669,10 @@ function computeDriftScores(findings) {
|
|
|
6325
6669
|
"security_posture",
|
|
6326
6670
|
"semantic_duplication",
|
|
6327
6671
|
"naming_conventions",
|
|
6328
|
-
"phantom_scaffolding"
|
|
6672
|
+
"phantom_scaffolding",
|
|
6673
|
+
"import_style",
|
|
6674
|
+
"export_style",
|
|
6675
|
+
"async_patterns"
|
|
6329
6676
|
];
|
|
6330
6677
|
const scores = {};
|
|
6331
6678
|
for (const cat of categories) {
|
|
@@ -7067,8 +7414,12 @@ function buildCoherenceMatrix(files) {
|
|
|
7067
7414
|
}
|
|
7068
7415
|
const colCount = categories.length + 3;
|
|
7069
7416
|
const alignedRows = aligned.slice(0, 3).map((f) => fileRow(f)).join("");
|
|
7070
|
-
const collapsedAligned = aligned.length > 3 ? `<tr><td colspan="${colCount}" style="padding:
|
|
7071
|
-
|
|
7417
|
+
const collapsedAligned = aligned.length > 3 ? `<tr><td colspan="${colCount}" style="padding:0"><details style="margin:0">
|
|
7418
|
+
<summary style="cursor:pointer;padding:8px 16px;font-size:12px;color:var(--text-tertiary);list-style:none;display:flex;align-items:center;gap:6px;background:rgba(255,255,255,0.02)">
|
|
7419
|
+
<span class="chevron">▶</span> ${aligned.length - 3} more files at 100% alignment
|
|
7420
|
+
</summary>
|
|
7421
|
+
<table style="width:100%;border-collapse:collapse">${aligned.slice(3).map((f) => fileRow(f)).join("")}</table>
|
|
7422
|
+
</details></td></tr>` : "";
|
|
7072
7423
|
const singleIssueDrifting = /* @__PURE__ */ new Map();
|
|
7073
7424
|
const multiIssueDrifting = [];
|
|
7074
7425
|
for (const f of drifting) {
|
|
@@ -7085,9 +7436,12 @@ function buildCoherenceMatrix(files) {
|
|
|
7085
7436
|
for (const [devKey, files2] of singleIssueDrifting) {
|
|
7086
7437
|
if (files2.length >= 4) {
|
|
7087
7438
|
driftingRows += fileRow(files2[0]);
|
|
7088
|
-
|
|
7089
|
-
|
|
7090
|
-
|
|
7439
|
+
driftingRows += `<tr><td colspan="${colCount}" style="padding:0"><details style="margin:0">
|
|
7440
|
+
<summary style="cursor:pointer;padding:8px 16px;font-size:12px;color:var(--text-tertiary);list-style:none;display:flex;align-items:center;gap:6px;background:rgba(255,255,255,0.02)">
|
|
7441
|
+
<span class="chevron">▶</span> ${files2.length - 1} more files with same issue <span style="color:var(--drift-orange)">(${esc(devKey)})</span>
|
|
7442
|
+
</summary>
|
|
7443
|
+
<table style="width:100%;border-collapse:collapse">${files2.slice(1).map((f) => fileRow(f)).join("")}</table>
|
|
7444
|
+
</details></td></tr>`;
|
|
7091
7445
|
} else {
|
|
7092
7446
|
driftingRows += files2.map((f) => fileRow(f)).join("");
|
|
7093
7447
|
}
|
|
@@ -7246,7 +7600,7 @@ function buildRadarSection(result) {
|
|
|
7246
7600
|
return `<section class="section">
|
|
7247
7601
|
<div class="label">DRIFT FINGERPRINT</div>
|
|
7248
7602
|
<p style="font-size:13px;color:var(--text-secondary);margin-bottom:20px;line-height:1.6">
|
|
7249
|
-
This radar shows how well your codebase maintains consistency across
|
|
7603
|
+
This radar shows how well your codebase maintains consistency across 8 drift categories.
|
|
7250
7604
|
The outer edge represents a perfect score in each category. Points closer to the center indicate more drift.
|
|
7251
7605
|
A balanced shape means consistent quality; an uneven shape reveals where drift concentrates.
|
|
7252
7606
|
</p>
|
|
@@ -7330,23 +7684,32 @@ function buildDeviatingBlocks(d) {
|
|
|
7330
7684
|
}
|
|
7331
7685
|
if (isConvention && d.deviatingFiles.length > 4) {
|
|
7332
7686
|
return [...devByPattern.entries()].map(([pattern, files]) => {
|
|
7333
|
-
const fileList = files.map((df) => esc(df.path)).join("
|
|
7334
|
-
|
|
7335
|
-
|
|
7336
|
-
|
|
7337
|
-
|
|
7338
|
-
|
|
7687
|
+
const fileList = files.map((df) => `<div style="padding:2px 0;font-size:12px;color:var(--text-secondary)" class="mono">${esc(df.path)}</div>`).join("");
|
|
7688
|
+
return `<div style="background:var(--tint-orange);border-left:3px solid var(--drift-orange);border-radius:0;margin:8px 0">
|
|
7689
|
+
<details>
|
|
7690
|
+
<summary style="cursor:pointer;padding:12px 16px;list-style:none;display:flex;align-items:center;gap:6px">
|
|
7691
|
+
<span class="chevron">▶</span>
|
|
7692
|
+
<span class="label" style="color:var(--drift-orange);margin:0">DRIFT — ${esc(pattern)}</span>
|
|
7693
|
+
<span style="font-size:12px;color:var(--text-tertiary);margin-left:auto">${files.length} files</span>
|
|
7694
|
+
</summary>
|
|
7695
|
+
<div style="padding:4px 16px 12px">${fileList}</div>
|
|
7696
|
+
</details>
|
|
7339
7697
|
</div>`;
|
|
7340
7698
|
}).join("");
|
|
7341
7699
|
}
|
|
7342
|
-
return d.deviatingFiles.slice(0,
|
|
7700
|
+
return d.deviatingFiles.slice(0, 6).map((df) => {
|
|
7343
7701
|
const evidence = df.evidence.slice(0, 3).map(
|
|
7344
7702
|
(e) => `<div style="background:var(--bg-code);padding:6px 12px;border-radius:0;margin:4px 0;overflow-x:auto" class="mono"><span style="color:var(--text-tertiary);margin-right:12px;user-select:none">${e.line}</span>${esc(e.code.slice(0, 120))}</div>`
|
|
7345
7703
|
).join("");
|
|
7346
|
-
return `<div style="background:var(--tint-orange);border-left:3px solid var(--drift-orange);border-radius:0;
|
|
7347
|
-
<
|
|
7348
|
-
|
|
7349
|
-
|
|
7704
|
+
return `<div style="background:var(--tint-orange);border-left:3px solid var(--drift-orange);border-radius:0;margin:8px 0">
|
|
7705
|
+
<details>
|
|
7706
|
+
<summary style="cursor:pointer;padding:10px 16px;list-style:none;display:flex;align-items:center;gap:6px">
|
|
7707
|
+
<span class="chevron">▶</span>
|
|
7708
|
+
<span class="label" style="color:var(--drift-orange);margin:0">DRIFT — ${esc(df.detectedPattern)}</span>
|
|
7709
|
+
<span class="mono" style="font-size:12px;color:var(--text-secondary);margin-left:8px">${esc(df.path)}</span>
|
|
7710
|
+
</summary>
|
|
7711
|
+
<div style="padding:4px 16px 12px">${evidence}</div>
|
|
7712
|
+
</details>
|
|
7350
7713
|
</div>`;
|
|
7351
7714
|
}).join("");
|
|
7352
7715
|
}
|
|
@@ -7381,13 +7744,16 @@ function buildDistributionBar(d) {
|
|
|
7381
7744
|
</div>`;
|
|
7382
7745
|
}
|
|
7383
7746
|
function buildDriftFindings(result) {
|
|
7384
|
-
const driftCats = ["architectural_consistency", "security_posture", "semantic_duplication", "naming_conventions", "phantom_scaffolding"];
|
|
7747
|
+
const driftCats = ["architectural_consistency", "security_posture", "semantic_duplication", "naming_conventions", "phantom_scaffolding", "import_style", "export_style", "async_patterns"];
|
|
7385
7748
|
const catLabels = {
|
|
7386
7749
|
architectural_consistency: "Architectural contradictions",
|
|
7387
7750
|
security_posture: "Security posture gaps",
|
|
7388
7751
|
semantic_duplication: "Semantic duplication",
|
|
7389
7752
|
naming_conventions: "Convention drift",
|
|
7390
|
-
phantom_scaffolding: "Phantom scaffolding"
|
|
7753
|
+
phantom_scaffolding: "Phantom scaffolding",
|
|
7754
|
+
import_style: "Import style drift",
|
|
7755
|
+
export_style: "Export style drift",
|
|
7756
|
+
async_patterns: "Async pattern drift"
|
|
7391
7757
|
};
|
|
7392
7758
|
const groups = driftCats.map((cat) => ({
|
|
7393
7759
|
cat,
|
|
@@ -7414,18 +7780,21 @@ function buildDriftFindings(result) {
|
|
|
7414
7780
|
const rec = recText ? `<div style="margin-top:12px;padding:10px 16px;background:var(--tint-cyan);border-left:3px solid var(--border);border-radius:0;font-size:14px;line-height:1.6;color:var(--text-primary)">
|
|
7415
7781
|
<span style="color:var(--text-secondary);font-weight:700;margin-right:4px">→</span> ${esc(recText)}
|
|
7416
7782
|
</div>` : "";
|
|
7417
|
-
return `<
|
|
7418
|
-
<
|
|
7783
|
+
return `<details style="background:var(--bg-surface);border-radius:0;margin-bottom:10px;border:1px solid var(--border)">
|
|
7784
|
+
<summary style="cursor:pointer;padding:16px 20px;display:flex;align-items:center;gap:8px;flex-wrap:wrap;list-style:none">
|
|
7785
|
+
<span class="chevron">▶</span>
|
|
7419
7786
|
<span class="sev-badge" style="background:${sevColor(d.severity)}">${sevLabel(d.severity)}</span>
|
|
7420
|
-
<span style="font-size:
|
|
7787
|
+
<span style="font-size:14px;font-weight:500;color:var(--text-primary);flex:1">${esc(d.finding)}</span>
|
|
7421
7788
|
<span class="mono" style="font-size:12px;color:var(--text-tertiary)">${d.dominantCount}/${d.totalRelevantFiles}</span>
|
|
7789
|
+
</summary>
|
|
7790
|
+
<div style="padding:4px 20px 20px">
|
|
7791
|
+
${domBlock}
|
|
7792
|
+
${devBlocks}
|
|
7793
|
+
${distBar}
|
|
7794
|
+
${rec}
|
|
7795
|
+
${closeSplitQualifier}
|
|
7422
7796
|
</div>
|
|
7423
|
-
|
|
7424
|
-
${devBlocks}
|
|
7425
|
-
${distBar}
|
|
7426
|
-
${rec}
|
|
7427
|
-
${closeSplitQualifier}
|
|
7428
|
-
</div>`;
|
|
7797
|
+
</details>`;
|
|
7429
7798
|
}).join("");
|
|
7430
7799
|
return `<details ${gi === 0 ? "open" : ""} style="margin-bottom:8px">
|
|
7431
7800
|
<summary style="cursor:pointer;padding:12px 18px;background:var(--bg-surface);border-radius:0;display:flex;align-items:center;gap:10px;font-size:14px;font-weight:600;color:var(--text-primary);list-style:none;border:1px solid var(--border)">
|