@kb-labs/quality-core 0.6.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/README.md +63 -0
- package/dist/builds/index.d.ts +34 -0
- package/dist/builds/index.js +133 -0
- package/dist/builds/index.js.map +1 -0
- package/dist/dead-code/index.d.ts +121 -0
- package/dist/dead-code/index.js +837 -0
- package/dist/dead-code/index.js.map +1 -0
- package/dist/dependencies/index.d.ts +50 -0
- package/dist/dependencies/index.js +200 -0
- package/dist/dependencies/index.js.map +1 -0
- package/dist/graph/index.d.ts +50 -0
- package/dist/graph/index.js +248 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/health/index.d.ts +39 -0
- package/dist/health/index.js +130 -0
- package/dist/health/index.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +1921 -0
- package/dist/index.js.map +1 -0
- package/dist/stale/index.d.ts +31 -0
- package/dist/stale/index.js +273 -0
- package/dist/stale/index.js.map +1 -0
- package/dist/stats/index.d.ts +33 -0
- package/dist/stats/index.js +74 -0
- package/dist/stats/index.js.map +1 -0
- package/dist/tests/index.d.ts +47 -0
- package/dist/tests/index.js +200 -0
- package/dist/tests/index.js.map +1 -0
- package/dist/types/index.d.ts +32 -0
- package/dist/types/index.js +159 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +90 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
// src/graph/dependency-graph.ts
|
|
5
|
+
function buildDependencyGraph(rootDir) {
|
|
6
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
7
|
+
const workspacePackages = /* @__PURE__ */ new Set();
|
|
8
|
+
if (!fs.existsSync(rootDir)) {
|
|
9
|
+
return { nodes, workspacePackages };
|
|
10
|
+
}
|
|
11
|
+
const entries = fs.readdirSync(rootDir, { withFileTypes: true });
|
|
12
|
+
for (const entry of entries) {
|
|
13
|
+
if (!entry.isDirectory() || !entry.name.startsWith("kb-labs-")) {
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
const repoPath = path.join(rootDir, entry.name);
|
|
17
|
+
const packagesDir = path.join(repoPath, "packages");
|
|
18
|
+
if (!fs.existsSync(packagesDir)) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const packageDirs = fs.readdirSync(packagesDir, { withFileTypes: true });
|
|
22
|
+
for (const pkgDir of packageDirs) {
|
|
23
|
+
if (!pkgDir.isDirectory()) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const packageJsonPath = path.join(packagesDir, pkgDir.name, "package.json");
|
|
27
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
28
|
+
const pkgJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
29
|
+
const packageName = pkgJson.name;
|
|
30
|
+
if (packageName) {
|
|
31
|
+
workspacePackages.add(packageName);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
for (const entry of entries) {
|
|
37
|
+
if (!entry.isDirectory() || !entry.name.startsWith("kb-labs-")) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const repoPath = path.join(rootDir, entry.name);
|
|
41
|
+
const packagesDir = path.join(repoPath, "packages");
|
|
42
|
+
if (!fs.existsSync(packagesDir)) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const packageDirs = fs.readdirSync(packagesDir, { withFileTypes: true });
|
|
46
|
+
for (const pkgDir of packageDirs) {
|
|
47
|
+
if (!pkgDir.isDirectory()) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const packageJsonPath = path.join(packagesDir, pkgDir.name, "package.json");
|
|
51
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
52
|
+
const pkgJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
53
|
+
const packageName = pkgJson.name;
|
|
54
|
+
if (!packageName) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const deps = /* @__PURE__ */ new Set();
|
|
58
|
+
const devDeps = /* @__PURE__ */ new Set();
|
|
59
|
+
const allDeps = {
|
|
60
|
+
...pkgJson.dependencies,
|
|
61
|
+
...pkgJson.devDependencies
|
|
62
|
+
};
|
|
63
|
+
for (const dep of Object.keys(allDeps)) {
|
|
64
|
+
if (workspacePackages.has(dep)) {
|
|
65
|
+
deps.add(dep);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (pkgJson.devDependencies) {
|
|
69
|
+
for (const dep of Object.keys(pkgJson.devDependencies)) {
|
|
70
|
+
if (workspacePackages.has(dep)) {
|
|
71
|
+
devDeps.add(dep);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
nodes.set(packageName, {
|
|
76
|
+
name: packageName,
|
|
77
|
+
path: packageJsonPath,
|
|
78
|
+
dir: path.dirname(packageJsonPath),
|
|
79
|
+
deps,
|
|
80
|
+
devDeps,
|
|
81
|
+
dependents: /* @__PURE__ */ new Set()
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
for (const [packageName, node] of nodes) {
|
|
87
|
+
for (const dep of node.deps) {
|
|
88
|
+
const depNode = nodes.get(dep);
|
|
89
|
+
if (depNode) {
|
|
90
|
+
depNode.dependents.add(packageName);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return { nodes, workspacePackages };
|
|
95
|
+
}
|
|
96
|
+
function topologicalSort(graph) {
|
|
97
|
+
const { nodes } = graph;
|
|
98
|
+
const layers = [];
|
|
99
|
+
const sorted = [];
|
|
100
|
+
const circular = [];
|
|
101
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
102
|
+
for (const [name] of nodes) {
|
|
103
|
+
inDegree.set(name, 0);
|
|
104
|
+
}
|
|
105
|
+
for (const [, node] of nodes) {
|
|
106
|
+
for (const dep of node.deps) {
|
|
107
|
+
if (nodes.has(dep)) {
|
|
108
|
+
inDegree.set(dep, (inDegree.get(dep) ?? 0) + 1);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const queue = [];
|
|
113
|
+
for (const [name, degree] of inDegree) {
|
|
114
|
+
if (degree === 0) {
|
|
115
|
+
queue.push(name);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
while (queue.length > 0) {
|
|
119
|
+
const layer = [...queue];
|
|
120
|
+
layers.push(layer);
|
|
121
|
+
sorted.push(...layer);
|
|
122
|
+
queue.length = 0;
|
|
123
|
+
for (const name of layer) {
|
|
124
|
+
const node = nodes.get(name);
|
|
125
|
+
if (!node) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
for (const dep of node.deps) {
|
|
129
|
+
if (nodes.has(dep)) {
|
|
130
|
+
const newDegree = (inDegree.get(dep) ?? 0) - 1;
|
|
131
|
+
inDegree.set(dep, newDegree);
|
|
132
|
+
if (newDegree === 0) {
|
|
133
|
+
queue.push(dep);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (sorted.length < nodes.size) {
|
|
140
|
+
const remaining = /* @__PURE__ */ new Set();
|
|
141
|
+
for (const [name] of nodes) {
|
|
142
|
+
if (!sorted.includes(name)) {
|
|
143
|
+
remaining.add(name);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const cycles = findCircularDependencies(graph, remaining);
|
|
147
|
+
circular.push(...cycles);
|
|
148
|
+
}
|
|
149
|
+
return { layers, sorted, circular };
|
|
150
|
+
}
|
|
151
|
+
function findCircularDependencies(graph, subset) {
|
|
152
|
+
const { nodes } = graph;
|
|
153
|
+
const visited = /* @__PURE__ */ new Set();
|
|
154
|
+
const recStack = /* @__PURE__ */ new Set();
|
|
155
|
+
const cycles = [];
|
|
156
|
+
function dfs(node, path2) {
|
|
157
|
+
visited.add(node);
|
|
158
|
+
recStack.add(node);
|
|
159
|
+
path2.push(node);
|
|
160
|
+
const nodeData = nodes.get(node);
|
|
161
|
+
if (!nodeData) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
for (const dep of nodeData.deps) {
|
|
165
|
+
if (!nodes.has(dep)) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (subset && !subset.has(dep)) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (!visited.has(dep)) {
|
|
172
|
+
dfs(dep, [...path2]);
|
|
173
|
+
} else if (recStack.has(dep)) {
|
|
174
|
+
const cycleStart = path2.indexOf(dep);
|
|
175
|
+
if (cycleStart !== -1) {
|
|
176
|
+
const cycle = path2.slice(cycleStart);
|
|
177
|
+
cycles.push([...cycle, dep]);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
recStack.delete(node);
|
|
182
|
+
}
|
|
183
|
+
const nodesToCheck = subset ?? new Set(nodes.keys());
|
|
184
|
+
for (const node of nodesToCheck) {
|
|
185
|
+
if (!visited.has(node)) {
|
|
186
|
+
dfs(node, []);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return cycles;
|
|
190
|
+
}
|
|
191
|
+
function getBuildOrderForPackage(graph, packageName) {
|
|
192
|
+
const { nodes } = graph;
|
|
193
|
+
const allDeps = /* @__PURE__ */ new Set();
|
|
194
|
+
const queue = [packageName];
|
|
195
|
+
while (queue.length > 0) {
|
|
196
|
+
const current = queue.shift();
|
|
197
|
+
allDeps.add(current);
|
|
198
|
+
const node = nodes.get(current);
|
|
199
|
+
if (!node) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
for (const dep of node.deps) {
|
|
203
|
+
if (!allDeps.has(dep) && nodes.has(dep)) {
|
|
204
|
+
queue.push(dep);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const subgraph = {
|
|
209
|
+
nodes: /* @__PURE__ */ new Map(),
|
|
210
|
+
workspacePackages: graph.workspacePackages
|
|
211
|
+
};
|
|
212
|
+
for (const pkg of allDeps) {
|
|
213
|
+
const node = nodes.get(pkg);
|
|
214
|
+
if (node) {
|
|
215
|
+
subgraph.nodes.set(pkg, node);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return topologicalSort(subgraph);
|
|
219
|
+
}
|
|
220
|
+
function getReverseDependencies(graph, packageName) {
|
|
221
|
+
const { nodes } = graph;
|
|
222
|
+
const reverseDeps = /* @__PURE__ */ new Set();
|
|
223
|
+
for (const [name, node] of nodes) {
|
|
224
|
+
if (node.deps.has(packageName)) {
|
|
225
|
+
reverseDeps.add(name);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return reverseDeps;
|
|
229
|
+
}
|
|
230
|
+
function getImpactAnalysis(graph, packageName) {
|
|
231
|
+
const affected = /* @__PURE__ */ new Set();
|
|
232
|
+
const queue = [packageName];
|
|
233
|
+
while (queue.length > 0) {
|
|
234
|
+
const current = queue.shift();
|
|
235
|
+
const reverseDeps = getReverseDependencies(graph, current);
|
|
236
|
+
for (const dep of reverseDeps) {
|
|
237
|
+
if (!affected.has(dep)) {
|
|
238
|
+
affected.add(dep);
|
|
239
|
+
queue.push(dep);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return affected;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export { buildDependencyGraph, findCircularDependencies, getBuildOrderForPackage, getImpactAnalysis, getReverseDependencies, topologicalSort };
|
|
247
|
+
//# sourceMappingURL=index.js.map
|
|
248
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/graph/dependency-graph.ts"],"names":["path"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Dependency graph utilities for monorepo analysis\n *\n * Provides topological sorting, circular dependency detection,\n * and build order calculation based on workspace dependencies.\n */\n\nimport fs from 'node:fs';\nimport path from 'node:path';\n\nexport interface PackageNode {\n name: string;\n path: string;\n dir: string;\n deps: Set<string>;\n devDeps?: Set<string>;\n dependents: Set<string>; // Packages that depend on this one\n}\n\nexport interface DependencyGraph {\n nodes: Map<string, PackageNode>;\n workspacePackages: Set<string>;\n}\n\nexport interface TopologicalSortResult {\n layers: string[][]; // Build layers - each layer can build in parallel\n sorted: string[]; // Flattened topological order\n circular: string[][]; // Circular dependency chains\n}\n\n/**\n * Build dependency graph from monorepo packages\n */\nexport function buildDependencyGraph(rootDir: string): DependencyGraph {\n const nodes = new Map<string, PackageNode>();\n const workspacePackages = new Set<string>();\n\n if (!fs.existsSync(rootDir)) {\n return { nodes, workspacePackages };\n }\n\n const entries = fs.readdirSync(rootDir, { withFileTypes: true });\n\n // First pass: collect all workspace packages\n for (const entry of entries) {\n if (!entry.isDirectory() || !entry.name.startsWith('kb-labs-')) {continue;}\n\n const repoPath = path.join(rootDir, entry.name);\n const packagesDir = path.join(repoPath, 'packages');\n\n if (!fs.existsSync(packagesDir)) {continue;}\n\n const packageDirs = fs.readdirSync(packagesDir, { withFileTypes: true });\n\n for (const pkgDir of packageDirs) {\n if (!pkgDir.isDirectory()) {continue;}\n\n const packageJsonPath = path.join(packagesDir, pkgDir.name, 'package.json');\n\n if (fs.existsSync(packageJsonPath)) {\n const pkgJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));\n const packageName = pkgJson.name;\n\n if (packageName) {\n workspacePackages.add(packageName);\n }\n }\n }\n }\n\n // Second pass: build dependency graph\n for (const entry of entries) {\n if (!entry.isDirectory() || !entry.name.startsWith('kb-labs-')) {continue;}\n\n const repoPath = path.join(rootDir, entry.name);\n const packagesDir = path.join(repoPath, 'packages');\n\n if (!fs.existsSync(packagesDir)) {continue;}\n\n const packageDirs = fs.readdirSync(packagesDir, { withFileTypes: true });\n\n for (const pkgDir of packageDirs) {\n if (!pkgDir.isDirectory()) {continue;}\n\n const packageJsonPath = path.join(packagesDir, pkgDir.name, 'package.json');\n\n if (fs.existsSync(packageJsonPath)) {\n const pkgJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));\n const packageName = pkgJson.name;\n\n if (!packageName) {continue;}\n\n const deps = new Set<string>();\n const devDeps = new Set<string>();\n\n // Collect workspace dependencies\n const allDeps = {\n ...pkgJson.dependencies,\n ...pkgJson.devDependencies,\n };\n\n for (const dep of Object.keys(allDeps)) {\n if (workspacePackages.has(dep)) {\n deps.add(dep);\n }\n }\n\n // Separate dev dependencies\n if (pkgJson.devDependencies) {\n for (const dep of Object.keys(pkgJson.devDependencies)) {\n if (workspacePackages.has(dep)) {\n devDeps.add(dep);\n }\n }\n }\n\n nodes.set(packageName, {\n name: packageName,\n path: packageJsonPath,\n dir: path.dirname(packageJsonPath),\n deps,\n devDeps,\n dependents: new Set(),\n });\n }\n }\n }\n\n // Second pass: calculate reverse dependencies (dependents)\n for (const [packageName, node] of nodes) {\n for (const dep of node.deps) {\n const depNode = nodes.get(dep);\n if (depNode) {\n depNode.dependents.add(packageName);\n }\n }\n }\n\n return { nodes, workspacePackages };\n}\n\n/**\n * Topological sort using Kahn's algorithm\n * Returns build layers where each layer can be built in parallel\n */\nexport function topologicalSort(graph: DependencyGraph): TopologicalSortResult {\n const { nodes } = graph;\n const layers: string[][] = [];\n const sorted: string[] = [];\n const circular: string[][] = [];\n\n // Calculate in-degree for each node\n const inDegree = new Map<string, number>();\n for (const [name] of nodes) {\n inDegree.set(name, 0);\n }\n\n for (const [, node] of nodes) {\n for (const dep of node.deps) {\n if (nodes.has(dep)) {\n inDegree.set(dep, (inDegree.get(dep) ?? 0) + 1);\n }\n }\n }\n\n // Kahn's algorithm with layers\n const queue: string[] = [];\n for (const [name, degree] of inDegree) {\n if (degree === 0) {\n queue.push(name);\n }\n }\n\n while (queue.length > 0) {\n const layer: string[] = [...queue];\n layers.push(layer);\n sorted.push(...layer);\n queue.length = 0;\n\n for (const name of layer) {\n const node = nodes.get(name);\n if (!node) {continue;}\n\n for (const dep of node.deps) {\n if (nodes.has(dep)) {\n const newDegree = (inDegree.get(dep) ?? 0) - 1;\n inDegree.set(dep, newDegree);\n if (newDegree === 0) {\n queue.push(dep);\n }\n }\n }\n }\n }\n\n // Check for circular dependencies\n if (sorted.length < nodes.size) {\n const remaining = new Set<string>();\n for (const [name] of nodes) {\n if (!sorted.includes(name)) {\n remaining.add(name);\n }\n }\n\n // Find cycles using DFS\n const cycles = findCircularDependencies(graph, remaining);\n circular.push(...cycles);\n }\n\n return { layers, sorted, circular };\n}\n\n/**\n * Find circular dependencies using DFS\n */\nexport function findCircularDependencies(\n graph: DependencyGraph,\n subset?: Set<string>\n): string[][] {\n const { nodes } = graph;\n const visited = new Set<string>();\n const recStack = new Set<string>();\n const cycles: string[][] = [];\n\n function dfs(node: string, path: string[]): void {\n visited.add(node);\n recStack.add(node);\n path.push(node);\n\n const nodeData = nodes.get(node);\n if (!nodeData) {return;}\n\n for (const dep of nodeData.deps) {\n if (!nodes.has(dep)) {continue;}\n if (subset && !subset.has(dep)) {continue;}\n\n if (!visited.has(dep)) {\n dfs(dep, [...path]);\n } else if (recStack.has(dep)) {\n // Found cycle\n const cycleStart = path.indexOf(dep);\n if (cycleStart !== -1) {\n const cycle = path.slice(cycleStart);\n cycles.push([...cycle, dep]);\n }\n }\n }\n\n recStack.delete(node);\n }\n\n const nodesToCheck = subset ?? new Set(nodes.keys());\n for (const node of nodesToCheck) {\n if (!visited.has(node)) {\n dfs(node, []);\n }\n }\n\n return cycles;\n}\n\n/**\n * Get build order for a specific package and its dependencies\n */\nexport function getBuildOrderForPackage(\n graph: DependencyGraph,\n packageName: string\n): TopologicalSortResult {\n const { nodes } = graph;\n\n // Find all dependencies recursively\n const allDeps = new Set<string>();\n const queue = [packageName];\n\n while (queue.length > 0) {\n const current = queue.shift()!;\n allDeps.add(current);\n\n const node = nodes.get(current);\n if (!node) {continue;}\n\n for (const dep of node.deps) {\n if (!allDeps.has(dep) && nodes.has(dep)) {\n queue.push(dep);\n }\n }\n }\n\n // Create subgraph with only relevant packages\n const subgraph: DependencyGraph = {\n nodes: new Map(),\n workspacePackages: graph.workspacePackages,\n };\n\n for (const pkg of allDeps) {\n const node = nodes.get(pkg);\n if (node) {\n subgraph.nodes.set(pkg, node);\n }\n }\n\n return topologicalSort(subgraph);\n}\n\n/**\n * Reverse dependency graph - who depends on this package\n */\nexport function getReverseDependencies(\n graph: DependencyGraph,\n packageName: string\n): Set<string> {\n const { nodes } = graph;\n const reverseDeps = new Set<string>();\n\n for (const [name, node] of nodes) {\n if (node.deps.has(packageName)) {\n reverseDeps.add(name);\n }\n }\n\n return reverseDeps;\n}\n\n/**\n * Get impact analysis - all packages affected by changes to this package\n */\nexport function getImpactAnalysis(\n graph: DependencyGraph,\n packageName: string\n): Set<string> {\n const affected = new Set<string>();\n const queue = [packageName];\n\n while (queue.length > 0) {\n const current = queue.shift()!;\n const reverseDeps = getReverseDependencies(graph, current);\n\n for (const dep of reverseDeps) {\n if (!affected.has(dep)) {\n affected.add(dep);\n queue.push(dep);\n }\n }\n }\n\n return affected;\n}\n"]}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calculate monorepo health score
|
|
3
|
+
*
|
|
4
|
+
* Atomic functions for health checks and scoring.
|
|
5
|
+
*/
|
|
6
|
+
interface HealthResult {
|
|
7
|
+
score: number;
|
|
8
|
+
grade: 'A' | 'B' | 'C' | 'D' | 'F';
|
|
9
|
+
issues: HealthIssue[];
|
|
10
|
+
}
|
|
11
|
+
interface HealthIssue {
|
|
12
|
+
type: 'duplicate' | 'unused' | 'missing' | 'structure' | 'readme';
|
|
13
|
+
severity: 'high' | 'medium' | 'low';
|
|
14
|
+
message: string;
|
|
15
|
+
count: number;
|
|
16
|
+
penalty: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Check for duplicate dependencies across packages
|
|
20
|
+
*/
|
|
21
|
+
declare function checkDuplicateDependencies(rootDir: string, pkgFiles?: string[]): Promise<HealthIssue | null>;
|
|
22
|
+
/**
|
|
23
|
+
* Check for packages missing README
|
|
24
|
+
*/
|
|
25
|
+
declare function checkMissingReadmes(rootDir: string, packageJsonFiles?: string[]): Promise<HealthIssue | null>;
|
|
26
|
+
/**
|
|
27
|
+
* Calculate health score from issues
|
|
28
|
+
*/
|
|
29
|
+
declare function calculateHealthScore(issues: HealthIssue[]): number;
|
|
30
|
+
/**
|
|
31
|
+
* Convert score to letter grade
|
|
32
|
+
*/
|
|
33
|
+
declare function scoreToGrade(score: number): 'A' | 'B' | 'C' | 'D' | 'F';
|
|
34
|
+
/**
|
|
35
|
+
* Calculate complete health report
|
|
36
|
+
*/
|
|
37
|
+
declare function calculateHealth(rootDir: string): Promise<HealthResult>;
|
|
38
|
+
|
|
39
|
+
export { type HealthIssue, type HealthResult, calculateHealth, calculateHealthScore, checkDuplicateDependencies, checkMissingReadmes, scoreToGrade };
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { readFile, access } from 'fs/promises';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import globby from 'globby';
|
|
4
|
+
|
|
5
|
+
// src/health/calculate-health.ts
|
|
6
|
+
async function checkDuplicateDependencies(rootDir, pkgFiles) {
|
|
7
|
+
const packageJsonFiles = pkgFiles ?? await globby("**/package.json", {
|
|
8
|
+
cwd: rootDir,
|
|
9
|
+
ignore: ["**/node_modules/**", "**/.git/**"],
|
|
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 {
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const duplicates = Array.from(depVersions.entries()).filter(
|
|
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
|
+
};
|
|
49
|
+
}
|
|
50
|
+
async function checkMissingReadmes(rootDir, packageJsonFiles) {
|
|
51
|
+
const pkgFiles = packageJsonFiles ?? await globby("**/package.json", {
|
|
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
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function calculateHealthScore(issues) {
|
|
82
|
+
const baseScore = 100;
|
|
83
|
+
const totalPenalty = issues.reduce((sum, issue) => sum + issue.penalty, 0);
|
|
84
|
+
return Math.max(0, baseScore - totalPenalty);
|
|
85
|
+
}
|
|
86
|
+
function scoreToGrade(score) {
|
|
87
|
+
if (score >= 90) {
|
|
88
|
+
return "A";
|
|
89
|
+
}
|
|
90
|
+
if (score >= 80) {
|
|
91
|
+
return "B";
|
|
92
|
+
}
|
|
93
|
+
if (score >= 70) {
|
|
94
|
+
return "C";
|
|
95
|
+
}
|
|
96
|
+
if (score >= 60) {
|
|
97
|
+
return "D";
|
|
98
|
+
}
|
|
99
|
+
return "F";
|
|
100
|
+
}
|
|
101
|
+
async function calculateHealth(rootDir) {
|
|
102
|
+
const issues = [];
|
|
103
|
+
const packageJsonFiles = await globby("**/package.json", {
|
|
104
|
+
cwd: rootDir,
|
|
105
|
+
ignore: ["**/node_modules/**", "**/.git/**", "**/.kb/**", "**/dist/**"],
|
|
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);
|
|
115
|
+
}
|
|
116
|
+
if (readmesIssue) {
|
|
117
|
+
issues.push(readmesIssue);
|
|
118
|
+
}
|
|
119
|
+
const score = calculateHealthScore(issues);
|
|
120
|
+
const grade = scoreToGrade(score);
|
|
121
|
+
return {
|
|
122
|
+
score,
|
|
123
|
+
grade,
|
|
124
|
+
issues
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export { calculateHealth, calculateHealthScore, checkDuplicateDependencies, checkMissingReadmes, scoreToGrade };
|
|
129
|
+
//# sourceMappingURL=index.js.map
|
|
130
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/health/calculate-health.ts"],"names":[],"mappings":";;;;;AA2BA,eAAsB,0BAAA,CAA2B,SAAiB,QAAA,EAAkD;AAClH,EAAA,MAAM,gBAAA,GAAmB,QAAA,IAAY,MAAM,MAAA,CAAO,iBAAA,EAAmB;AAAA,IACnE,GAAA,EAAK,OAAA;AAAA,IACL,MAAA,EAAQ,CAAC,oBAAA,EAAsB,YAAY,CAAA;AAAA,IAC3C,QAAA,EAAU;AAAA,GACX,CAAA;AAGD,EAAA,MAAM,WAAA,uBAAkB,GAAA,EAAyB;AACjD,EAAA,MAAM,QAAA,GAAW,MAAM,OAAA,CAAQ,GAAA;AAAA,IAC7B,gBAAA,CAAiB,GAAA,CAAI,CAAA,CAAA,KAAK,QAAA,CAAS,CAAA,EAAG,OAAO,CAAA,CAAE,KAAA,CAAM,MAAM,IAAI,CAAC;AAAA,GAClE;AAEA,EAAA,KAAA,MAAW,WAAW,QAAA,EAAU;AAC9B,IAAA,IAAI,CAAC,OAAA,EAAS;AAAC,MAAA;AAAA,IAAS;AACxB,IAAA,IAAI;AACF,MAAA,MAAM,GAAA,GAAM,IAAA,CAAK,KAAA,CAAM,OAAO,CAAA;AAC9B,MAAA,MAAM,UAAU,EAAE,GAAG,IAAI,YAAA,EAAc,GAAG,IAAI,eAAA,EAAgB;AAC9D,MAAA,KAAA,MAAW,CAAC,IAAA,EAAM,OAAO,KAAK,MAAA,CAAO,OAAA,CAAQ,OAAO,CAAA,EAAG;AACrD,QAAA,IAAI,OAAO,YAAY,QAAA,EAAU;AAAC,UAAA;AAAA,QAAS;AAC3C,QAAA,IAAI,CAAC,WAAA,CAAY,GAAA,CAAI,IAAI,CAAA,EAAG;AAAC,UAAA,WAAA,CAAY,GAAA,CAAI,IAAA,kBAAM,IAAI,GAAA,EAAK,CAAA;AAAA,QAAE;AAC9D,QAAA,WAAA,CAAY,GAAA,CAAI,IAAI,CAAA,CAAG,GAAA,CAAI,OAAO,CAAA;AAAA,MACpC;AAAA,IACF,CAAA,CAAA,MAAQ;AAAA,IAAqB;AAAA,EAC/B;AAGA,EAAA,MAAM,aAAa,KAAA,CAAM,IAAA,CAAK,WAAA,CAAY,OAAA,EAAS,CAAA,CAAE,MAAA;AAAA,IACnD,CAAC,GAAG,QAAQ,CAAA,KAAM,SAAS,IAAA,GAAO;AAAA,GACpC;AAEA,EAAA,IAAI,UAAA,CAAW,WAAW,CAAA,EAAG;AAAC,IAAA,OAAO,IAAA;AAAA,EAAK;AAE1C,EAAA,MAAM,UAAU,IAAA,CAAK,GAAA,CAAI,UAAA,CAAW,MAAA,GAAS,GAAG,EAAE,CAAA;AAElD,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,WAAA;AAAA,IACN,QAAA,EAAU,WAAW,MAAA,GAAS,EAAA,GAAK,SAAS,UAAA,CAAW,MAAA,GAAS,KAAK,QAAA,GAAW,KAAA;AAAA,IAChF,OAAA,EAAS,CAAA,MAAA,EAAS,UAAA,CAAW,MAAM,CAAA,+CAAA,CAAA;AAAA,IACnC,OAAO,UAAA,CAAW,MAAA;AAAA,IAClB;AAAA,GACF;AACF;AAKA,eAAsB,mBAAA,CAAoB,SAAiB,gBAAA,EAA0D;AACnH,EAAA,MAAM,QAAA,GAAW,gBAAA,IAAoB,MAAM,MAAA,CAAO,iBAAA,EAAmB;AAAA,IACnE,GAAA,EAAK,OAAA;AAAA,IACL,MAAA,EAAQ,CAAC,oBAAA,EAAsB,YAAA,EAAc,cAAc,CAAA;AAAA,IAC3D,QAAA,EAAU;AAAA,GACX,CAAA;AAED,EAAA,MAAM,SAAA,GAAY,OAAO,OAAA,KAAsC;AAC7D,IAAA,MAAM,GAAA,GAAM,IAAA,CAAK,OAAA,EAAS,IAAI,CAAA;AAC9B,IAAA,KAAA,MAAW,IAAA,IAAQ,CAAC,WAAA,EAAa,WAAA,EAAa,WAAW,CAAA,EAAG;AAC1D,MAAA,IAAI;AACF,QAAA,MAAM,MAAA,CAAO,IAAA,CAAK,GAAA,EAAK,IAAI,CAAC,CAAA;AAC5B,QAAA,OAAO,IAAA;AAAA,MACT,CAAA,CAAA,MAAQ;AAAA,MAAkB;AAAA,IAC5B;AACA,IAAA,OAAO,KAAA;AAAA,EACT,CAAA;AAEA,EAAA,MAAM,UAAU,MAAM,OAAA,CAAQ,IAAI,QAAA,CAAS,GAAA,CAAI,SAAS,CAAC,CAAA;AACzD,EAAA,MAAM,eAAe,OAAA,CAAQ,MAAA,CAAO,CAAA,GAAA,KAAO,CAAC,GAAG,CAAA,CAAE,MAAA;AAEjD,EAAA,IAAI,iBAAiB,CAAA,EAAG;AAAC,IAAA,OAAO,IAAA;AAAA,EAAK;AAErC,EAAA,MAAM,OAAA,GAAU,IAAA,CAAK,GAAA,CAAI,YAAA,EAAc,EAAE,CAAA;AAEzC,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,QAAA;AAAA,IACN,UAAU,YAAA,GAAe,EAAA,GAAK,MAAA,GAAS,YAAA,GAAe,KAAK,QAAA,GAAW,KAAA;AAAA,IACtE,OAAA,EAAS,SAAS,YAAY,CAAA,wBAAA,CAAA;AAAA,IAC9B,KAAA,EAAO,YAAA;AAAA,IACP;AAAA,GACF;AACF;AAKO,SAAS,qBAAqB,MAAA,EAA+B;AAClE,EAAA,MAAM,SAAA,GAAY,GAAA;AAClB,EAAA,MAAM,YAAA,GAAe,OAAO,MAAA,CAAO,CAAC,KAAK,KAAA,KAAU,GAAA,GAAM,KAAA,CAAM,OAAA,EAAS,CAAC,CAAA;AAEzE,EAAA,OAAO,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,SAAA,GAAY,YAAY,CAAA;AAC7C;AAKO,SAAS,aAAa,KAAA,EAA4C;AACvE,EAAA,IAAI,SAAS,EAAA,EAAI;AAAC,IAAA,OAAO,GAAA;AAAA,EAAI;AAC7B,EAAA,IAAI,SAAS,EAAA,EAAI;AAAC,IAAA,OAAO,GAAA;AAAA,EAAI;AAC7B,EAAA,IAAI,SAAS,EAAA,EAAI;AAAC,IAAA,OAAO,GAAA;AAAA,EAAI;AAC7B,EAAA,IAAI,SAAS,EAAA,EAAI;AAAC,IAAA,OAAO,GAAA;AAAA,EAAI;AAC7B,EAAA,OAAO,GAAA;AACT;AAKA,eAAsB,gBAAgB,OAAA,EAAwC;AAC5E,EAAA,MAAM,SAAwB,EAAC;AAI/B,EAAA,MAAM,gBAAA,GAAmB,MAAM,MAAA,CAAO,iBAAA,EAAmB;AAAA,IACvD,GAAA,EAAK,OAAA;AAAA,IACL,MAAA,EAAQ,CAAC,oBAAA,EAAsB,YAAA,EAAc,aAAa,YAAY,CAAA;AAAA,IACtE,QAAA,EAAU,IAAA;AAAA,IACV,IAAA,EAAM;AAAA,GACP,CAAA;AAED,EAAA,MAAM,CAAC,eAAA,EAAiB,YAAY,CAAA,GAAI,MAAM,QAAQ,GAAA,CAAI;AAAA,IACxD,0BAAA,CAA2B,SAAS,gBAAgB,CAAA;AAAA,IACpD,mBAAA,CAAoB,SAAS,gBAAgB;AAAA,GAC9C,CAAA;AAED,EAAA,IAAI,eAAA,EAAiB;AAAC,IAAA,MAAA,CAAO,KAAK,eAAe,CAAA;AAAA,EAAE;AACnD,EAAA,IAAI,YAAA,EAAc;AAAC,IAAA,MAAA,CAAO,KAAK,YAAY,CAAA;AAAA,EAAE;AAE7C,EAAA,MAAM,KAAA,GAAQ,qBAAqB,MAAM,CAAA;AACzC,EAAA,MAAM,KAAA,GAAQ,aAAa,KAAK,CAAA;AAEhC,EAAA,OAAO;AAAA,IACL,KAAA;AAAA,IACA,KAAA;AAAA,IACA;AAAA,GACF;AACF","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"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { DependencyGraph, PackageNode, TopologicalSortResult, buildDependencyGraph, findCircularDependencies, getBuildOrderForPackage, getImpactAnalysis, getReverseDependencies, topologicalSort } from './graph/index.js';
|
|
2
|
+
export { MonorepoStats, calculateLinesOfCode, calculateSize, calculateStats, countPackages, formatBytes } from './stats/index.js';
|
|
3
|
+
export { HealthIssue, HealthResult, calculateHealth, calculateHealthScore, checkDuplicateDependencies, checkMissingReadmes, scoreToGrade } from './health/index.js';
|
|
4
|
+
export { DependencyAnalysis, DuplicateDependency, MissingDependency, UnusedDependency, VersionInfo, analyzeDependencies, analyzeDuplicateDependencies, analyzeMissingDependencies, analyzeUnusedDependencies } from './dependencies/index.js';
|
|
5
|
+
export { BuildCheckOptions, BuildCheckResult, checkBuilds } from './builds/index.js';
|
|
6
|
+
export { TypeAnalysisOptions, TypeAnalysisResult, analyzeTypes } from './types/index.js';
|
|
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';
|