@kb-labs/quality-core 2.93.0 → 2.96.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/dist/index.js CHANGED
@@ -1,10 +1,12 @@
1
- import fs8 from 'fs';
2
- import path8, { join } from 'path';
3
- import { readFile, stat, access } from 'fs/promises';
4
- import globby3 from 'globby';
5
- import { exec, execSync } from 'child_process';
1
+ import fs8, { existsSync, readFileSync, mkdirSync, writeFileSync } from 'fs';
2
+ import path9, { join, dirname } from 'path';
3
+ import { stat, readFile } from 'fs/promises';
4
+ import { exec, execSync, execFile } from 'child_process';
5
+ import globby2 from 'globby';
6
+ import { defaultQualityConfig, DIMENSION_WEIGHTS, MAX_SNAPSHOTS_DEFAULT, SNAPSHOT_DIR, SNAPSHOT_FILE, HEALTH_GRADES } from '@kb-labs/quality-contracts';
6
7
  import { promisify } from 'util';
7
8
  import ts from 'typescript';
9
+ import { randomUUID } from 'crypto';
8
10
 
9
11
  // src/graph/dependency-graph.ts
10
12
  function buildDependencyGraph(rootDir) {
@@ -18,8 +20,8 @@ function buildDependencyGraph(rootDir) {
18
20
  if (!entry.isDirectory() || !entry.name.startsWith("kb-labs-")) {
19
21
  continue;
20
22
  }
21
- const repoPath = path8.join(rootDir, entry.name);
22
- const packagesDir = path8.join(repoPath, "packages");
23
+ const repoPath = path9.join(rootDir, entry.name);
24
+ const packagesDir = path9.join(repoPath, "packages");
23
25
  if (!fs8.existsSync(packagesDir)) {
24
26
  continue;
25
27
  }
@@ -28,7 +30,7 @@ function buildDependencyGraph(rootDir) {
28
30
  if (!pkgDir.isDirectory()) {
29
31
  continue;
30
32
  }
31
- const packageJsonPath = path8.join(packagesDir, pkgDir.name, "package.json");
33
+ const packageJsonPath = path9.join(packagesDir, pkgDir.name, "package.json");
32
34
  if (fs8.existsSync(packageJsonPath)) {
33
35
  const pkgJson = JSON.parse(fs8.readFileSync(packageJsonPath, "utf-8"));
34
36
  const packageName = pkgJson.name;
@@ -42,8 +44,8 @@ function buildDependencyGraph(rootDir) {
42
44
  if (!entry.isDirectory() || !entry.name.startsWith("kb-labs-")) {
43
45
  continue;
44
46
  }
45
- const repoPath = path8.join(rootDir, entry.name);
46
- const packagesDir = path8.join(repoPath, "packages");
47
+ const repoPath = path9.join(rootDir, entry.name);
48
+ const packagesDir = path9.join(repoPath, "packages");
47
49
  if (!fs8.existsSync(packagesDir)) {
48
50
  continue;
49
51
  }
@@ -52,7 +54,7 @@ function buildDependencyGraph(rootDir) {
52
54
  if (!pkgDir.isDirectory()) {
53
55
  continue;
54
56
  }
55
- const packageJsonPath = path8.join(packagesDir, pkgDir.name, "package.json");
57
+ const packageJsonPath = path9.join(packagesDir, pkgDir.name, "package.json");
56
58
  if (fs8.existsSync(packageJsonPath)) {
57
59
  const pkgJson = JSON.parse(fs8.readFileSync(packageJsonPath, "utf-8"));
58
60
  const packageName = pkgJson.name;
@@ -80,7 +82,7 @@ function buildDependencyGraph(rootDir) {
80
82
  nodes.set(packageName, {
81
83
  name: packageName,
82
84
  path: packageJsonPath,
83
- dir: path8.dirname(packageJsonPath),
85
+ dir: path9.dirname(packageJsonPath),
84
86
  deps,
85
87
  devDeps,
86
88
  dependents: /* @__PURE__ */ new Set()
@@ -158,10 +160,10 @@ function findCircularDependencies(graph, subset) {
158
160
  const visited = /* @__PURE__ */ new Set();
159
161
  const recStack = /* @__PURE__ */ new Set();
160
162
  const cycles = [];
161
- function dfs(node, path9) {
163
+ function dfs(node, path12) {
162
164
  visited.add(node);
163
165
  recStack.add(node);
164
- path9.push(node);
166
+ path12.push(node);
165
167
  const nodeData = nodes.get(node);
166
168
  if (!nodeData) {
167
169
  return;
@@ -174,11 +176,11 @@ function findCircularDependencies(graph, subset) {
174
176
  continue;
175
177
  }
176
178
  if (!visited.has(dep)) {
177
- dfs(dep, [...path9]);
179
+ dfs(dep, [...path12]);
178
180
  } else if (recStack.has(dep)) {
179
- const cycleStart = path9.indexOf(dep);
181
+ const cycleStart = path12.indexOf(dep);
180
182
  if (cycleStart !== -1) {
181
- const cycle = path9.slice(cycleStart);
183
+ const cycle = path12.slice(cycleStart);
182
184
  cycles.push([...cycle, dep]);
183
185
  }
184
186
  }
@@ -247,33 +249,54 @@ function getImpactAnalysis(graph, packageName) {
247
249
  }
248
250
  return affected;
249
251
  }
250
- async function calculateLinesOfCode(rootDir, sourceFiles) {
251
- const files = sourceFiles ?? await globby3("**/*.{ts,tsx,js,jsx}", {
252
- cwd: rootDir,
253
- ignore: [
254
- "**/node_modules/**",
255
- "**/dist/**",
256
- "**/.git/**",
257
- "**/.kb/**",
258
- "**/build/**",
259
- "**/*.test.{ts,tsx,js,jsx}",
260
- "**/*.spec.{ts,tsx,js,jsx}"
261
- ],
262
- absolute: true,
263
- deep: 8
264
- });
265
- const contents = await Promise.all(files.map((f) => readFile(f, "utf-8").catch(() => null)));
266
- return contents.reduce((sum, c) => sum + (c ? c.split("\n").length : 0), 0);
252
+ var LOC_SAMPLE_SIZE = 200;
253
+ var FALLBACK_BYTES_PER_LINE = 45;
254
+ async function readTextFile(f) {
255
+ try {
256
+ return await readFile(f, "utf8");
257
+ } catch {
258
+ return null;
259
+ }
267
260
  }
268
- async function calculateSize(rootDir, sourceFiles) {
269
- const files = sourceFiles ?? await globby3("**/*.{ts,tsx,js,jsx}", {
270
- cwd: rootDir,
271
- ignore: ["**/node_modules/**", "**/dist/**", "**/.git/**", "**/.kb/**", "**/build/**"],
272
- absolute: true,
273
- deep: 8
261
+ var SOURCE_PATTERNS = ["*.ts", "*.tsx", "*.js", "*.jsx", "*.go"];
262
+ async function getSourceFiles(rootDir) {
263
+ return new Promise((resolve) => {
264
+ execFile(
265
+ "git",
266
+ ["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", ...SOURCE_PATTERNS],
267
+ { cwd: rootDir, maxBuffer: 64 * 1024 * 1024 },
268
+ (err, stdout) => {
269
+ if (err || !stdout) {
270
+ return resolve([]);
271
+ }
272
+ resolve(
273
+ stdout.split("\0").filter(Boolean).map((f) => path9.join(rootDir, f))
274
+ );
275
+ }
276
+ );
274
277
  });
275
- const stats = await Promise.all(files.map((f) => stat(f).catch(() => null)));
276
- return stats.reduce((sum, s) => sum + (s ? s.size : 0), 0);
278
+ }
279
+ async function estimateLoc(files, totalSize) {
280
+ if (files.length === 0) {
281
+ return 0;
282
+ }
283
+ const indices = /* @__PURE__ */ new Set();
284
+ while (indices.size < Math.min(LOC_SAMPLE_SIZE, files.length)) {
285
+ indices.add(Math.floor(Math.random() * files.length));
286
+ }
287
+ const sample = Array.from(indices).map((i) => files[i]).filter((f) => f !== void 0);
288
+ const contents = await Promise.all(sample.map(readTextFile));
289
+ let sampleBytes = 0;
290
+ let sampleLines = 0;
291
+ for (const c of contents) {
292
+ if (!c) {
293
+ continue;
294
+ }
295
+ sampleBytes += Buffer.byteLength(c, "utf-8");
296
+ sampleLines += c.split("\n").length;
297
+ }
298
+ const bytesPerLine = sampleLines > 0 ? sampleBytes / sampleLines : FALLBACK_BYTES_PER_LINE;
299
+ return Math.round(totalSize / bytesPerLine);
277
300
  }
278
301
  function formatBytes(bytes) {
279
302
  if (bytes === 0) {
@@ -286,7 +309,7 @@ function formatBytes(bytes) {
286
309
  return `${value.toFixed(2)} ${units[i]}`;
287
310
  }
288
311
  async function countPackages(rootDir) {
289
- const packageJsonFiles = await globby3("**/package.json", {
312
+ const packageJsonFiles = await globby2("**/package.json", {
290
313
  cwd: rootDir,
291
314
  ignore: ["**/node_modules/**", "**/.git/**", "**/.kb/**"],
292
315
  absolute: false,
@@ -295,17 +318,13 @@ async function countPackages(rootDir) {
295
318
  return packageJsonFiles.filter((p) => p !== "package.json").length;
296
319
  }
297
320
  async function calculateStats(rootDir) {
298
- const sourceFiles = await globby3("**/*.{ts,tsx,js,jsx}", {
299
- cwd: rootDir,
300
- ignore: ["**/node_modules/**", "**/dist/**", "**/.git/**", "**/.kb/**", "**/build/**"],
301
- absolute: true,
302
- deep: 8
303
- });
304
- const [packages, loc, size] = await Promise.all([
305
- countPackages(rootDir),
306
- calculateLinesOfCode(rootDir, sourceFiles),
307
- calculateSize(rootDir, sourceFiles)
321
+ const [files, packages] = await Promise.all([
322
+ getSourceFiles(rootDir),
323
+ countPackages(rootDir)
308
324
  ]);
325
+ const fileStats = await Promise.all(files.map((f) => stat(f).catch(() => null)));
326
+ const size = fileStats.reduce((sum, s) => sum + (s ? s.size : 0), 0);
327
+ const loc = await estimateLoc(files, size);
309
328
  return {
310
329
  packages,
311
330
  loc,
@@ -313,129 +332,114 @@ async function calculateStats(rootDir) {
313
332
  sizeFormatted: formatBytes(size)
314
333
  };
315
334
  }
316
- async function checkDuplicateDependencies(rootDir, pkgFiles) {
317
- const packageJsonFiles = pkgFiles ?? await globby3("**/package.json", {
318
- cwd: rootDir,
319
- ignore: ["**/node_modules/**", "**/.git/**"],
320
- absolute: true
321
- });
322
- const depVersions = /* @__PURE__ */ new Map();
323
- const contents = await Promise.all(
324
- packageJsonFiles.map((f) => readFile(f, "utf-8").catch(() => null))
325
- );
326
- for (const content of contents) {
327
- if (!content) {
328
- continue;
329
- }
330
- try {
331
- const pkg = JSON.parse(content);
332
- const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
333
- for (const [name, version] of Object.entries(allDeps)) {
334
- if (typeof version !== "string") {
335
- continue;
336
- }
337
- if (!depVersions.has(name)) {
338
- depVersions.set(name, /* @__PURE__ */ new Set());
339
- }
340
- depVersions.get(name).add(version);
341
- }
342
- } catch {
343
- }
344
- }
345
- const duplicates = Array.from(depVersions.entries()).filter(
346
- ([, versions]) => versions.size > 1
347
- );
348
- if (duplicates.length === 0) {
349
- return null;
350
- }
351
- const penalty = Math.min(duplicates.length * 2, 30);
352
- return {
353
- type: "duplicate",
354
- severity: duplicates.length > 20 ? "high" : duplicates.length > 10 ? "medium" : "low",
355
- message: `Found ${duplicates.length} duplicate dependencies with different versions`,
356
- count: duplicates.length,
357
- penalty
358
- };
335
+ async function calculateLinesOfCode(rootDir) {
336
+ const files = await getSourceFiles(rootDir);
337
+ const fileStats = await Promise.all(files.map((f) => stat(f).catch(() => null)));
338
+ const size = fileStats.reduce((sum, s) => sum + (s ? s.size : 0), 0);
339
+ return estimateLoc(files, size);
359
340
  }
360
- async function checkMissingReadmes(rootDir, packageJsonFiles) {
361
- const pkgFiles = packageJsonFiles ?? await globby3("**/package.json", {
362
- cwd: rootDir,
363
- ignore: ["**/node_modules/**", "**/.git/**", "package.json"],
364
- absolute: true
365
- });
366
- const hasReadme = async (pkgPath) => {
367
- const dir = join(pkgPath, "..");
368
- for (const name of ["README.md", "readme.md", "Readme.md"]) {
369
- try {
370
- await access(join(dir, name));
371
- return true;
372
- } catch {
373
- }
374
- }
375
- return false;
376
- };
377
- const results = await Promise.all(pkgFiles.map(hasReadme));
378
- const missingCount = results.filter((has) => !has).length;
379
- if (missingCount === 0) {
380
- return null;
381
- }
382
- const penalty = Math.min(missingCount, 15);
383
- return {
384
- type: "readme",
385
- severity: missingCount > 20 ? "high" : missingCount > 10 ? "medium" : "low",
386
- message: `Found ${missingCount} packages without README`,
387
- count: missingCount,
388
- penalty
389
- };
390
- }
391
- function calculateHealthScore(issues) {
392
- const baseScore = 100;
393
- const totalPenalty = issues.reduce((sum, issue) => sum + issue.penalty, 0);
394
- return Math.max(0, baseScore - totalPenalty);
341
+ async function calculateSize(rootDir) {
342
+ const files = await getSourceFiles(rootDir);
343
+ const fileStats = await Promise.all(files.map((f) => stat(f).catch(() => null)));
344
+ return fileStats.reduce((sum, s) => sum + (s ? s.size : 0), 0);
395
345
  }
396
- function scoreToGrade(score) {
397
- if (score >= 90) {
398
- return "A";
399
- }
400
- if (score >= 80) {
401
- return "B";
402
- }
403
- if (score >= 70) {
404
- return "C";
405
- }
406
- if (score >= 60) {
407
- return "D";
346
+ function gradeFromScore(score) {
347
+ for (const [g, { min }] of Object.entries(HEALTH_GRADES)) {
348
+ if (score >= min) {
349
+ return g;
350
+ }
408
351
  }
409
352
  return "F";
410
353
  }
411
- async function calculateHealth(rootDir) {
412
- const issues = [];
413
- const packageJsonFiles = await globby3("**/package.json", {
414
- cwd: rootDir,
415
- ignore: ["**/node_modules/**", "**/.git/**", "**/.kb/**", "**/dist/**"],
416
- absolute: true,
417
- deep: 6
418
- });
419
- const [duplicatesIssue, readmesIssue] = await Promise.all([
420
- checkDuplicateDependencies(rootDir, packageJsonFiles),
421
- checkMissingReadmes(rootDir, packageJsonFiles)
422
- ]);
423
- if (duplicatesIssue) {
424
- issues.push(duplicatesIssue);
425
- }
426
- if (readmesIssue) {
427
- issues.push(readmesIssue);
428
- }
429
- const score = calculateHealthScore(issues);
430
- const grade = scoreToGrade(score);
431
- return {
432
- score,
433
- grade,
434
- issues
354
+ function clamp(n) {
355
+ return Math.max(0, Math.min(100, Math.round(n)));
356
+ }
357
+ function scoreArchitecture(layering, coupling, thresholds) {
358
+ const details = [];
359
+ const layeringPenalty = Math.min(layering.totalViolations * 5, 60);
360
+ if (layering.totalViolations > 0) {
361
+ details.push(`${layering.totalViolations} layering violation(s) in ${layering.affectedPackages.length} package(s)`);
362
+ }
363
+ const highInstability = coupling.packages.filter((p) => p.instability > thresholds.instability);
364
+ const couplingPenalty = Math.min(highInstability.length * 3, 30);
365
+ if (highInstability.length > 0) {
366
+ details.push(`${highInstability.length} package(s) with instability > ${thresholds.instability}`);
367
+ }
368
+ const score = clamp(100 - layeringPenalty - couplingPenalty);
369
+ return { score, grade: gradeFromScore(score), details };
370
+ }
371
+ function scoreTypeScript(types) {
372
+ const details = [];
373
+ const totalAny = types.packages.reduce((s, p) => s + p.anyCount, 0);
374
+ const anyPenalty = Math.min(totalAny * 0.5, 50);
375
+ if (totalAny > 0) {
376
+ details.push(`${totalAny} \`any\` usage(s)`);
377
+ }
378
+ const totalIgnore = types.packages.reduce((s, p) => s + p.tsIgnoreCount, 0);
379
+ const ignorePenalty = Math.min(totalIgnore * 2, 30);
380
+ if (totalIgnore > 0) {
381
+ details.push(`${totalIgnore} @ts-ignore(s)`);
382
+ }
383
+ const errorPenalty = Math.min(types.totalErrors * 3, 20);
384
+ if (types.totalErrors > 0) {
385
+ details.push(`${types.totalErrors} type error(s)`);
386
+ }
387
+ const score = clamp(100 - anyPenalty - ignorePenalty - errorPenalty);
388
+ return { score, grade: gradeFromScore(score), details };
389
+ }
390
+ function scoreDeadCode(knip) {
391
+ const details = [];
392
+ const filePenalty = Math.min(knip.unusedFiles.length * 2, 50);
393
+ if (knip.unusedFiles.length > 0) {
394
+ details.push(`${knip.unusedFiles.length} unused file(s)`);
395
+ }
396
+ const exportPenalty = Math.min(knip.unusedExports.length * 0.5, 30);
397
+ if (knip.unusedExports.length > 0) {
398
+ details.push(`${knip.unusedExports.length} unused export(s)`);
399
+ }
400
+ const score = clamp(100 - filePenalty - exportPenalty);
401
+ return { score, grade: gradeFromScore(score), details };
402
+ }
403
+ function scoreDepHygiene(knip) {
404
+ const details = [];
405
+ const unusedPenalty = Math.min(knip.unusedDependencies.length * 5, 50);
406
+ if (knip.unusedDependencies.length > 0) {
407
+ details.push(`${knip.unusedDependencies.length} unused dep(s)`);
408
+ }
409
+ const unlistedPenalty = Math.min(knip.unlistedDependencies.length * 10, 40);
410
+ if (knip.unlistedDependencies.length > 0) {
411
+ details.push(`${knip.unlistedDependencies.length} unlisted dep(s)`);
412
+ }
413
+ const score = clamp(100 - unusedPenalty - unlistedPenalty);
414
+ return { score, grade: gradeFromScore(score), details };
415
+ }
416
+ function scoreTestCoverage(avgCoverage) {
417
+ if (avgCoverage === null) {
418
+ return { score: 100, grade: "A", details: ["no coverage data \u2014 skipped"] };
419
+ }
420
+ const score = clamp(avgCoverage);
421
+ const details = avgCoverage < 80 ? [`avg coverage ${avgCoverage.toFixed(1)}%`] : [];
422
+ return { score, grade: gradeFromScore(score), details };
423
+ }
424
+ function calculateHealth(input) {
425
+ const { layering, coupling, types, knip, avgTestCoverage, thresholds } = input;
426
+ const dimensions = {
427
+ architecture: scoreArchitecture(layering, coupling, thresholds),
428
+ typescript: scoreTypeScript(types),
429
+ deadCode: scoreDeadCode(knip),
430
+ depHygiene: scoreDepHygiene(knip),
431
+ testCoverage: scoreTestCoverage(avgTestCoverage)
435
432
  };
433
+ const score = clamp(
434
+ Object.entries(DIMENSION_WEIGHTS).reduce(
435
+ (sum, [key, weight]) => sum + dimensions[key].score * weight,
436
+ 0
437
+ )
438
+ );
439
+ return { score, grade: gradeFromScore(score), dimensions };
436
440
  }
437
441
  async function analyzeDuplicateDependencies(rootDir) {
438
- const packageJsonFiles = await globby3("**/package.json", {
442
+ const packageJsonFiles = await globby2("**/package.json", {
439
443
  cwd: rootDir,
440
444
  ignore: ["**/node_modules/**", "**/.git/**"],
441
445
  absolute: true
@@ -490,7 +494,7 @@ async function analyzeDuplicateDependencies(rootDir) {
490
494
  return duplicates;
491
495
  }
492
496
  async function analyzeUnusedDependencies(rootDir) {
493
- const packageJsonFiles = await globby3("**/package.json", {
497
+ const packageJsonFiles = await globby2("**/package.json", {
494
498
  cwd: rootDir,
495
499
  ignore: ["**/node_modules/**", "**/.git/**"],
496
500
  absolute: true
@@ -506,7 +510,7 @@ async function analyzeUnusedDependencies(rootDir) {
506
510
  ...pkg.dependencies,
507
511
  ...pkg.devDependencies
508
512
  };
509
- const sourceFiles = await globby3("src/**/*.{ts,tsx,js,jsx}", {
513
+ const sourceFiles = await globby2("src/**/*.{ts,tsx,js,jsx}", {
510
514
  cwd: pkgDir,
511
515
  absolute: true,
512
516
  ignore: ["**/*.test.*", "**/*.spec.*"]
@@ -543,7 +547,7 @@ async function analyzeUnusedDependencies(rootDir) {
543
547
  return unused;
544
548
  }
545
549
  async function analyzeMissingDependencies(rootDir) {
546
- const packageJsonFiles = await globby3("**/package.json", {
550
+ const packageJsonFiles = await globby2("**/package.json", {
547
551
  cwd: rootDir,
548
552
  ignore: ["**/node_modules/**", "**/.git/**"],
549
553
  absolute: true
@@ -573,7 +577,7 @@ async function analyzeMissingDependencies(rootDir) {
573
577
  ...Object.keys(pkg.dependencies || {}),
574
578
  ...Object.keys(pkg.devDependencies || {})
575
579
  ]);
576
- const sourceFiles = await globby3("src/**/*.{ts,tsx,js,jsx}", {
580
+ const sourceFiles = await globby2("src/**/*.{ts,tsx,js,jsx}", {
577
581
  cwd: pkgDir,
578
582
  absolute: true
579
583
  });
@@ -636,8 +640,8 @@ function findPackagesWithBuildScript(rootDir, filter) {
636
640
  if (!entry.isDirectory() || !entry.name.startsWith("kb-labs-")) {
637
641
  continue;
638
642
  }
639
- const repoPath = path8.join(rootDir, entry.name);
640
- const packagesDir = path8.join(repoPath, "packages");
643
+ const repoPath = path9.join(rootDir, entry.name);
644
+ const packagesDir = path9.join(repoPath, "packages");
641
645
  if (!fs8.existsSync(packagesDir)) {
642
646
  continue;
643
647
  }
@@ -646,7 +650,7 @@ function findPackagesWithBuildScript(rootDir, filter) {
646
650
  if (!pkgDir.isDirectory()) {
647
651
  continue;
648
652
  }
649
- const packageJsonPath = path8.join(packagesDir, pkgDir.name, "package.json");
653
+ const packageJsonPath = path9.join(packagesDir, pkgDir.name, "package.json");
650
654
  if (fs8.existsSync(packageJsonPath)) {
651
655
  const pkgJson = JSON.parse(fs8.readFileSync(packageJsonPath, "utf-8"));
652
656
  if (!pkgJson.scripts?.build) {
@@ -662,8 +666,8 @@ function findPackagesWithBuildScript(rootDir, filter) {
662
666
  return packages;
663
667
  }
664
668
  function isDistStale(packageDir) {
665
- const distFile = path8.join(packageDir, "dist/index.js");
666
- const srcDir = path8.join(packageDir, "src");
669
+ const distFile = path9.join(packageDir, "dist/index.js");
670
+ const srcDir = path9.join(packageDir, "src");
667
671
  if (!fs8.existsSync(distFile)) {
668
672
  return { stale: false };
669
673
  }
@@ -675,7 +679,7 @@ function isDistStale(packageDir) {
675
679
  function walkDir(dir) {
676
680
  const entries = fs8.readdirSync(dir, { withFileTypes: true });
677
681
  for (const entry of entries) {
678
- const fullPath = path8.join(dir, entry.name);
682
+ const fullPath = path9.join(dir, entry.name);
679
683
  if (entry.isDirectory()) {
680
684
  walkDir(fullPath);
681
685
  } else if (/\.(ts|tsx|js|jsx)$/.test(entry.name)) {
@@ -725,7 +729,7 @@ async function checkBuilds(rootDir, options = {}) {
725
729
  for (const pkgPath of packagePaths) {
726
730
  const pkgJson = JSON.parse(fs8.readFileSync(pkgPath, "utf-8"));
727
731
  const packageName = pkgJson.name;
728
- const packageDir = path8.dirname(pkgPath);
732
+ const packageDir = path9.dirname(pkgPath);
729
733
  const staleCheck = isDistStale(packageDir);
730
734
  if (staleCheck.stale) {
731
735
  result.staleBuilds.push({
@@ -759,8 +763,8 @@ function findPackagesWithTsConfig(rootDir, filter) {
759
763
  if (!entry.isDirectory() || !entry.name.startsWith("kb-labs-")) {
760
764
  continue;
761
765
  }
762
- const repoPath = path8.join(rootDir, entry.name);
763
- const packagesDir = path8.join(repoPath, "packages");
766
+ const repoPath = path9.join(rootDir, entry.name);
767
+ const packagesDir = path9.join(repoPath, "packages");
764
768
  if (!fs8.existsSync(packagesDir)) {
765
769
  continue;
766
770
  }
@@ -769,8 +773,8 @@ function findPackagesWithTsConfig(rootDir, filter) {
769
773
  if (!pkgDir.isDirectory()) {
770
774
  continue;
771
775
  }
772
- const packageJsonPath = path8.join(packagesDir, pkgDir.name, "package.json");
773
- const tsconfigPath = path8.join(packagesDir, pkgDir.name, "tsconfig.json");
776
+ const packageJsonPath = path9.join(packagesDir, pkgDir.name, "package.json");
777
+ const tsconfigPath = path9.join(packagesDir, pkgDir.name, "tsconfig.json");
774
778
  if (fs8.existsSync(packageJsonPath) && fs8.existsSync(tsconfigPath)) {
775
779
  const pkgJson = JSON.parse(fs8.readFileSync(packageJsonPath, "utf-8"));
776
780
  if (filter && !pkgJson.name.includes(filter) && !pkgDir.name.includes(filter)) {
@@ -778,7 +782,7 @@ function findPackagesWithTsConfig(rootDir, filter) {
778
782
  }
779
783
  packages.push({
780
784
  name: pkgJson.name,
781
- dir: path8.dirname(packageJsonPath),
785
+ dir: path9.dirname(packageJsonPath),
782
786
  tsconfigPath
783
787
  });
784
788
  }
@@ -804,7 +808,7 @@ function createProgram(packageDir, tsconfigPath) {
804
808
  rootNames: parsedConfig.fileNames,
805
809
  options: parsedConfig.options
806
810
  });
807
- } catch (err) {
811
+ } catch {
808
812
  return null;
809
813
  }
810
814
  }
@@ -910,8 +914,8 @@ function findPackagesWithTests(rootDir, filter) {
910
914
  if (!entry.isDirectory() || !entry.name.startsWith("kb-labs-")) {
911
915
  continue;
912
916
  }
913
- const repoPath = path8.join(rootDir, entry.name);
914
- const packagesDir = path8.join(repoPath, "packages");
917
+ const repoPath = path9.join(rootDir, entry.name);
918
+ const packagesDir = path9.join(repoPath, "packages");
915
919
  if (!fs8.existsSync(packagesDir)) {
916
920
  continue;
917
921
  }
@@ -920,7 +924,7 @@ function findPackagesWithTests(rootDir, filter) {
920
924
  if (!pkgDir.isDirectory()) {
921
925
  continue;
922
926
  }
923
- const packageJsonPath = path8.join(packagesDir, pkgDir.name, "package.json");
927
+ const packageJsonPath = path9.join(packagesDir, pkgDir.name, "package.json");
924
928
  if (fs8.existsSync(packageJsonPath)) {
925
929
  const pkgJson = JSON.parse(fs8.readFileSync(packageJsonPath, "utf-8"));
926
930
  if (filter && !pkgJson.name.includes(filter) && !pkgDir.name.includes(filter)) {
@@ -928,7 +932,7 @@ function findPackagesWithTests(rootDir, filter) {
928
932
  }
929
933
  packages.push({
930
934
  name: pkgJson.name,
931
- dir: path8.dirname(packageJsonPath),
935
+ dir: path9.dirname(packageJsonPath),
932
936
  hasTestScript: !!pkgJson.scripts?.test
933
937
  });
934
938
  }
@@ -985,7 +989,7 @@ function parseTestOutput(output) {
985
989
  return {};
986
990
  }
987
991
  function readCoverage(packageDir) {
988
- const coveragePath = path8.join(packageDir, "coverage", "coverage-summary.json");
992
+ const coveragePath = path9.join(packageDir, "coverage", "coverage-summary.json");
989
993
  if (!fs8.existsSync(coveragePath)) {
990
994
  return null;
991
995
  }
@@ -1122,7 +1126,7 @@ async function collectEntryPoints(packageDir, packageJson) {
1122
1126
  await collectTestFiles(packageDir, aliveByConvention);
1123
1127
  collectConfigFiles(packageDir, aliveByConvention);
1124
1128
  if (entryFiles.size === 0) {
1125
- const defaultEntry = path8.join(packageDir, "src", "index.ts");
1129
+ const defaultEntry = path9.join(packageDir, "src", "index.ts");
1126
1130
  if (fs8.existsSync(defaultEntry)) {
1127
1131
  entryFiles.add(defaultEntry);
1128
1132
  warnings.push("No entry points found, using src/index.ts as default");
@@ -1136,11 +1140,11 @@ function distPathToSrcPath(distPath, packageDir) {
1136
1140
  normalized = "src/" + normalized.slice(5);
1137
1141
  }
1138
1142
  normalized = normalized.replace(/\.d\.ts$/, ".ts").replace(/\.js$/, ".ts").replace(/\.mjs$/, ".ts").replace(/\.cjs$/, ".ts");
1139
- const absolute = path8.resolve(packageDir, normalized);
1143
+ const absolute = path9.resolve(packageDir, normalized);
1140
1144
  if (fs8.existsSync(absolute)) {
1141
1145
  return absolute;
1142
1146
  }
1143
- const indexPath = path8.join(absolute.replace(/\.ts$/, ""), "index.ts");
1147
+ const indexPath = path9.join(absolute.replace(/\.ts$/, ""), "index.ts");
1144
1148
  if (fs8.existsSync(indexPath)) {
1145
1149
  return indexPath;
1146
1150
  }
@@ -1219,7 +1223,7 @@ async function collectTsupEntries(packageDir, entryFiles, warnings) {
1219
1223
  ];
1220
1224
  let foundAny = false;
1221
1225
  for (const configName of tsupConfigs) {
1222
- const configPath = path8.join(packageDir, configName);
1226
+ const configPath = path9.join(packageDir, configName);
1223
1227
  if (!fs8.existsSync(configPath)) {
1224
1228
  continue;
1225
1229
  }
@@ -1229,7 +1233,7 @@ async function collectTsupEntries(packageDir, entryFiles, warnings) {
1229
1233
  const entries = parseTsupEntries(content);
1230
1234
  for (const entry of entries) {
1231
1235
  if (entry.includes("*") || entry.includes("{")) {
1232
- const expanded = await globby3(entry, {
1236
+ const expanded = await globby2(entry, {
1233
1237
  cwd: packageDir,
1234
1238
  absolute: true
1235
1239
  });
@@ -1237,7 +1241,7 @@ async function collectTsupEntries(packageDir, entryFiles, warnings) {
1237
1241
  entryFiles.add(file);
1238
1242
  }
1239
1243
  } else {
1240
- const absolute = path8.resolve(packageDir, entry);
1244
+ const absolute = path9.resolve(packageDir, entry);
1241
1245
  if (fs8.existsSync(absolute)) {
1242
1246
  entryFiles.add(absolute);
1243
1247
  }
@@ -1277,7 +1281,7 @@ function parseTsupEntries(content) {
1277
1281
  return entries;
1278
1282
  }
1279
1283
  function collectManifestHandlers(packageDir, entryFiles, warnings) {
1280
- const manifestPath = path8.join(packageDir, "src", "manifest.ts");
1284
+ const manifestPath = path9.join(packageDir, "src", "manifest.ts");
1281
1285
  if (!fs8.existsSync(manifestPath)) {
1282
1286
  return;
1283
1287
  }
@@ -1287,7 +1291,7 @@ function collectManifestHandlers(packageDir, entryFiles, warnings) {
1287
1291
  for (const handlerPath of handlers) {
1288
1292
  const pathOnly = handlerPath.split("#")[0] ?? handlerPath;
1289
1293
  const srcRelative = "src/" + pathOnly.replace(/^\.\//, "").replace(/\.js$/, ".ts");
1290
- const absolute = path8.resolve(packageDir, srcRelative);
1294
+ const absolute = path9.resolve(packageDir, srcRelative);
1291
1295
  if (fs8.existsSync(absolute)) {
1292
1296
  entryFiles.add(absolute);
1293
1297
  }
@@ -1316,11 +1320,11 @@ function parseManifestHandlers(content) {
1316
1320
  return handlers;
1317
1321
  }
1318
1322
  async function collectDynamicImportTargets(packageDir, entryFiles, _warnings) {
1319
- const srcDir = path8.join(packageDir, "src");
1323
+ const srcDir = path9.join(packageDir, "src");
1320
1324
  if (!fs8.existsSync(srcDir)) {
1321
1325
  return;
1322
1326
  }
1323
- const sourceFiles = await globby3("**/*.{ts,tsx}", {
1327
+ const sourceFiles = await globby2("**/*.{ts,tsx}", {
1324
1328
  cwd: srcDir,
1325
1329
  absolute: true,
1326
1330
  ignore: ["**/*.test.ts", "**/*.spec.ts", "**/__tests__/**"]
@@ -1345,11 +1349,11 @@ async function collectDynamicImportTargets(packageDir, entryFiles, _warnings) {
1345
1349
  }
1346
1350
  }
1347
1351
  async function collectTestFiles(packageDir, aliveByConvention) {
1348
- const srcDir = path8.join(packageDir, "src");
1352
+ const srcDir = path9.join(packageDir, "src");
1349
1353
  if (!fs8.existsSync(srcDir)) {
1350
1354
  return;
1351
1355
  }
1352
- const testFiles = await globby3(
1356
+ const testFiles = await globby2(
1353
1357
  ["**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts", "**/*.spec.tsx", "**/__tests__/**/*.ts"],
1354
1358
  { cwd: srcDir, absolute: true }
1355
1359
  );
@@ -1359,15 +1363,15 @@ async function collectTestFiles(packageDir, aliveByConvention) {
1359
1363
  }
1360
1364
  function collectConfigFiles(packageDir, aliveByConvention) {
1361
1365
  for (const pattern of CONFIG_FILE_PATTERNS) {
1362
- const configPath = path8.join(packageDir, pattern);
1366
+ const configPath = path9.join(packageDir, pattern);
1363
1367
  if (fs8.existsSync(configPath)) {
1364
1368
  aliveByConvention.add(configPath);
1365
1369
  }
1366
1370
  }
1367
1371
  }
1368
1372
  function resolveFilePath(importPath, fromFile) {
1369
- const dir = path8.dirname(fromFile);
1370
- const base = path8.resolve(dir, importPath);
1373
+ const dir = path9.dirname(fromFile);
1374
+ const base = path9.resolve(dir, importPath);
1371
1375
  const tsPath = base.replace(/\.js$/, ".ts");
1372
1376
  if (fs8.existsSync(tsPath)) {
1373
1377
  return tsPath;
@@ -1381,8 +1385,8 @@ function resolveFilePath(importPath, fromFile) {
1381
1385
  if (fs8.existsSync(base + ".tsx")) {
1382
1386
  return base + ".tsx";
1383
1387
  }
1384
- if (fs8.existsSync(path8.join(base, "index.ts"))) {
1385
- return path8.join(base, "index.ts");
1388
+ if (fs8.existsSync(path9.join(base, "index.ts"))) {
1389
+ return path9.join(base, "index.ts");
1386
1390
  }
1387
1391
  return null;
1388
1392
  }
@@ -1415,8 +1419,8 @@ function isRelativeImport(specifier) {
1415
1419
  return specifier.startsWith("./") || specifier.startsWith("../");
1416
1420
  }
1417
1421
  function resolveRelativeImport(specifier, sourceFile) {
1418
- const dir = path8.dirname(sourceFile);
1419
- const basePath = path8.resolve(dir, specifier);
1422
+ const dir = path9.dirname(sourceFile);
1423
+ const basePath = path9.resolve(dir, specifier);
1420
1424
  if (specifier.endsWith(".js")) {
1421
1425
  const tsPath = basePath.slice(0, -3) + ".ts";
1422
1426
  if (fs8.existsSync(tsPath)) {
@@ -1438,14 +1442,14 @@ function resolveRelativeImport(specifier, sourceFile) {
1438
1442
  }
1439
1443
  if (fs8.existsSync(basePath) && fs8.statSync(basePath).isDirectory()) {
1440
1444
  for (const ext of RESOLVE_EXTENSIONS) {
1441
- const indexPath = path8.join(basePath, "index" + ext);
1445
+ const indexPath = path9.join(basePath, "index" + ext);
1442
1446
  if (fs8.existsSync(indexPath)) {
1443
1447
  return indexPath;
1444
1448
  }
1445
1449
  }
1446
1450
  }
1447
1451
  for (const ext of RESOLVE_EXTENSIONS) {
1448
- const indexPath = path8.join(basePath, "index" + ext);
1452
+ const indexPath = path9.join(basePath, "index" + ext);
1449
1453
  if (fs8.existsSync(indexPath)) {
1450
1454
  return indexPath;
1451
1455
  }
@@ -1530,8 +1534,8 @@ function findPackagesInMonorepo(rootDir, filter) {
1530
1534
  if (!entry.isDirectory() || !entry.name.startsWith("kb-labs-")) {
1531
1535
  continue;
1532
1536
  }
1533
- const repoPath = path8.join(rootDir, entry.name);
1534
- const packagesDir = path8.join(repoPath, "packages");
1537
+ const repoPath = path9.join(rootDir, entry.name);
1538
+ const packagesDir = path9.join(repoPath, "packages");
1535
1539
  if (!fs8.existsSync(packagesDir)) {
1536
1540
  continue;
1537
1541
  }
@@ -1540,15 +1544,15 @@ function findPackagesInMonorepo(rootDir, filter) {
1540
1544
  if (!pkgDir.isDirectory()) {
1541
1545
  continue;
1542
1546
  }
1543
- const packageJsonPath = path8.join(packagesDir, pkgDir.name, "package.json");
1547
+ const packageJsonPath = path9.join(packagesDir, pkgDir.name, "package.json");
1544
1548
  if (!fs8.existsSync(packageJsonPath)) {
1545
1549
  continue;
1546
1550
  }
1547
1551
  try {
1548
1552
  const pkgJson = JSON.parse(fs8.readFileSync(packageJsonPath, "utf-8"));
1549
1553
  const pkgName = pkgJson.name || pkgDir.name;
1550
- const packageDir = path8.join(packagesDir, pkgDir.name);
1551
- const srcDir = path8.join(packageDir, "src");
1554
+ const packageDir = path9.join(packagesDir, pkgDir.name);
1555
+ const srcDir = path9.join(packageDir, "src");
1552
1556
  if (!fs8.existsSync(srcDir)) {
1553
1557
  continue;
1554
1558
  }
@@ -1564,8 +1568,8 @@ function findPackagesInMonorepo(rootDir, filter) {
1564
1568
  }
1565
1569
  async function analyzePackage(pkg) {
1566
1570
  const { packageDir, packageJson, packageName } = pkg;
1567
- const srcDir = path8.join(packageDir, "src");
1568
- const allSourceFiles = await globby3("**/*.{ts,tsx}", {
1571
+ const srcDir = path9.join(packageDir, "src");
1572
+ const allSourceFiles = await globby2("**/*.{ts,tsx}", {
1569
1573
  cwd: srcDir,
1570
1574
  absolute: true,
1571
1575
  ignore: ["**/*.d.ts"]
@@ -1586,7 +1590,7 @@ async function analyzePackage(pkg) {
1586
1590
  totalFiles: allSourceFiles.length,
1587
1591
  aliveFiles: allSourceFiles.length,
1588
1592
  deadFiles: [],
1589
- entryPoints: [...entryFiles].map((f) => path8.relative(packageDir, f)),
1593
+ entryPoints: [...entryFiles].map((f) => path9.relative(packageDir, f)),
1590
1594
  graphEdgeCount: 0,
1591
1595
  warnings: [...warnings, "FAIL-OPEN: All files treated as alive due to config parse errors"]
1592
1596
  };
@@ -1602,7 +1606,7 @@ async function analyzePackage(pkg) {
1602
1606
  const stats = safeFileStat(file);
1603
1607
  deadFiles.push({
1604
1608
  absolutePath: file,
1605
- relativePath: path8.relative(packageDir, file),
1609
+ relativePath: path9.relative(packageDir, file),
1606
1610
  packageName,
1607
1611
  packageDir,
1608
1612
  sizeBytes: stats?.size ?? 0
@@ -1616,7 +1620,7 @@ async function analyzePackage(pkg) {
1616
1620
  totalFiles: allSourceFiles.length,
1617
1621
  aliveFiles: allSourceFiles.length - deadFiles.length,
1618
1622
  deadFiles,
1619
- entryPoints: [...entryFiles].map((f) => path8.relative(packageDir, f)),
1623
+ entryPoints: [...entryFiles].map((f) => path9.relative(packageDir, f)),
1620
1624
  graphEdgeCount,
1621
1625
  warnings
1622
1626
  };
@@ -1650,14 +1654,14 @@ function findPotentialEmptyDirs(results, _rootDir) {
1650
1654
  }
1651
1655
  const deadByDir = /* @__PURE__ */ new Map();
1652
1656
  for (const deadFile of pkg.deadFiles) {
1653
- const dir = path8.dirname(deadFile.absolutePath);
1657
+ const dir = path9.dirname(deadFile.absolutePath);
1654
1658
  deadByDir.set(dir, (deadByDir.get(dir) ?? 0) + 1);
1655
1659
  }
1656
1660
  for (const [dir, deadCount] of deadByDir) {
1657
1661
  try {
1658
1662
  const allFiles = fs8.readdirSync(dir);
1659
1663
  if (allFiles.length === deadCount) {
1660
- emptyDirs.push(path8.relative(pkg.packageDir, dir));
1664
+ emptyDirs.push(path9.relative(pkg.packageDir, dir));
1661
1665
  }
1662
1666
  } catch {
1663
1667
  }
@@ -1672,11 +1676,65 @@ function safeFileStat(filePath) {
1672
1676
  return null;
1673
1677
  }
1674
1678
  }
1679
+
1680
+ // src/dead-code/knip-runner.ts
1681
+ async function runKnip(opts) {
1682
+ const { shell, rootDir } = opts;
1683
+ const result = await shell.exec("pnpm", ["knip", "--reporter", "json"], { cwd: rootDir }).catch(() => null);
1684
+ if (!result) {
1685
+ return emptyReport();
1686
+ }
1687
+ const raw = result.stdout.trim();
1688
+ if (!raw || raw === "{}" || raw === "[]") {
1689
+ return emptyReport();
1690
+ }
1691
+ let parsed;
1692
+ try {
1693
+ parsed = JSON.parse(raw);
1694
+ } catch {
1695
+ return emptyReport();
1696
+ }
1697
+ return parseKnipOutput(parsed, rootDir);
1698
+ }
1699
+ function parseKnipOutput(out, rootDir) {
1700
+ const unusedFiles = (out.files ?? []).map(
1701
+ (f) => f.startsWith("/") ? f : `${rootDir}/${f}`
1702
+ );
1703
+ const unusedExports = (out.exports ?? []).flatMap(
1704
+ (entry) => (entry.exports ?? []).map((exp) => ({
1705
+ file: entry.file,
1706
+ symbol: exp.symbol
1707
+ }))
1708
+ );
1709
+ const unusedDependencies = (out.dependencies ?? []).flatMap(
1710
+ (dep) => (dep.packages ?? [rootDir]).map((workspace) => ({
1711
+ package: dep.name,
1712
+ workspace
1713
+ }))
1714
+ );
1715
+ const unlistedDependencies = (out.unlisted ?? []).flatMap(
1716
+ (dep) => (dep.packages ?? [rootDir]).map((workspace) => ({
1717
+ package: dep.name,
1718
+ workspace
1719
+ }))
1720
+ );
1721
+ const totalIssues = unusedFiles.length + unusedExports.length + unusedDependencies.length + unlistedDependencies.length;
1722
+ return { unusedFiles, unusedExports, unusedDependencies, unlistedDependencies, totalIssues };
1723
+ }
1724
+ function emptyReport() {
1725
+ return {
1726
+ unusedFiles: [],
1727
+ unusedExports: [],
1728
+ unusedDependencies: [],
1729
+ unlistedDependencies: [],
1730
+ totalIssues: 0
1731
+ };
1732
+ }
1675
1733
  var BACKUP_DIR = ".dead-code-backup";
1676
1734
  async function removeDeadFiles(rootDir, scanResult, options) {
1677
1735
  const dryRun = options?.dryRun ?? false;
1678
1736
  const backupId = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1679
- const backupPath = path8.join(rootDir, BACKUP_DIR, backupId);
1737
+ const backupPath = path9.join(rootDir, BACKUP_DIR, backupId);
1680
1738
  const gitSha = safeExec("git rev-parse HEAD", rootDir) ?? "unknown";
1681
1739
  const gitBranch = safeExec("git rev-parse --abbrev-ref HEAD", rootDir) ?? "unknown";
1682
1740
  const allDeadFiles = [];
@@ -1711,7 +1769,7 @@ async function removeDeadFiles(rootDir, scanResult, options) {
1711
1769
  gitBranch,
1712
1770
  removedFiles: allDeadFiles.map((f) => ({
1713
1771
  originalPath: f.absolutePath,
1714
- backupPath: path8.relative(rootDir, f.absolutePath),
1772
+ backupPath: path9.relative(rootDir, f.absolutePath),
1715
1773
  packageName: f.packageName,
1716
1774
  sizeBytes: f.sizeBytes
1717
1775
  })),
@@ -1731,45 +1789,45 @@ async function removeDeadFiles(rootDir, scanResult, options) {
1731
1789
  manifest
1732
1790
  };
1733
1791
  }
1734
- const filesDir = path8.join(backupPath, "files");
1792
+ const filesDir = path9.join(backupPath, "files");
1735
1793
  for (const deadFile of allDeadFiles) {
1736
- const relPath = path8.relative(rootDir, deadFile.absolutePath);
1737
- const destPath = path8.join(filesDir, relPath);
1738
- const destDir = path8.dirname(destPath);
1794
+ const relPath = path9.relative(rootDir, deadFile.absolutePath);
1795
+ const destPath = path9.join(filesDir, relPath);
1796
+ const destDir = path9.dirname(destPath);
1739
1797
  fs8.mkdirSync(destDir, { recursive: true });
1740
1798
  fs8.copyFileSync(deadFile.absolutePath, destPath);
1741
1799
  }
1742
1800
  fs8.writeFileSync(
1743
- path8.join(backupPath, "manifest.json"),
1801
+ path9.join(backupPath, "manifest.json"),
1744
1802
  JSON.stringify(manifest, null, 2) + "\n"
1745
1803
  );
1746
1804
  for (const deadFile of allDeadFiles) {
1747
1805
  fs8.unlinkSync(deadFile.absolutePath);
1748
1806
  }
1749
1807
  const removedDirs = [];
1750
- const deadFileDirs = new Set(allDeadFiles.map((f) => path8.dirname(f.absolutePath)));
1808
+ const deadFileDirs = new Set(allDeadFiles.map((f) => path9.dirname(f.absolutePath)));
1751
1809
  for (const dir of deadFileDirs) {
1752
1810
  removeEmptyDirsUpward(dir, rootDir, removedDirs);
1753
1811
  }
1754
- manifest.removedEmptyDirs = removedDirs.map((d) => path8.relative(rootDir, d));
1812
+ manifest.removedEmptyDirs = removedDirs.map((d) => path9.relative(rootDir, d));
1755
1813
  const deletedPaths = new Set(allDeadFiles.map((f) => f.absolutePath));
1756
1814
  let exportsCleanedUp = 0;
1757
1815
  for (const pkg of scanResult.packages) {
1758
1816
  if (pkg.deadFiles.length === 0) {
1759
1817
  continue;
1760
1818
  }
1761
- const pkgJsonPath = path8.join(pkg.packageDir, "package.json");
1819
+ const pkgJsonPath = path9.join(pkg.packageDir, "package.json");
1762
1820
  const cleaned = cleanPackageJsonExports(pkgJsonPath, deletedPaths, pkg.packageDir);
1763
1821
  if (cleaned.length > 0) {
1764
1822
  manifest.cleanedExports.push({
1765
- packageJsonPath: path8.relative(rootDir, pkgJsonPath),
1823
+ packageJsonPath: path9.relative(rootDir, pkgJsonPath),
1766
1824
  removedExportKeys: cleaned
1767
1825
  });
1768
1826
  exportsCleanedUp += cleaned.length;
1769
1827
  }
1770
1828
  }
1771
1829
  fs8.writeFileSync(
1772
- path8.join(backupPath, "manifest.json"),
1830
+ path9.join(backupPath, "manifest.json"),
1773
1831
  JSON.stringify(manifest, null, 2) + "\n"
1774
1832
  );
1775
1833
  return {
@@ -1783,8 +1841,8 @@ async function removeDeadFiles(rootDir, scanResult, options) {
1783
1841
  };
1784
1842
  }
1785
1843
  async function restoreFromBackup(rootDir, backupId) {
1786
- const backupPath = path8.join(rootDir, BACKUP_DIR, backupId);
1787
- const manifestPath = path8.join(backupPath, "manifest.json");
1844
+ const backupPath = path9.join(rootDir, BACKUP_DIR, backupId);
1845
+ const manifestPath = path9.join(backupPath, "manifest.json");
1788
1846
  if (!fs8.existsSync(manifestPath)) {
1789
1847
  throw new Error(`Backup not found: ${backupId}`);
1790
1848
  }
@@ -1793,18 +1851,18 @@ async function restoreFromBackup(rootDir, backupId) {
1793
1851
  );
1794
1852
  let restoredFiles = 0;
1795
1853
  for (const entry of manifest.removedFiles) {
1796
- const backupFilePath = path8.join(backupPath, "files", entry.backupPath);
1854
+ const backupFilePath = path9.join(backupPath, "files", entry.backupPath);
1797
1855
  if (!fs8.existsSync(backupFilePath)) {
1798
1856
  continue;
1799
1857
  }
1800
- const parentDir = path8.dirname(entry.originalPath);
1858
+ const parentDir = path9.dirname(entry.originalPath);
1801
1859
  fs8.mkdirSync(parentDir, { recursive: true });
1802
1860
  fs8.copyFileSync(backupFilePath, entry.originalPath);
1803
1861
  restoredFiles++;
1804
1862
  }
1805
1863
  let restoredExports = 0;
1806
1864
  for (const exportEntry of manifest.cleanedExports) {
1807
- const pkgJsonPath = path8.resolve(rootDir, exportEntry.packageJsonPath);
1865
+ const pkgJsonPath = path9.resolve(rootDir, exportEntry.packageJsonPath);
1808
1866
  if (!fs8.existsSync(pkgJsonPath)) {
1809
1867
  continue;
1810
1868
  }
@@ -1813,7 +1871,7 @@ async function restoreFromBackup(rootDir, backupId) {
1813
1871
  return { restoredFiles, restoredExports };
1814
1872
  }
1815
1873
  function listBackups(rootDir) {
1816
- const backupDir = path8.join(rootDir, BACKUP_DIR);
1874
+ const backupDir = path9.join(rootDir, BACKUP_DIR);
1817
1875
  if (!fs8.existsSync(backupDir)) {
1818
1876
  return [];
1819
1877
  }
@@ -1823,7 +1881,7 @@ function listBackups(rootDir) {
1823
1881
  if (!entry.isDirectory()) {
1824
1882
  continue;
1825
1883
  }
1826
- const manifestPath = path8.join(backupDir, entry.name, "manifest.json");
1884
+ const manifestPath = path9.join(backupDir, entry.name, "manifest.json");
1827
1885
  if (!fs8.existsSync(manifestPath)) {
1828
1886
  continue;
1829
1887
  }
@@ -1855,7 +1913,7 @@ function removeEmptyDirsUpward(dir, rootBoundary, removed) {
1855
1913
  }
1856
1914
  fs8.rmdirSync(current);
1857
1915
  removed.push(current);
1858
- current = path8.dirname(current);
1916
+ current = path9.dirname(current);
1859
1917
  } catch {
1860
1918
  break;
1861
1919
  }
@@ -1915,9 +1973,324 @@ function distToSrcForExportCheck(distPath, packageDir) {
1915
1973
  }
1916
1974
  normalized = "src/" + normalized.slice(5);
1917
1975
  normalized = normalized.replace(/\.d\.ts$/, ".ts").replace(/\.js$/, ".ts").replace(/\.mjs$/, ".ts").replace(/\.cjs$/, ".ts");
1918
- return path8.resolve(packageDir, normalized);
1976
+ return path9.resolve(packageDir, normalized);
1977
+ }
1978
+ function resolveLayer(pkgDir, rootDir, layerMap = defaultQualityConfig.layers) {
1979
+ const rel = path9.relative(rootDir, pkgDir).replace(/\\/g, "/");
1980
+ for (const [prefix, layer] of Object.entries(layerMap)) {
1981
+ if (rel.startsWith(prefix)) {
1982
+ return layer;
1983
+ }
1984
+ }
1985
+ return -1;
1986
+ }
1987
+ function scanWorkspace(rootDir, layerMap = defaultQualityConfig.layers) {
1988
+ const packages = [];
1989
+ if (!fs8.existsSync(rootDir)) {
1990
+ return packages;
1991
+ }
1992
+ const topDirs = ["core", "sdk", "shared", "cli", "adapters", "studio", "infra"];
1993
+ const pluginDirs = ["plugins"];
1994
+ const collect = (dir, maxDepth) => {
1995
+ if (!fs8.existsSync(dir)) {
1996
+ return;
1997
+ }
1998
+ const pkgJson = path9.join(dir, "package.json");
1999
+ if (fs8.existsSync(pkgJson)) {
2000
+ tryAddPackage(pkgJson);
2001
+ return;
2002
+ }
2003
+ if (maxDepth <= 0) {
2004
+ return;
2005
+ }
2006
+ for (const entry of fs8.readdirSync(dir, { withFileTypes: true })) {
2007
+ if (!entry.isDirectory() || entry.name === "node_modules" || entry.name.startsWith(".")) {
2008
+ continue;
2009
+ }
2010
+ collect(path9.join(dir, entry.name), maxDepth - 1);
2011
+ }
2012
+ };
2013
+ const tryAddPackage = (pkgJsonPath) => {
2014
+ try {
2015
+ const raw = JSON.parse(fs8.readFileSync(pkgJsonPath, "utf-8"));
2016
+ if (!raw.name) {
2017
+ return;
2018
+ }
2019
+ const dir = path9.dirname(pkgJsonPath);
2020
+ packages.push({
2021
+ name: raw.name,
2022
+ dir,
2023
+ packageJsonPath: pkgJsonPath,
2024
+ layer: resolveLayer(dir, rootDir, layerMap),
2025
+ deps: Object.keys(raw.dependencies ?? {}),
2026
+ devDeps: Object.keys(raw.devDependencies ?? {})
2027
+ });
2028
+ } catch {
2029
+ }
2030
+ };
2031
+ for (const top of topDirs) {
2032
+ collect(path9.join(rootDir, top), 1);
2033
+ }
2034
+ for (const top of pluginDirs) {
2035
+ collect(path9.join(rootDir, top), 2);
2036
+ }
2037
+ return packages;
2038
+ }
2039
+ function buildPackageMap(packages) {
2040
+ return new Map(packages.map((p) => [p.name, p]));
2041
+ }
2042
+ function extractImportSpecifiers(filePath) {
2043
+ let source;
2044
+ try {
2045
+ source = fs8.readFileSync(filePath, "utf-8");
2046
+ } catch {
2047
+ return [];
2048
+ }
2049
+ const sf = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true);
2050
+ const specifiers = [];
2051
+ const visit = (node) => {
2052
+ if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
2053
+ specifiers.push(node.moduleSpecifier.text);
2054
+ }
2055
+ ts.forEachChild(node, visit);
2056
+ };
2057
+ visit(sf);
2058
+ return specifiers;
2059
+ }
2060
+ function resolveToPackageName(specifier, workspaceNames) {
2061
+ if (specifier.startsWith(".") || specifier.startsWith("node:")) {
2062
+ return null;
2063
+ }
2064
+ const scoped = specifier.match(/^(@[^/]+\/[^/]+)/);
2065
+ if (scoped) {
2066
+ const pkg = scoped[1];
2067
+ return workspaceNames.has(pkg) ? pkg : null;
2068
+ }
2069
+ const name = specifier.split("/")[0];
2070
+ if (name) {
2071
+ return workspaceNames.has(name) ? name : null;
2072
+ }
2073
+ return null;
1919
2074
  }
2075
+ async function analyzeLayering(opts) {
2076
+ const { rootDir, layerMap } = opts;
2077
+ const packages = scanWorkspace(rootDir, layerMap);
2078
+ const packageMap = buildPackageMap(packages);
2079
+ const workspaceNames = new Set(packageMap.keys());
2080
+ const violations = [];
2081
+ await Promise.all(
2082
+ packages.filter((pkg) => pkg.layer >= 0).map(async (pkg) => {
2083
+ const srcDir = path9.join(pkg.dir, "src");
2084
+ if (!fs8.existsSync(srcDir)) {
2085
+ return;
2086
+ }
2087
+ const files = await globby2("**/*.{ts,tsx}", {
2088
+ cwd: srcDir,
2089
+ ignore: ["**/*.test.ts", "**/*.spec.ts", "**/__tests__/**", "**/node_modules/**"],
2090
+ absolute: true,
2091
+ deep: 10
2092
+ });
2093
+ for (const file of files) {
2094
+ const specifiers = extractImportSpecifiers(file);
2095
+ for (const spec of specifiers) {
2096
+ const importedPkg = resolveToPackageName(spec, workspaceNames);
2097
+ if (!importedPkg) {
2098
+ continue;
2099
+ }
2100
+ const importedLayer = packageMap.get(importedPkg)?.layer ?? resolveLayer(importedPkg, rootDir, layerMap);
2101
+ if (importedLayer < 0) {
2102
+ continue;
2103
+ }
2104
+ if (importedLayer > pkg.layer) {
2105
+ violations.push({
2106
+ file,
2107
+ fromPackage: pkg.name,
2108
+ fromLayer: pkg.layer,
2109
+ toPackage: importedPkg,
2110
+ toLayer: importedLayer,
2111
+ importSpecifier: spec
2112
+ });
2113
+ }
2114
+ }
2115
+ }
2116
+ })
2117
+ );
2118
+ const affectedPackages = [...new Set(violations.map((v) => v.fromPackage))];
2119
+ return {
2120
+ violations,
2121
+ totalViolations: violations.length,
2122
+ affectedPackages,
2123
+ layerMap: layerMap ?? defaultQualityConfig.layers
2124
+ };
2125
+ }
2126
+
2127
+ // src/architecture/coupling.ts
2128
+ function analyzeCoupling(opts) {
2129
+ const { rootDir, layerMap, topN = 5 } = opts;
2130
+ const packages = scanWorkspace(rootDir, layerMap);
2131
+ const allNames = new Set(packages.map((p) => p.name));
2132
+ const afferentCount = new Map(packages.map((p) => [p.name, 0]));
2133
+ for (const pkg of packages) {
2134
+ const workspaceDeps = [...pkg.deps, ...pkg.devDeps].filter((d) => allNames.has(d));
2135
+ for (const dep of workspaceDeps) {
2136
+ afferentCount.set(dep, (afferentCount.get(dep) ?? 0) + 1);
2137
+ }
2138
+ }
2139
+ const result = packages.map((pkg) => {
2140
+ const ce = [...pkg.deps, ...pkg.devDeps].filter((d) => allNames.has(d)).length;
2141
+ const ca = afferentCount.get(pkg.name) ?? 0;
2142
+ const total = ca + ce;
2143
+ const instability = total === 0 ? 0 : ce / total;
2144
+ return {
2145
+ name: pkg.name,
2146
+ afferent: ca,
2147
+ efferent: ce,
2148
+ instability: Math.round(instability * 100) / 100
2149
+ };
2150
+ });
2151
+ const avgInstability = result.length === 0 ? 0 : Math.round(result.reduce((s, p) => s + p.instability, 0) / result.length * 100) / 100;
2152
+ const sorted = [...result].sort((a, b) => b.instability - a.instability);
2153
+ const mostUnstable = sorted.slice(0, topN).map((p) => ({ ...p }));
2154
+ const mostCoupled = [...result].sort((a, b) => b.afferent + b.efferent - (a.afferent + a.efferent)).slice(0, topN).map((p) => ({ ...p }));
2155
+ return { packages: result, avgInstability, mostUnstable, mostCoupled };
2156
+ }
2157
+
2158
+ // src/architecture/build-order.ts
2159
+ function analyzeBuildOrder(opts) {
2160
+ const { rootDir, layerMap, filterPackage } = opts;
2161
+ const packages = scanWorkspace(rootDir, layerMap);
2162
+ const allNames = new Set(packages.map((p) => p.name));
2163
+ const deps = /* @__PURE__ */ new Map();
2164
+ for (const pkg of packages) {
2165
+ const wsDeps = new Set(pkg.deps.filter((d) => allNames.has(d)));
2166
+ deps.set(pkg.name, wsDeps);
2167
+ }
2168
+ let names;
2169
+ if (filterPackage && allNames.has(filterPackage)) {
2170
+ const visited = /* @__PURE__ */ new Set();
2171
+ const visit = (n) => {
2172
+ if (visited.has(n)) {
2173
+ return;
2174
+ }
2175
+ visited.add(n);
2176
+ for (const d of deps.get(n) ?? []) {
2177
+ visit(d);
2178
+ }
2179
+ };
2180
+ visit(filterPackage);
2181
+ names = [...visited];
2182
+ } else {
2183
+ names = packages.map((p) => p.name);
2184
+ }
2185
+ const nameSet = new Set(names);
2186
+ const inDegree = /* @__PURE__ */ new Map();
2187
+ for (const n of names) {
2188
+ const count = [...deps.get(n) ?? []].filter((d) => nameSet.has(d)).length;
2189
+ inDegree.set(n, count);
2190
+ }
2191
+ const layers = [];
2192
+ const sorted = [];
2193
+ const enqueued = /* @__PURE__ */ new Set();
2194
+ let queue = names.filter((n) => (inDegree.get(n) ?? 0) === 0);
2195
+ for (const n of queue) {
2196
+ enqueued.add(n);
2197
+ }
2198
+ while (queue.length > 0) {
2199
+ layers.push([...queue]);
2200
+ sorted.push(...queue);
2201
+ const next = [];
2202
+ for (const d of queue) {
2203
+ for (const n of names) {
2204
+ if (enqueued.has(n)) {
2205
+ continue;
2206
+ }
2207
+ if ((deps.get(n) ?? /* @__PURE__ */ new Set()).has(d)) {
2208
+ const deg = (inDegree.get(n) ?? 1) - 1;
2209
+ inDegree.set(n, deg);
2210
+ if (deg === 0) {
2211
+ next.push(n);
2212
+ enqueued.add(n);
2213
+ }
2214
+ }
2215
+ }
2216
+ }
2217
+ queue = next;
2218
+ }
2219
+ const circular = names.filter((n) => !sorted.includes(n));
2220
+ return {
2221
+ layers,
2222
+ sorted,
2223
+ circular: circular.length > 0 ? [circular] : [],
2224
+ packageCount: sorted.length,
2225
+ layerCount: layers.length,
2226
+ hasCircular: circular.length > 0
2227
+ };
2228
+ }
2229
+ function readSnapshots(filePath) {
2230
+ if (!existsSync(filePath)) {
2231
+ return [];
2232
+ }
2233
+ try {
2234
+ return JSON.parse(readFileSync(filePath, "utf-8"));
2235
+ } catch {
2236
+ return [];
2237
+ }
2238
+ }
2239
+ function writeSnapshots(filePath, data) {
2240
+ const dir = dirname(filePath);
2241
+ if (!existsSync(dir)) {
2242
+ mkdirSync(dir, { recursive: true });
2243
+ }
2244
+ writeFileSync(filePath, JSON.stringify(data, null, 2));
2245
+ }
2246
+ function computeDelta(current, previous) {
2247
+ return {
2248
+ score: current.score - previous.score,
2249
+ layeringViolations: current.counters.layeringViolations - previous.counters.layeringViolations,
2250
+ anyCount: current.counters.anyCount - previous.counters.anyCount,
2251
+ tsIgnoreCount: current.counters.tsIgnoreCount - previous.counters.tsIgnoreCount,
2252
+ unusedFiles: current.counters.unusedFiles - previous.counters.unusedFiles,
2253
+ unusedDeps: current.counters.unusedDeps - previous.counters.unusedDeps,
2254
+ avgInstability: Math.round((current.counters.avgInstability - previous.counters.avgInstability) * 100) / 100
2255
+ };
2256
+ }
2257
+ var QualitySnapshotStore = class {
2258
+ snapshotPath;
2259
+ maxEntries;
2260
+ constructor(rootDir, maxEntries = MAX_SNAPSHOTS_DEFAULT) {
2261
+ this.snapshotPath = join(rootDir, SNAPSHOT_DIR, SNAPSHOT_FILE);
2262
+ this.maxEntries = maxEntries;
2263
+ }
2264
+ load() {
2265
+ return readSnapshots(this.snapshotPath);
2266
+ }
2267
+ save(input) {
2268
+ const snap = {
2269
+ id: randomUUID(),
2270
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2271
+ ...input
2272
+ };
2273
+ const history = this.load();
2274
+ history.push(snap);
2275
+ while (history.length > this.maxEntries) {
2276
+ history.shift();
2277
+ }
2278
+ writeSnapshots(this.snapshotPath, history);
2279
+ return snap;
2280
+ }
2281
+ latest() {
2282
+ const h = this.load();
2283
+ return h.length > 0 ? h[h.length - 1] ?? null : null;
2284
+ }
2285
+ history() {
2286
+ const snapshots = this.load();
2287
+ const latest = snapshots.length > 0 ? snapshots[snapshots.length - 1] ?? null : null;
2288
+ const previous = snapshots.length > 1 ? snapshots[snapshots.length - 2] ?? null : null;
2289
+ const delta = latest && previous ? computeDelta(latest, previous) : null;
2290
+ return { snapshots, delta, latest };
2291
+ }
2292
+ };
1920
2293
 
1921
- export { analyzeDependencies, analyzeDuplicateDependencies, analyzeMissingDependencies, analyzeTypes, analyzeUnusedDependencies, buildDependencyGraph, buildFileImportGraph, calculateHealth, calculateHealthScore, calculateLinesOfCode, calculateSize, calculateStats, checkBuilds, checkDuplicateDependencies, checkMissingReadmes, collectEntryPoints, countPackages, distPathToSrcPath, extractFileImports, findCircularDependencies, findReachableFiles, formatBytes, getBuildOrderForPackage, getImpactAnalysis, getReverseDependencies, listBackups, parseManifestHandlers, parseTsupEntries, removeDeadFiles, resolveRelativeImport, restoreFromBackup, runTests, scanDeadFiles, scoreToGrade, topologicalSort };
2294
+ export { QualitySnapshotStore, analyzeBuildOrder, analyzeCoupling, analyzeDependencies, analyzeDuplicateDependencies, analyzeLayering, analyzeMissingDependencies, analyzeTypes, analyzeUnusedDependencies, buildDependencyGraph, buildFileImportGraph, buildPackageMap, calculateHealth, calculateLinesOfCode, calculateSize, calculateStats, checkBuilds, collectEntryPoints, countPackages, distPathToSrcPath, extractFileImports, findCircularDependencies, findReachableFiles, formatBytes, getBuildOrderForPackage, getImpactAnalysis, getReverseDependencies, listBackups, parseManifestHandlers, parseTsupEntries, removeDeadFiles, resolveLayer, resolveRelativeImport, restoreFromBackup, runKnip, runTests, scanDeadFiles, scanWorkspace, topologicalSort };
1922
2295
  //# sourceMappingURL=index.js.map
1923
2296
  //# sourceMappingURL=index.js.map