@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 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 (5 detectors)
45
- - **Architectural consistency** — data access patterns, error handling, DI, config approaches
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 the AI generated but never wired up
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: 25,
5182
- security_posture: 25,
5183
- semantic_duplication: 20,
5184
- naming_conventions: 15,
5185
- phantom_scaffolding: 15
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 && isHandlerOrController(file.path)) {
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 || !isHandlerOrController(file.path)) return null;
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:6px 12px;font-size:12px;color:var(--text-tertiary);cursor:pointer" data-collapse="aligned-rest">&middot;&middot;&middot; ${aligned.length - 3} more at 100% &middot;&middot;&middot;</td></tr>
7071
- <tbody id="aligned-rest" style="display:none">${aligned.slice(3).map((f) => fileRow(f)).join("")}</tbody>` : "";
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">&#9654;</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
- const collapseId = `single-dev-${devKey.replace(/[^a-zA-Z0-9]/g, "-")}`;
7089
- driftingRows += `<tr><td colspan="${colCount}" style="padding:4px 12px;font-size:12px;color:var(--text-tertiary);cursor:pointer" data-collapse="${collapseId}">&middot;&middot;&middot; ${files2.length - 1} more files with same issue (${esc(devKey)}) &middot;&middot;&middot;</td></tr>`;
7090
- driftingRows += `<tbody id="${collapseId}" style="display:none">${files2.slice(1).map((f) => fileRow(f)).join("")}</tbody>`;
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">&#9654;</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 5 drift categories.
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("</div><div style='padding:1px 0'>");
7334
- const collapseId = `conv-${pattern.replace(/[^a-zA-Z0-9]/g, "-")}-${Math.random().toString(36).slice(2, 6)}`;
7335
- return `<div style="background:var(--tint-orange);border-left:3px solid var(--drift-orange);border-radius:0;padding:12px 16px;margin:8px 0">
7336
- <div class="label" style="color:var(--drift-orange);margin-bottom:4px">DRIFT &mdash; ${esc(pattern)} &mdash; ${files.length} files</div>
7337
- <div style="cursor:pointer;font-size:12px;color:var(--text-secondary);margin-top:6px" data-collapse="${collapseId}">&#9654; Show ${files.length} files</div>
7338
- <div id="${collapseId}" style="display:none;padding:6px 0" class="mono" style="font-size:12px;color:var(--text-secondary)"><div style="padding:1px 0">${fileList}</div></div>
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">&#9654;</span>
7692
+ <span class="label" style="color:var(--drift-orange);margin:0">DRIFT &mdash; ${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, 4).map((df) => {
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;padding:12px 16px;margin:8px 0">
7347
- <div class="label" style="color:var(--drift-orange);margin-bottom:4px">DRIFT &mdash; ${esc(df.detectedPattern)}</div>
7348
- <div class="mono" style="font-size:12px;color:var(--text-secondary);margin-bottom:6px">${esc(df.path)}</div>
7349
- ${evidence}
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">&#9654;</span>
7708
+ <span class="label" style="color:var(--drift-orange);margin:0">DRIFT &mdash; ${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">&rarr;</span> ${esc(recText)}
7416
7782
  </div>` : "";
7417
- return `<div style="background:var(--bg-surface);border-radius:0;padding:20px 24px;margin-bottom:10px;border:1px solid var(--border)">
7418
- <div style="display:flex;align-items:center;gap:8px;margin-bottom:10px;flex-wrap:wrap">
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">&#9654;</span>
7419
7786
  <span class="sev-badge" style="background:${sevColor(d.severity)}">${sevLabel(d.severity)}</span>
7420
- <span style="font-size:15px;font-weight:500;color:var(--text-primary);flex:1">${esc(d.finding)}</span>
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
- ${domBlock}
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)">