@kb-labs/quality-core 2.94.0 → 2.98.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/dead-code/index.d.ts +20 -2
- package/dist/dead-code/index.js +55 -1
- package/dist/dead-code/index.js.map +1 -1
- package/dist/health/index.d.ts +21 -34
- package/dist/health/index.js +83 -111
- package/dist/health/index.js.map +1 -1
- package/dist/index.d.ts +100 -3
- package/dist/index.js +618 -245
- package/dist/index.js.map +1 -1
- package/dist/stale/index.js.map +1 -1
- package/dist/stats/index.d.ts +4 -9
- package/dist/stats/index.js +66 -36
- package/dist/stats/index.js.map +1 -1
- package/dist/tests/index.js.map +1 -1
- package/dist/types/index.js +1 -1
- package/dist/types/index.js.map +1 -1
- package/package.json +5 -5
package/dist/health/index.js
CHANGED
|
@@ -1,130 +1,102 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { join } from 'path';
|
|
3
|
-
import globby from 'globby';
|
|
1
|
+
import { DIMENSION_WEIGHTS, HEALTH_GRADES } from '@kb-labs/quality-contracts';
|
|
4
2
|
|
|
5
3
|
// src/health/calculate-health.ts
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
absolute: true
|
|
11
|
-
});
|
|
12
|
-
const depVersions = /* @__PURE__ */ new Map();
|
|
13
|
-
const contents = await Promise.all(
|
|
14
|
-
packageJsonFiles.map((f) => readFile(f, "utf-8").catch(() => null))
|
|
15
|
-
);
|
|
16
|
-
for (const content of contents) {
|
|
17
|
-
if (!content) {
|
|
18
|
-
continue;
|
|
19
|
-
}
|
|
20
|
-
try {
|
|
21
|
-
const pkg = JSON.parse(content);
|
|
22
|
-
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
23
|
-
for (const [name, version] of Object.entries(allDeps)) {
|
|
24
|
-
if (typeof version !== "string") {
|
|
25
|
-
continue;
|
|
26
|
-
}
|
|
27
|
-
if (!depVersions.has(name)) {
|
|
28
|
-
depVersions.set(name, /* @__PURE__ */ new Set());
|
|
29
|
-
}
|
|
30
|
-
depVersions.get(name).add(version);
|
|
31
|
-
}
|
|
32
|
-
} catch {
|
|
4
|
+
function gradeFromScore(score) {
|
|
5
|
+
for (const [g, { min }] of Object.entries(HEALTH_GRADES)) {
|
|
6
|
+
if (score >= min) {
|
|
7
|
+
return g;
|
|
33
8
|
}
|
|
34
9
|
}
|
|
35
|
-
|
|
36
|
-
([, versions]) => versions.size > 1
|
|
37
|
-
);
|
|
38
|
-
if (duplicates.length === 0) {
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
41
|
-
const penalty = Math.min(duplicates.length * 2, 30);
|
|
42
|
-
return {
|
|
43
|
-
type: "duplicate",
|
|
44
|
-
severity: duplicates.length > 20 ? "high" : duplicates.length > 10 ? "medium" : "low",
|
|
45
|
-
message: `Found ${duplicates.length} duplicate dependencies with different versions`,
|
|
46
|
-
count: duplicates.length,
|
|
47
|
-
penalty
|
|
48
|
-
};
|
|
10
|
+
return "F";
|
|
49
11
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
cwd: rootDir,
|
|
53
|
-
ignore: ["**/node_modules/**", "**/.git/**", "package.json"],
|
|
54
|
-
absolute: true
|
|
55
|
-
});
|
|
56
|
-
const hasReadme = async (pkgPath) => {
|
|
57
|
-
const dir = join(pkgPath, "..");
|
|
58
|
-
for (const name of ["README.md", "readme.md", "Readme.md"]) {
|
|
59
|
-
try {
|
|
60
|
-
await access(join(dir, name));
|
|
61
|
-
return true;
|
|
62
|
-
} catch {
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return false;
|
|
66
|
-
};
|
|
67
|
-
const results = await Promise.all(pkgFiles.map(hasReadme));
|
|
68
|
-
const missingCount = results.filter((has) => !has).length;
|
|
69
|
-
if (missingCount === 0) {
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
const penalty = Math.min(missingCount, 15);
|
|
73
|
-
return {
|
|
74
|
-
type: "readme",
|
|
75
|
-
severity: missingCount > 20 ? "high" : missingCount > 10 ? "medium" : "low",
|
|
76
|
-
message: `Found ${missingCount} packages without README`,
|
|
77
|
-
count: missingCount,
|
|
78
|
-
penalty
|
|
79
|
-
};
|
|
12
|
+
function clamp(n) {
|
|
13
|
+
return Math.max(0, Math.min(100, Math.round(n)));
|
|
80
14
|
}
|
|
81
|
-
function
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
|
|
15
|
+
function scoreArchitecture(layering, coupling, thresholds) {
|
|
16
|
+
const details = [];
|
|
17
|
+
const layeringPenalty = Math.min(layering.totalViolations * 5, 60);
|
|
18
|
+
if (layering.totalViolations > 0) {
|
|
19
|
+
details.push(`${layering.totalViolations} layering violation(s) in ${layering.affectedPackages.length} package(s)`);
|
|
20
|
+
}
|
|
21
|
+
const highInstability = coupling.packages.filter((p) => p.instability > thresholds.instability);
|
|
22
|
+
const couplingPenalty = Math.min(highInstability.length * 3, 30);
|
|
23
|
+
if (highInstability.length > 0) {
|
|
24
|
+
details.push(`${highInstability.length} package(s) with instability > ${thresholds.instability}`);
|
|
25
|
+
}
|
|
26
|
+
const score = clamp(100 - layeringPenalty - couplingPenalty);
|
|
27
|
+
return { score, grade: gradeFromScore(score), details };
|
|
85
28
|
}
|
|
86
|
-
function
|
|
87
|
-
|
|
88
|
-
|
|
29
|
+
function scoreTypeScript(types) {
|
|
30
|
+
const details = [];
|
|
31
|
+
const totalAny = types.packages.reduce((s, p) => s + p.anyCount, 0);
|
|
32
|
+
const anyPenalty = Math.min(totalAny * 0.5, 50);
|
|
33
|
+
if (totalAny > 0) {
|
|
34
|
+
details.push(`${totalAny} \`any\` usage(s)`);
|
|
89
35
|
}
|
|
90
|
-
|
|
91
|
-
|
|
36
|
+
const totalIgnore = types.packages.reduce((s, p) => s + p.tsIgnoreCount, 0);
|
|
37
|
+
const ignorePenalty = Math.min(totalIgnore * 2, 30);
|
|
38
|
+
if (totalIgnore > 0) {
|
|
39
|
+
details.push(`${totalIgnore} @ts-ignore(s)`);
|
|
92
40
|
}
|
|
93
|
-
|
|
94
|
-
|
|
41
|
+
const errorPenalty = Math.min(types.totalErrors * 3, 20);
|
|
42
|
+
if (types.totalErrors > 0) {
|
|
43
|
+
details.push(`${types.totalErrors} type error(s)`);
|
|
95
44
|
}
|
|
96
|
-
|
|
97
|
-
|
|
45
|
+
const score = clamp(100 - anyPenalty - ignorePenalty - errorPenalty);
|
|
46
|
+
return { score, grade: gradeFromScore(score), details };
|
|
47
|
+
}
|
|
48
|
+
function scoreDeadCode(knip) {
|
|
49
|
+
const details = [];
|
|
50
|
+
const filePenalty = Math.min(knip.unusedFiles.length * 2, 50);
|
|
51
|
+
if (knip.unusedFiles.length > 0) {
|
|
52
|
+
details.push(`${knip.unusedFiles.length} unused file(s)`);
|
|
98
53
|
}
|
|
99
|
-
|
|
54
|
+
const exportPenalty = Math.min(knip.unusedExports.length * 0.5, 30);
|
|
55
|
+
if (knip.unusedExports.length > 0) {
|
|
56
|
+
details.push(`${knip.unusedExports.length} unused export(s)`);
|
|
57
|
+
}
|
|
58
|
+
const score = clamp(100 - filePenalty - exportPenalty);
|
|
59
|
+
return { score, grade: gradeFromScore(score), details };
|
|
100
60
|
}
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
absolute: true,
|
|
107
|
-
deep: 6
|
|
108
|
-
});
|
|
109
|
-
const [duplicatesIssue, readmesIssue] = await Promise.all([
|
|
110
|
-
checkDuplicateDependencies(rootDir, packageJsonFiles),
|
|
111
|
-
checkMissingReadmes(rootDir, packageJsonFiles)
|
|
112
|
-
]);
|
|
113
|
-
if (duplicatesIssue) {
|
|
114
|
-
issues.push(duplicatesIssue);
|
|
61
|
+
function scoreDepHygiene(knip) {
|
|
62
|
+
const details = [];
|
|
63
|
+
const unusedPenalty = Math.min(knip.unusedDependencies.length * 5, 50);
|
|
64
|
+
if (knip.unusedDependencies.length > 0) {
|
|
65
|
+
details.push(`${knip.unusedDependencies.length} unused dep(s)`);
|
|
115
66
|
}
|
|
116
|
-
|
|
117
|
-
|
|
67
|
+
const unlistedPenalty = Math.min(knip.unlistedDependencies.length * 10, 40);
|
|
68
|
+
if (knip.unlistedDependencies.length > 0) {
|
|
69
|
+
details.push(`${knip.unlistedDependencies.length} unlisted dep(s)`);
|
|
118
70
|
}
|
|
119
|
-
const score =
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
71
|
+
const score = clamp(100 - unusedPenalty - unlistedPenalty);
|
|
72
|
+
return { score, grade: gradeFromScore(score), details };
|
|
73
|
+
}
|
|
74
|
+
function scoreTestCoverage(avgCoverage) {
|
|
75
|
+
if (avgCoverage === null) {
|
|
76
|
+
return { score: 100, grade: "A", details: ["no coverage data \u2014 skipped"] };
|
|
77
|
+
}
|
|
78
|
+
const score = clamp(avgCoverage);
|
|
79
|
+
const details = avgCoverage < 80 ? [`avg coverage ${avgCoverage.toFixed(1)}%`] : [];
|
|
80
|
+
return { score, grade: gradeFromScore(score), details };
|
|
81
|
+
}
|
|
82
|
+
function calculateHealth(input) {
|
|
83
|
+
const { layering, coupling, types, knip, avgTestCoverage, thresholds } = input;
|
|
84
|
+
const dimensions = {
|
|
85
|
+
architecture: scoreArchitecture(layering, coupling, thresholds),
|
|
86
|
+
typescript: scoreTypeScript(types),
|
|
87
|
+
deadCode: scoreDeadCode(knip),
|
|
88
|
+
depHygiene: scoreDepHygiene(knip),
|
|
89
|
+
testCoverage: scoreTestCoverage(avgTestCoverage)
|
|
125
90
|
};
|
|
91
|
+
const score = clamp(
|
|
92
|
+
Object.entries(DIMENSION_WEIGHTS).reduce(
|
|
93
|
+
(sum, [key, weight]) => sum + dimensions[key].score * weight,
|
|
94
|
+
0
|
|
95
|
+
)
|
|
96
|
+
);
|
|
97
|
+
return { score, grade: gradeFromScore(score), dimensions };
|
|
126
98
|
}
|
|
127
99
|
|
|
128
|
-
export { calculateHealth
|
|
100
|
+
export { calculateHealth };
|
|
129
101
|
//# sourceMappingURL=index.js.map
|
|
130
102
|
//# sourceMappingURL=index.js.map
|
package/dist/health/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/health/calculate-health.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Calculate monorepo health score\n *\n * Atomic functions for health checks and scoring.\n */\n\nimport { readFile, access } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport globby from 'globby';\n\nexport interface HealthResult {\n score: number;\n grade: 'A' | 'B' | 'C' | 'D' | 'F';\n issues: HealthIssue[];\n}\n\nexport interface HealthIssue {\n type: 'duplicate' | 'unused' | 'missing' | 'structure' | 'readme';\n severity: 'high' | 'medium' | 'low';\n message: string;\n count: number;\n penalty: number;\n}\n\n/**\n * Check for duplicate dependencies across packages\n */\nexport async function checkDuplicateDependencies(rootDir: string, pkgFiles?: string[]): Promise<HealthIssue | null> {\n const packageJsonFiles = pkgFiles ?? await globby('**/package.json', {\n cwd: rootDir,\n ignore: ['**/node_modules/**', '**/.git/**'],\n absolute: true,\n });\n\n // Read all package.json files in parallel\n const depVersions = new Map<string, Set<string>>();\n const contents = await Promise.all(\n packageJsonFiles.map(f => readFile(f, 'utf-8').catch(() => null))\n );\n\n for (const content of contents) {\n if (!content) {continue;}\n try {\n const pkg = JSON.parse(content);\n const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };\n for (const [name, version] of Object.entries(allDeps)) {\n if (typeof version !== 'string') {continue;}\n if (!depVersions.has(name)) {depVersions.set(name, new Set());}\n depVersions.get(name)!.add(version);\n }\n } catch { /* skip invalid */ }\n }\n\n // Count duplicates (deps with more than 1 version)\n const duplicates = Array.from(depVersions.entries()).filter(\n ([, versions]) => versions.size > 1\n );\n\n if (duplicates.length === 0) {return null;}\n\n const penalty = Math.min(duplicates.length * 2, 30); // Max -30 points\n\n return {\n type: 'duplicate',\n severity: duplicates.length > 20 ? 'high' : duplicates.length > 10 ? 'medium' : 'low',\n message: `Found ${duplicates.length} duplicate dependencies with different versions`,\n count: duplicates.length,\n penalty,\n };\n}\n\n/**\n * Check for packages missing README\n */\nexport async function checkMissingReadmes(rootDir: string, packageJsonFiles?: string[]): Promise<HealthIssue | null> {\n const pkgFiles = packageJsonFiles ?? await globby('**/package.json', {\n cwd: rootDir,\n ignore: ['**/node_modules/**', '**/.git/**', 'package.json'],\n absolute: true,\n });\n\n const hasReadme = async (pkgPath: string): Promise<boolean> => {\n const dir = join(pkgPath, '..');\n for (const name of ['README.md', 'readme.md', 'Readme.md']) {\n try {\n await access(join(dir, name));\n return true;\n } catch { /* not found */ }\n }\n return false;\n };\n\n const results = await Promise.all(pkgFiles.map(hasReadme));\n const missingCount = results.filter(has => !has).length;\n\n if (missingCount === 0) {return null;}\n\n const penalty = Math.min(missingCount, 15); // Max -15 points\n\n return {\n type: 'readme',\n severity: missingCount > 20 ? 'high' : missingCount > 10 ? 'medium' : 'low',\n message: `Found ${missingCount} packages without README`,\n count: missingCount,\n penalty,\n };\n}\n\n/**\n * Calculate health score from issues\n */\nexport function calculateHealthScore(issues: HealthIssue[]): number {\n const baseScore = 100;\n const totalPenalty = issues.reduce((sum, issue) => sum + issue.penalty, 0);\n\n return Math.max(0, baseScore - totalPenalty);\n}\n\n/**\n * Convert score to letter grade\n */\nexport function scoreToGrade(score: number): 'A' | 'B' | 'C' | 'D' | 'F' {\n if (score >= 90) {return 'A';}\n if (score >= 80) {return 'B';}\n if (score >= 70) {return 'C';}\n if (score >= 60) {return 'D';}\n return 'F';\n}\n\n/**\n * Calculate complete health report\n */\nexport async function calculateHealth(rootDir: string): Promise<HealthResult> {\n const issues: HealthIssue[] = [];\n\n // Single globby scan shared by all checks\n // depth=6 covers workspace/subrepo/packages/pkg-name/src/... without scanning deep trees\n const packageJsonFiles = await globby('**/package.json', {\n cwd: rootDir,\n ignore: ['**/node_modules/**', '**/.git/**', '**/.kb/**', '**/dist/**'],\n absolute: true,\n deep: 6,\n });\n\n const [duplicatesIssue, readmesIssue] = await Promise.all([\n checkDuplicateDependencies(rootDir, packageJsonFiles),\n checkMissingReadmes(rootDir, packageJsonFiles),\n ]);\n\n if (duplicatesIssue) {issues.push(duplicatesIssue);}\n if (readmesIssue) {issues.push(readmesIssue);}\n\n const score = calculateHealthScore(issues);\n const grade = scoreToGrade(score);\n\n return {\n score,\n grade,\n issues,\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/health/calculate-health.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Multidimensional health score calculation.\n *\n * Composite score = weighted average of 5 dimensions:\n * architecture 30% — layering violations + avg instability\n * typescript 25% — any count + ts-ignore count\n * deadCode 20% — unused files + unused exports from knip\n * depHygiene 15% — unused + unlisted dependencies from knip\n * testCoverage 10% — avg test coverage % (optional, defaults to 100 if unavailable)\n */\n\nimport { DIMENSION_WEIGHTS, HEALTH_GRADES } from '@kb-labs/quality-contracts';\nimport type {\n HealthScore,\n DimensionScore,\n DimensionScores,\n LayeringReport,\n CouplingReport,\n KnipReport,\n TypeAnalysisResult,\n QualityThresholds,\n} from '@kb-labs/quality-contracts';\n\nfunction gradeFromScore(score: number): HealthScore['grade'] {\n for (const [g, { min }] of Object.entries(HEALTH_GRADES) as [HealthScore['grade'], { min: number }][]) {\n if (score >= min) {return g;}\n }\n return 'F';\n}\n\nfunction clamp(n: number): number {\n return Math.max(0, Math.min(100, Math.round(n)));\n}\n\n// ── Dimension scorers ─────────────────────────────────────────────────────────\n\nfunction scoreArchitecture(\n layering: LayeringReport,\n coupling: CouplingReport,\n thresholds: QualityThresholds\n): DimensionScore {\n const details: string[] = [];\n\n // Each violation costs 5 points, max -60\n const layeringPenalty = Math.min(layering.totalViolations * 5, 60);\n if (layering.totalViolations > 0) {\n details.push(`${layering.totalViolations} layering violation(s) in ${layering.affectedPackages.length} package(s)`);\n }\n\n // Packages above instability threshold cost 3 points each, max -30\n const highInstability = coupling.packages.filter(p => p.instability > thresholds.instability);\n const couplingPenalty = Math.min(highInstability.length * 3, 30);\n if (highInstability.length > 0) {\n details.push(`${highInstability.length} package(s) with instability > ${thresholds.instability}`);\n }\n\n const score = clamp(100 - layeringPenalty - couplingPenalty);\n return { score, grade: gradeFromScore(score), details };\n}\n\nfunction scoreTypeScript(types: TypeAnalysisResult): DimensionScore {\n const details: string[] = [];\n\n const totalAny = types.packages.reduce((s, p) => s + p.anyCount, 0);\n const anyPenalty = Math.min(totalAny * 0.5, 50);\n if (totalAny > 0) {details.push(`${totalAny} \\`any\\` usage(s)`);}\n\n const totalIgnore = types.packages.reduce((s, p) => s + p.tsIgnoreCount, 0);\n const ignorePenalty = Math.min(totalIgnore * 2, 30);\n if (totalIgnore > 0) {details.push(`${totalIgnore} @ts-ignore(s)`);}\n\n const errorPenalty = Math.min(types.totalErrors * 3, 20);\n if (types.totalErrors > 0) {details.push(`${types.totalErrors} type error(s)`);}\n\n const score = clamp(100 - anyPenalty - ignorePenalty - errorPenalty);\n return { score, grade: gradeFromScore(score), details };\n}\n\nfunction scoreDeadCode(knip: KnipReport): DimensionScore {\n const details: string[] = [];\n\n const filePenalty = Math.min(knip.unusedFiles.length * 2, 50);\n if (knip.unusedFiles.length > 0) {details.push(`${knip.unusedFiles.length} unused file(s)`);}\n\n const exportPenalty = Math.min(knip.unusedExports.length * 0.5, 30);\n if (knip.unusedExports.length > 0) {details.push(`${knip.unusedExports.length} unused export(s)`);}\n\n const score = clamp(100 - filePenalty - exportPenalty);\n return { score, grade: gradeFromScore(score), details };\n}\n\nfunction scoreDepHygiene(knip: KnipReport): DimensionScore {\n const details: string[] = [];\n\n const unusedPenalty = Math.min(knip.unusedDependencies.length * 5, 50);\n if (knip.unusedDependencies.length > 0) {details.push(`${knip.unusedDependencies.length} unused dep(s)`);}\n\n const unlistedPenalty = Math.min(knip.unlistedDependencies.length * 10, 40);\n if (knip.unlistedDependencies.length > 0) {details.push(`${knip.unlistedDependencies.length} unlisted dep(s)`);}\n\n const score = clamp(100 - unusedPenalty - unlistedPenalty);\n return { score, grade: gradeFromScore(score), details };\n}\n\nfunction scoreTestCoverage(avgCoverage: number | null): DimensionScore {\n if (avgCoverage === null) {\n return { score: 100, grade: 'A', details: ['no coverage data — skipped'] };\n }\n const score = clamp(avgCoverage);\n const details = avgCoverage < 80 ? [`avg coverage ${avgCoverage.toFixed(1)}%`] : [];\n return { score, grade: gradeFromScore(score), details };\n}\n\n// ── Public API ────────────────────────────────────────────────────────────────\n\nexport interface HealthInput {\n layering: LayeringReport;\n coupling: CouplingReport;\n types: TypeAnalysisResult;\n knip: KnipReport;\n totalPackages: number;\n /** Average test coverage in %, or null if not collected */\n avgTestCoverage: number | null;\n thresholds: QualityThresholds;\n}\n\nexport function calculateHealth(input: HealthInput): HealthScore {\n const { layering, coupling, types, knip, avgTestCoverage, thresholds } = input;\n\n const dimensions: DimensionScores = {\n architecture: scoreArchitecture(layering, coupling, thresholds),\n typescript: scoreTypeScript(types),\n deadCode: scoreDeadCode(knip),\n depHygiene: scoreDepHygiene(knip),\n testCoverage: scoreTestCoverage(avgTestCoverage),\n };\n\n const score = clamp(\n Object.entries(DIMENSION_WEIGHTS).reduce(\n (sum, [key, weight]) => sum + dimensions[key as keyof DimensionScores].score * weight,\n 0\n )\n );\n\n return { score, grade: gradeFromScore(score), dimensions };\n}\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,106 @@
|
|
|
1
1
|
export { DependencyGraph, PackageNode, TopologicalSortResult, buildDependencyGraph, findCircularDependencies, getBuildOrderForPackage, getImpactAnalysis, getReverseDependencies, topologicalSort } from './graph/index.js';
|
|
2
2
|
export { MonorepoStats, calculateLinesOfCode, calculateSize, calculateStats, countPackages, formatBytes } from './stats/index.js';
|
|
3
|
-
export {
|
|
3
|
+
export { HealthInput, calculateHealth } from './health/index.js';
|
|
4
4
|
export { DependencyAnalysis, DuplicateDependency, MissingDependency, UnusedDependency, VersionInfo, analyzeDependencies, analyzeDuplicateDependencies, analyzeMissingDependencies, analyzeUnusedDependencies } from './dependencies/index.js';
|
|
5
5
|
export { BuildCheckOptions, BuildCheckResult, checkBuilds } from './builds/index.js';
|
|
6
6
|
export { TypeAnalysisOptions, TypeAnalysisResult, analyzeTypes } from './types/index.js';
|
|
7
7
|
export { TestRunOptions, TestRunResult, runTests } from './tests/index.js';
|
|
8
|
-
export { buildFileImportGraph, collectEntryPoints, distPathToSrcPath, extractFileImports, findReachableFiles, listBackups, parseManifestHandlers, parseTsupEntries, removeDeadFiles, resolveRelativeImport, restoreFromBackup, scanDeadFiles } from './dead-code/index.js';
|
|
9
|
-
import '@kb-labs/quality-contracts';
|
|
8
|
+
export { buildFileImportGraph, collectEntryPoints, distPathToSrcPath, extractFileImports, findReachableFiles, listBackups, parseManifestHandlers, parseTsupEntries, removeDeadFiles, resolveRelativeImport, restoreFromBackup, runKnip, scanDeadFiles } from './dead-code/index.js';
|
|
9
|
+
import { QualityLayerMap, LayeringReport, CouplingReport, QualitySnapshot, SnapshotHistory } from '@kb-labs/quality-contracts';
|
|
10
|
+
import '@kb-labs/sdk';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Workspace package scanner for flat monorepo layout.
|
|
14
|
+
*
|
|
15
|
+
* Discovers all packages by scanning top-level dirs (core/, sdk/, plugins/, etc.)
|
|
16
|
+
* and reading their package.json files.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
interface WorkspacePackage {
|
|
20
|
+
name: string;
|
|
21
|
+
dir: string;
|
|
22
|
+
packageJsonPath: string;
|
|
23
|
+
/** Layer index derived from path prefix */
|
|
24
|
+
layer: number;
|
|
25
|
+
deps: string[];
|
|
26
|
+
devDeps: string[];
|
|
27
|
+
}
|
|
28
|
+
/** Resolve layer index for a package dir relative to rootDir */
|
|
29
|
+
declare function resolveLayer(pkgDir: string, rootDir: string, layerMap?: QualityLayerMap): number;
|
|
30
|
+
/** Scan all workspace packages in a flat monorepo */
|
|
31
|
+
declare function scanWorkspace(rootDir: string, layerMap?: QualityLayerMap): WorkspacePackage[];
|
|
32
|
+
/** Build a map from package name → WorkspacePackage */
|
|
33
|
+
declare function buildPackageMap(packages: WorkspacePackage[]): Map<string, WorkspacePackage>;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Layering violation detector using TypeScript compiler API.
|
|
37
|
+
*
|
|
38
|
+
* Checks that imports respect the layer hierarchy defined in CLAUDE.md:
|
|
39
|
+
* Layer 0: core/
|
|
40
|
+
* Layer 1: sdk/ shared/
|
|
41
|
+
* Layer 2: cli/ adapters/
|
|
42
|
+
* Layer 3: plugins/
|
|
43
|
+
* Layer 4: studio/ sites/
|
|
44
|
+
*
|
|
45
|
+
* A violation = a package at layer N importing a package at layer M where M > N.
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
interface LayeringOptions {
|
|
49
|
+
rootDir: string;
|
|
50
|
+
layerMap?: QualityLayerMap;
|
|
51
|
+
}
|
|
52
|
+
/** Run layering analysis and return violations */
|
|
53
|
+
declare function analyzeLayering(opts: LayeringOptions): Promise<LayeringReport>;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Coupling metrics — Robert Martin's package metrics.
|
|
57
|
+
*
|
|
58
|
+
* Ca (afferent coupling): packages that depend on this package
|
|
59
|
+
* Ce (efferent coupling): packages this package depends on
|
|
60
|
+
* Instability = Ce / (Ca + Ce), range [0, 1]
|
|
61
|
+
* 0 = maximally stable (many dependents, no deps)
|
|
62
|
+
* 1 = maximally unstable (no dependents, many deps)
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
interface CouplingOptions {
|
|
66
|
+
rootDir: string;
|
|
67
|
+
layerMap?: QualityLayerMap;
|
|
68
|
+
/** How many packages to include in mostUnstable / mostCoupled lists */
|
|
69
|
+
topN?: number;
|
|
70
|
+
}
|
|
71
|
+
declare function analyzeCoupling(opts: CouplingOptions): CouplingReport;
|
|
72
|
+
|
|
73
|
+
interface BuildOrderOptions {
|
|
74
|
+
rootDir: string;
|
|
75
|
+
layerMap?: QualityLayerMap;
|
|
76
|
+
/** Limit to transitive deps of this package */
|
|
77
|
+
filterPackage?: string;
|
|
78
|
+
}
|
|
79
|
+
interface BuildOrderResult {
|
|
80
|
+
layers: string[][];
|
|
81
|
+
sorted: string[];
|
|
82
|
+
circular: string[][];
|
|
83
|
+
packageCount: number;
|
|
84
|
+
layerCount: number;
|
|
85
|
+
hasCircular: boolean;
|
|
86
|
+
}
|
|
87
|
+
declare function analyzeBuildOrder(opts: BuildOrderOptions): BuildOrderResult;
|
|
88
|
+
|
|
89
|
+
interface SnapshotInput {
|
|
90
|
+
score: number;
|
|
91
|
+
grade: QualitySnapshot['grade'];
|
|
92
|
+
dimensions: QualitySnapshot['dimensions'];
|
|
93
|
+
counters: QualitySnapshot['counters'];
|
|
94
|
+
git: QualitySnapshot['git'];
|
|
95
|
+
}
|
|
96
|
+
declare class QualitySnapshotStore {
|
|
97
|
+
private readonly snapshotPath;
|
|
98
|
+
private readonly maxEntries;
|
|
99
|
+
constructor(rootDir: string, maxEntries?: number);
|
|
100
|
+
load(): QualitySnapshot[];
|
|
101
|
+
save(input: SnapshotInput): QualitySnapshot;
|
|
102
|
+
latest(): QualitySnapshot | null;
|
|
103
|
+
history(): SnapshotHistory;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export { type BuildOrderOptions, type BuildOrderResult, type CouplingOptions, type LayeringOptions, QualitySnapshotStore, type SnapshotInput, type WorkspacePackage, analyzeBuildOrder, analyzeCoupling, analyzeLayering, buildPackageMap, resolveLayer, scanWorkspace };
|