@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,273 @@
|
|
|
1
|
+
import { readFile, access, stat } from 'fs/promises';
|
|
2
|
+
import path, { join } from 'path';
|
|
3
|
+
import globby from 'globby';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
|
|
6
|
+
// src/stale/analyze-stale.ts
|
|
7
|
+
function buildDependencyGraph(rootDir) {
|
|
8
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
9
|
+
const workspacePackages = /* @__PURE__ */ new Set();
|
|
10
|
+
if (!fs.existsSync(rootDir)) {
|
|
11
|
+
return { nodes, workspacePackages };
|
|
12
|
+
}
|
|
13
|
+
const entries = fs.readdirSync(rootDir, { withFileTypes: true });
|
|
14
|
+
for (const entry of entries) {
|
|
15
|
+
if (!entry.isDirectory() || !entry.name.startsWith("kb-labs-")) {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const repoPath = path.join(rootDir, entry.name);
|
|
19
|
+
const packagesDir = path.join(repoPath, "packages");
|
|
20
|
+
if (!fs.existsSync(packagesDir)) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const packageDirs = fs.readdirSync(packagesDir, { withFileTypes: true });
|
|
24
|
+
for (const pkgDir of packageDirs) {
|
|
25
|
+
if (!pkgDir.isDirectory()) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const packageJsonPath = path.join(packagesDir, pkgDir.name, "package.json");
|
|
29
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
30
|
+
const pkgJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
31
|
+
const packageName = pkgJson.name;
|
|
32
|
+
if (packageName) {
|
|
33
|
+
workspacePackages.add(packageName);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
for (const entry of entries) {
|
|
39
|
+
if (!entry.isDirectory() || !entry.name.startsWith("kb-labs-")) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const repoPath = path.join(rootDir, entry.name);
|
|
43
|
+
const packagesDir = path.join(repoPath, "packages");
|
|
44
|
+
if (!fs.existsSync(packagesDir)) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const packageDirs = fs.readdirSync(packagesDir, { withFileTypes: true });
|
|
48
|
+
for (const pkgDir of packageDirs) {
|
|
49
|
+
if (!pkgDir.isDirectory()) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const packageJsonPath = path.join(packagesDir, pkgDir.name, "package.json");
|
|
53
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
54
|
+
const pkgJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
55
|
+
const packageName = pkgJson.name;
|
|
56
|
+
if (!packageName) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const deps = /* @__PURE__ */ new Set();
|
|
60
|
+
const devDeps = /* @__PURE__ */ new Set();
|
|
61
|
+
const allDeps = {
|
|
62
|
+
...pkgJson.dependencies,
|
|
63
|
+
...pkgJson.devDependencies
|
|
64
|
+
};
|
|
65
|
+
for (const dep of Object.keys(allDeps)) {
|
|
66
|
+
if (workspacePackages.has(dep)) {
|
|
67
|
+
deps.add(dep);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (pkgJson.devDependencies) {
|
|
71
|
+
for (const dep of Object.keys(pkgJson.devDependencies)) {
|
|
72
|
+
if (workspacePackages.has(dep)) {
|
|
73
|
+
devDeps.add(dep);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
nodes.set(packageName, {
|
|
78
|
+
name: packageName,
|
|
79
|
+
path: packageJsonPath,
|
|
80
|
+
dir: path.dirname(packageJsonPath),
|
|
81
|
+
deps,
|
|
82
|
+
devDeps,
|
|
83
|
+
dependents: /* @__PURE__ */ new Set()
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
for (const [packageName, node] of nodes) {
|
|
89
|
+
for (const dep of node.deps) {
|
|
90
|
+
const depNode = nodes.get(dep);
|
|
91
|
+
if (depNode) {
|
|
92
|
+
depNode.dependents.add(packageName);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return { nodes, workspacePackages };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/stale/analyze-stale.ts
|
|
100
|
+
async function getLatestModTime(files) {
|
|
101
|
+
if (files.length === 0) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
let latest = null;
|
|
105
|
+
for (const file of files) {
|
|
106
|
+
try {
|
|
107
|
+
const stats = await stat(file);
|
|
108
|
+
if (!latest || stats.mtime > latest) {
|
|
109
|
+
latest = stats.mtime;
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return latest;
|
|
115
|
+
}
|
|
116
|
+
async function distExists(packagePath) {
|
|
117
|
+
try {
|
|
118
|
+
await access(join(packagePath, "dist"));
|
|
119
|
+
return true;
|
|
120
|
+
} catch {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async function analyzePackage(packagePath, packageJson, graph) {
|
|
125
|
+
const name = packageJson.name;
|
|
126
|
+
const hasDistCheck = await distExists(packagePath);
|
|
127
|
+
if (!hasDistCheck) {
|
|
128
|
+
const node = graph.nodes.get(name);
|
|
129
|
+
const affectedPackages = node ? Array.from(node.dependents) : [];
|
|
130
|
+
return {
|
|
131
|
+
name,
|
|
132
|
+
path: packagePath,
|
|
133
|
+
reason: "missing-dist",
|
|
134
|
+
affectedPackages,
|
|
135
|
+
affectedCount: affectedPackages.length,
|
|
136
|
+
severity: affectedPackages.length > 20 ? "critical" : affectedPackages.length > 10 ? "high" : "medium"
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
const srcFiles = await globby("src/**/*.{ts,tsx,js,jsx}", {
|
|
140
|
+
cwd: packagePath,
|
|
141
|
+
absolute: true,
|
|
142
|
+
ignore: ["**/*.test.ts", "**/*.spec.ts", "**/__tests__/**"]
|
|
143
|
+
});
|
|
144
|
+
const distFiles = await globby("dist/**/*.{js,mjs,cjs,d.ts}", {
|
|
145
|
+
cwd: packagePath,
|
|
146
|
+
absolute: true
|
|
147
|
+
});
|
|
148
|
+
const srcModTime = await getLatestModTime(srcFiles);
|
|
149
|
+
const distModTime = await getLatestModTime(distFiles);
|
|
150
|
+
if (srcModTime && distModTime && srcModTime > distModTime) {
|
|
151
|
+
const node = graph.nodes.get(name);
|
|
152
|
+
const affectedPackages = node ? Array.from(node.dependents) : [];
|
|
153
|
+
return {
|
|
154
|
+
name,
|
|
155
|
+
path: packagePath,
|
|
156
|
+
reason: "source-newer",
|
|
157
|
+
sourceModified: srcModTime.toISOString(),
|
|
158
|
+
distModified: distModTime.toISOString(),
|
|
159
|
+
affectedPackages,
|
|
160
|
+
affectedCount: affectedPackages.length,
|
|
161
|
+
severity: affectedPackages.length > 20 ? "critical" : affectedPackages.length > 10 ? "high" : affectedPackages.length > 5 ? "medium" : "low"
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
if (distModTime) {
|
|
165
|
+
packageJson.dependencies || {};
|
|
166
|
+
const node = graph.nodes.get(name);
|
|
167
|
+
if (node) {
|
|
168
|
+
for (const depName of node.deps) {
|
|
169
|
+
const depNode = graph.nodes.get(depName);
|
|
170
|
+
if (depNode) {
|
|
171
|
+
const depPackages = await globby("**/package.json", {
|
|
172
|
+
cwd: join(packagePath, "../.."),
|
|
173
|
+
// Go up to monorepo root
|
|
174
|
+
absolute: true,
|
|
175
|
+
ignore: ["**/node_modules/**"]
|
|
176
|
+
});
|
|
177
|
+
for (const depPkgPath of depPackages) {
|
|
178
|
+
try {
|
|
179
|
+
const depPkg = JSON.parse(await readFile(depPkgPath, "utf-8"));
|
|
180
|
+
if (depPkg.name === depName) {
|
|
181
|
+
const depDir = join(depPkgPath, "..");
|
|
182
|
+
const depDistFiles = await globby("dist/**/*.{js,mjs,cjs}", {
|
|
183
|
+
cwd: depDir,
|
|
184
|
+
absolute: true
|
|
185
|
+
});
|
|
186
|
+
const depDistModTime = await getLatestModTime(depDistFiles);
|
|
187
|
+
if (depDistModTime && depDistModTime > distModTime) {
|
|
188
|
+
const affectedPackages = node ? Array.from(node.dependents) : [];
|
|
189
|
+
return {
|
|
190
|
+
name,
|
|
191
|
+
path: packagePath,
|
|
192
|
+
reason: "dependency-rebuilt",
|
|
193
|
+
sourceModified: srcModTime?.toISOString(),
|
|
194
|
+
distModified: distModTime.toISOString(),
|
|
195
|
+
affectedPackages,
|
|
196
|
+
affectedCount: affectedPackages.length,
|
|
197
|
+
severity: affectedPackages.length > 20 ? "critical" : affectedPackages.length > 10 ? "high" : affectedPackages.length > 5 ? "medium" : "low"
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
} catch {
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
function getMaxDepth(graph, packageName, visited = /* @__PURE__ */ new Set()) {
|
|
211
|
+
if (visited.has(packageName)) {
|
|
212
|
+
return 0;
|
|
213
|
+
}
|
|
214
|
+
visited.add(packageName);
|
|
215
|
+
const node = graph.nodes.get(packageName);
|
|
216
|
+
if (!node || node.dependents.size === 0) {
|
|
217
|
+
return 0;
|
|
218
|
+
}
|
|
219
|
+
let maxDepth = 0;
|
|
220
|
+
for (const dependent of node.dependents) {
|
|
221
|
+
const depth = 1 + getMaxDepth(graph, dependent, new Set(visited));
|
|
222
|
+
if (depth > maxDepth) {
|
|
223
|
+
maxDepth = depth;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return maxDepth;
|
|
227
|
+
}
|
|
228
|
+
async function analyzeStalePackages(rootDir, detailed = false) {
|
|
229
|
+
const graph = buildDependencyGraph(rootDir);
|
|
230
|
+
const packageJsonFiles = await globby("**/package.json", {
|
|
231
|
+
cwd: rootDir,
|
|
232
|
+
ignore: ["**/node_modules/**", "**/.git/**", "**/.kb/**", "**/dist/**", "package.json"],
|
|
233
|
+
absolute: true,
|
|
234
|
+
deep: 6
|
|
235
|
+
});
|
|
236
|
+
const results = await Promise.all(
|
|
237
|
+
packageJsonFiles.map(async (pkgPath) => {
|
|
238
|
+
try {
|
|
239
|
+
const content = await readFile(pkgPath, "utf-8");
|
|
240
|
+
const pkg = JSON.parse(content);
|
|
241
|
+
const packagePath = join(pkgPath, "..");
|
|
242
|
+
return await analyzePackage(packagePath, pkg, graph);
|
|
243
|
+
} catch {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
})
|
|
247
|
+
);
|
|
248
|
+
const stalePackages = results.filter((r) => r !== null);
|
|
249
|
+
const allAffected = /* @__PURE__ */ new Set();
|
|
250
|
+
for (const stalePkg of stalePackages) {
|
|
251
|
+
for (const affected of stalePkg.affectedPackages) {
|
|
252
|
+
allAffected.add(affected);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
let criticalChains = [];
|
|
256
|
+
if (detailed) {
|
|
257
|
+
criticalChains = stalePackages.filter((pkg) => pkg.affectedCount > 5).map((pkg) => ({
|
|
258
|
+
root: pkg.name,
|
|
259
|
+
affected: pkg.affectedPackages,
|
|
260
|
+
depth: getMaxDepth(graph, pkg.name)
|
|
261
|
+
})).sort((a, b) => b.affected.length - a.affected.length).slice(0, 10);
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
stalePackages: stalePackages.sort((a, b) => b.affectedCount - a.affectedCount),
|
|
265
|
+
totalStale: stalePackages.length,
|
|
266
|
+
totalAffected: allAffected.size,
|
|
267
|
+
criticalChains
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export { analyzeStalePackages };
|
|
272
|
+
//# sourceMappingURL=index.js.map
|
|
273
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/graph/dependency-graph.ts","../../src/stale/analyze-stale.ts"],"names":[],"mappings":";;;;;;AAiCO,SAAS,qBAAqB,OAAA,EAAkC;AACrE,EAAA,MAAM,KAAA,uBAAY,GAAA,EAAyB;AAC3C,EAAA,MAAM,iBAAA,uBAAwB,GAAA,EAAY;AAE1C,EAAA,IAAI,CAAC,EAAA,CAAG,UAAA,CAAW,OAAO,CAAA,EAAG;AAC3B,IAAA,OAAO,EAAE,OAAO,iBAAA,EAAkB;AAAA,EACpC;AAEA,EAAA,MAAM,UAAU,EAAA,CAAG,WAAA,CAAY,SAAS,EAAE,aAAA,EAAe,MAAM,CAAA;AAG/D,EAAA,KAAA,MAAW,SAAS,OAAA,EAAS;AAC3B,IAAA,IAAI,CAAC,MAAM,WAAA,EAAY,IAAK,CAAC,KAAA,CAAM,IAAA,CAAK,UAAA,CAAW,UAAU,CAAA,EAAG;AAAC,MAAA;AAAA,IAAS;AAE1E,IAAA,MAAM,QAAA,GAAW,IAAA,CAAK,IAAA,CAAK,OAAA,EAAS,MAAM,IAAI,CAAA;AAC9C,IAAA,MAAM,WAAA,GAAc,IAAA,CAAK,IAAA,CAAK,QAAA,EAAU,UAAU,CAAA;AAElD,IAAA,IAAI,CAAC,EAAA,CAAG,UAAA,CAAW,WAAW,CAAA,EAAG;AAAC,MAAA;AAAA,IAAS;AAE3C,IAAA,MAAM,cAAc,EAAA,CAAG,WAAA,CAAY,aAAa,EAAE,aAAA,EAAe,MAAM,CAAA;AAEvE,IAAA,KAAA,MAAW,UAAU,WAAA,EAAa;AAChC,MAAA,IAAI,CAAC,MAAA,CAAO,WAAA,EAAY,EAAG;AAAC,QAAA;AAAA,MAAS;AAErC,MAAA,MAAM,kBAAkB,IAAA,CAAK,IAAA,CAAK,WAAA,EAAa,MAAA,CAAO,MAAM,cAAc,CAAA;AAE1E,MAAA,IAAI,EAAA,CAAG,UAAA,CAAW,eAAe,CAAA,EAAG;AAClC,QAAA,MAAM,UAAU,IAAA,CAAK,KAAA,CAAM,GAAG,YAAA,CAAa,eAAA,EAAiB,OAAO,CAAC,CAAA;AACpE,QAAA,MAAM,cAAc,OAAA,CAAQ,IAAA;AAE5B,QAAA,IAAI,WAAA,EAAa;AACf,UAAA,iBAAA,CAAkB,IAAI,WAAW,CAAA;AAAA,QACnC;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAGA,EAAA,KAAA,MAAW,SAAS,OAAA,EAAS;AAC3B,IAAA,IAAI,CAAC,MAAM,WAAA,EAAY,IAAK,CAAC,KAAA,CAAM,IAAA,CAAK,UAAA,CAAW,UAAU,CAAA,EAAG;AAAC,MAAA;AAAA,IAAS;AAE1E,IAAA,MAAM,QAAA,GAAW,IAAA,CAAK,IAAA,CAAK,OAAA,EAAS,MAAM,IAAI,CAAA;AAC9C,IAAA,MAAM,WAAA,GAAc,IAAA,CAAK,IAAA,CAAK,QAAA,EAAU,UAAU,CAAA;AAElD,IAAA,IAAI,CAAC,EAAA,CAAG,UAAA,CAAW,WAAW,CAAA,EAAG;AAAC,MAAA;AAAA,IAAS;AAE3C,IAAA,MAAM,cAAc,EAAA,CAAG,WAAA,CAAY,aAAa,EAAE,aAAA,EAAe,MAAM,CAAA;AAEvE,IAAA,KAAA,MAAW,UAAU,WAAA,EAAa;AAChC,MAAA,IAAI,CAAC,MAAA,CAAO,WAAA,EAAY,EAAG;AAAC,QAAA;AAAA,MAAS;AAErC,MAAA,MAAM,kBAAkB,IAAA,CAAK,IAAA,CAAK,WAAA,EAAa,MAAA,CAAO,MAAM,cAAc,CAAA;AAE1E,MAAA,IAAI,EAAA,CAAG,UAAA,CAAW,eAAe,CAAA,EAAG;AAClC,QAAA,MAAM,UAAU,IAAA,CAAK,KAAA,CAAM,GAAG,YAAA,CAAa,eAAA,EAAiB,OAAO,CAAC,CAAA;AACpE,QAAA,MAAM,cAAc,OAAA,CAAQ,IAAA;AAE5B,QAAA,IAAI,CAAC,WAAA,EAAa;AAAC,UAAA;AAAA,QAAS;AAE5B,QAAA,MAAM,IAAA,uBAAW,GAAA,EAAY;AAC7B,QAAA,MAAM,OAAA,uBAAc,GAAA,EAAY;AAGhC,QAAA,MAAM,OAAA,GAAU;AAAA,UACd,GAAG,OAAA,CAAQ,YAAA;AAAA,UACX,GAAG,OAAA,CAAQ;AAAA,SACb;AAEA,QAAA,KAAA,MAAW,GAAA,IAAO,MAAA,CAAO,IAAA,CAAK,OAAO,CAAA,EAAG;AACtC,UAAA,IAAI,iBAAA,CAAkB,GAAA,CAAI,GAAG,CAAA,EAAG;AAC9B,YAAA,IAAA,CAAK,IAAI,GAAG,CAAA;AAAA,UACd;AAAA,QACF;AAGA,QAAA,IAAI,QAAQ,eAAA,EAAiB;AAC3B,UAAA,KAAA,MAAW,GAAA,IAAO,MAAA,CAAO,IAAA,CAAK,OAAA,CAAQ,eAAe,CAAA,EAAG;AACtD,YAAA,IAAI,iBAAA,CAAkB,GAAA,CAAI,GAAG,CAAA,EAAG;AAC9B,cAAA,OAAA,CAAQ,IAAI,GAAG,CAAA;AAAA,YACjB;AAAA,UACF;AAAA,QACF;AAEA,QAAA,KAAA,CAAM,IAAI,WAAA,EAAa;AAAA,UACrB,IAAA,EAAM,WAAA;AAAA,UACN,IAAA,EAAM,eAAA;AAAA,UACN,GAAA,EAAK,IAAA,CAAK,OAAA,CAAQ,eAAe,CAAA;AAAA,UACjC,IAAA;AAAA,UACA,OAAA;AAAA,UACA,UAAA,sBAAgB,GAAA;AAAI,SACrB,CAAA;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAGA,EAAA,KAAA,MAAW,CAAC,WAAA,EAAa,IAAI,CAAA,IAAK,KAAA,EAAO;AACvC,IAAA,KAAA,MAAW,GAAA,IAAO,KAAK,IAAA,EAAM;AAC3B,MAAA,MAAM,OAAA,GAAU,KAAA,CAAM,GAAA,CAAI,GAAG,CAAA;AAC7B,MAAA,IAAI,OAAA,EAAS;AACX,QAAA,OAAA,CAAQ,UAAA,CAAW,IAAI,WAAW,CAAA;AAAA,MACpC;AAAA,IACF;AAAA,EACF;AAEA,EAAA,OAAO,EAAE,OAAO,iBAAA,EAAkB;AACpC;;;ACtGA,eAAe,iBAAiB,KAAA,EAAuC;AACrE,EAAA,IAAI,KAAA,CAAM,WAAW,CAAA,EAAG;AAAC,IAAA,OAAO,IAAA;AAAA,EAAK;AAErC,EAAA,IAAI,MAAA,GAAsB,IAAA;AAE1B,EAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACxB,IAAA,IAAI;AACF,MAAA,MAAM,KAAA,GAAQ,MAAM,IAAA,CAAK,IAAI,CAAA;AAC7B,MAAA,IAAI,CAAC,MAAA,IAAU,KAAA,CAAM,KAAA,GAAQ,MAAA,EAAQ;AACnC,QAAA,MAAA,GAAS,KAAA,CAAM,KAAA;AAAA,MACjB;AAAA,IACF,CAAA,CAAA,MAAQ;AAAA,IAER;AAAA,EACF;AAEA,EAAA,OAAO,MAAA;AACT;AAKA,eAAe,WAAW,WAAA,EAAuC;AAC/D,EAAA,IAAI;AACF,IAAA,MAAM,MAAA,CAAO,IAAA,CAAK,WAAA,EAAa,MAAM,CAAC,CAAA;AACtC,IAAA,OAAO,IAAA;AAAA,EACT,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,KAAA;AAAA,EACT;AACF;AAKA,eAAe,cAAA,CACb,WAAA,EACA,WAAA,EACA,KAAA,EAC8B;AAC9B,EAAA,MAAM,OAAO,WAAA,CAAY,IAAA;AAGzB,EAAA,MAAM,YAAA,GAAe,MAAM,UAAA,CAAW,WAAW,CAAA;AACjD,EAAA,IAAI,CAAC,YAAA,EAAc;AAEjB,IAAA,MAAM,IAAA,GAAO,KAAA,CAAM,KAAA,CAAM,GAAA,CAAI,IAAI,CAAA;AACjC,IAAA,MAAM,mBAAmB,IAAA,GAAO,KAAA,CAAM,KAAK,IAAA,CAAK,UAAU,IAAI,EAAC;AAE/D,IAAA,OAAO;AAAA,MACL,IAAA;AAAA,MACA,IAAA,EAAM,WAAA;AAAA,MACN,MAAA,EAAQ,cAAA;AAAA,MACR,gBAAA;AAAA,MACA,eAAe,gBAAA,CAAiB,MAAA;AAAA,MAChC,QAAA,EAAU,iBAAiB,MAAA,GAAS,EAAA,GAAK,aAAa,gBAAA,CAAiB,MAAA,GAAS,KAAK,MAAA,GAAS;AAAA,KAChG;AAAA,EACF;AAGA,EAAA,MAAM,QAAA,GAAW,MAAM,MAAA,CAAO,0BAAA,EAA4B;AAAA,IACxD,GAAA,EAAK,WAAA;AAAA,IACL,QAAA,EAAU,IAAA;AAAA,IACV,MAAA,EAAQ,CAAC,cAAA,EAAgB,cAAA,EAAgB,iBAAiB;AAAA,GAC3D,CAAA;AAGD,EAAA,MAAM,SAAA,GAAY,MAAM,MAAA,CAAO,6BAAA,EAA+B;AAAA,IAC5D,GAAA,EAAK,WAAA;AAAA,IACL,QAAA,EAAU;AAAA,GACX,CAAA;AAED,EAAA,MAAM,UAAA,GAAa,MAAM,gBAAA,CAAiB,QAAQ,CAAA;AAClD,EAAA,MAAM,WAAA,GAAc,MAAM,gBAAA,CAAiB,SAAS,CAAA;AAGpD,EAAA,IAAI,UAAA,IAAc,WAAA,IAAe,UAAA,GAAa,WAAA,EAAa;AACzD,IAAA,MAAM,IAAA,GAAO,KAAA,CAAM,KAAA,CAAM,GAAA,CAAI,IAAI,CAAA;AACjC,IAAA,MAAM,mBAAmB,IAAA,GAAO,KAAA,CAAM,KAAK,IAAA,CAAK,UAAU,IAAI,EAAC;AAE/D,IAAA,OAAO;AAAA,MACL,IAAA;AAAA,MACA,IAAA,EAAM,WAAA;AAAA,MACN,MAAA,EAAQ,cAAA;AAAA,MACR,cAAA,EAAgB,WAAW,WAAA,EAAY;AAAA,MACvC,YAAA,EAAc,YAAY,WAAA,EAAY;AAAA,MACtC,gBAAA;AAAA,MACA,eAAe,gBAAA,CAAiB,MAAA;AAAA,MAChC,QAAA,EAAU,gBAAA,CAAiB,MAAA,GAAS,EAAA,GAAK,UAAA,GAAa,gBAAA,CAAiB,MAAA,GAAS,EAAA,GAAK,MAAA,GAAS,gBAAA,CAAiB,MAAA,GAAS,CAAA,GAAI,QAAA,GAAW;AAAA,KACzI;AAAA,EACF;AAGA,EAAA,IAAI,WAAA,EAAa;AACf,IAAa,WAAA,CAAY,YAAA,IAAgB;AACzC,IAAA,MAAM,IAAA,GAAO,KAAA,CAAM,KAAA,CAAM,GAAA,CAAI,IAAI,CAAA;AAEjC,IAAA,IAAI,IAAA,EAAM;AACR,MAAA,KAAA,MAAW,OAAA,IAAW,KAAK,IAAA,EAAM;AAC/B,QAAA,MAAM,OAAA,GAAU,KAAA,CAAM,KAAA,CAAM,GAAA,CAAI,OAAO,CAAA;AACvC,QAAA,IAAI,OAAA,EAAS;AAEX,UAAA,MAAM,WAAA,GAAc,MAAM,MAAA,CAAO,iBAAA,EAAmB;AAAA,YAClD,GAAA,EAAK,IAAA,CAAK,WAAA,EAAa,OAAO,CAAA;AAAA;AAAA,YAC9B,QAAA,EAAU,IAAA;AAAA,YACV,MAAA,EAAQ,CAAC,oBAAoB;AAAA,WAC9B,CAAA;AAED,UAAA,KAAA,MAAW,cAAc,WAAA,EAAa;AACpC,YAAA,IAAI;AACF,cAAA,MAAM,SAAS,IAAA,CAAK,KAAA,CAAM,MAAM,QAAA,CAAS,UAAA,EAAY,OAAO,CAAC,CAAA;AAC7D,cAAA,IAAI,MAAA,CAAO,SAAS,OAAA,EAAS;AAC3B,gBAAA,MAAM,MAAA,GAAS,IAAA,CAAK,UAAA,EAAY,IAAI,CAAA;AACpC,gBAAA,MAAM,YAAA,GAAe,MAAM,MAAA,CAAO,wBAAA,EAA0B;AAAA,kBAC1D,GAAA,EAAK,MAAA;AAAA,kBACL,QAAA,EAAU;AAAA,iBACX,CAAA;AACD,gBAAA,MAAM,cAAA,GAAiB,MAAM,gBAAA,CAAiB,YAAY,CAAA;AAE1D,gBAAA,IAAI,cAAA,IAAkB,iBAAiB,WAAA,EAAa;AAClD,kBAAA,MAAM,mBAAmB,IAAA,GAAO,KAAA,CAAM,KAAK,IAAA,CAAK,UAAU,IAAI,EAAC;AAE/D,kBAAA,OAAO;AAAA,oBACL,IAAA;AAAA,oBACA,IAAA,EAAM,WAAA;AAAA,oBACN,MAAA,EAAQ,oBAAA;AAAA,oBACR,cAAA,EAAgB,YAAY,WAAA,EAAY;AAAA,oBACxC,YAAA,EAAc,YAAY,WAAA,EAAY;AAAA,oBACtC,gBAAA;AAAA,oBACA,eAAe,gBAAA,CAAiB,MAAA;AAAA,oBAChC,QAAA,EAAU,gBAAA,CAAiB,MAAA,GAAS,EAAA,GAAK,UAAA,GAAa,gBAAA,CAAiB,MAAA,GAAS,EAAA,GAAK,MAAA,GAAS,gBAAA,CAAiB,MAAA,GAAS,CAAA,GAAI,QAAA,GAAW;AAAA,mBACzI;AAAA,gBACF;AAAA,cACF;AAAA,YACF,CAAA,CAAA,MAAQ;AAAA,YAER;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,EAAA,OAAO,IAAA;AACT;AAKA,SAAS,YACP,KAAA,EACA,WAAA,EACA,OAAA,mBAAU,IAAI,KAAY,EAClB;AACR,EAAA,IAAI,OAAA,CAAQ,GAAA,CAAI,WAAW,CAAA,EAAG;AAAC,IAAA,OAAO,CAAA;AAAA,EAAE;AACxC,EAAA,OAAA,CAAQ,IAAI,WAAW,CAAA;AAEvB,EAAA,MAAM,IAAA,GAAO,KAAA,CAAM,KAAA,CAAM,GAAA,CAAI,WAAW,CAAA;AACxC,EAAA,IAAI,CAAC,IAAA,IAAQ,IAAA,CAAK,UAAA,CAAW,SAAS,CAAA,EAAG;AAAC,IAAA,OAAO,CAAA;AAAA,EAAE;AAEnD,EAAA,IAAI,QAAA,GAAW,CAAA;AACf,EAAA,KAAA,MAAW,SAAA,IAAa,KAAK,UAAA,EAAY;AACvC,IAAA,MAAM,KAAA,GAAQ,IAAI,WAAA,CAAY,KAAA,EAAO,WAAW,IAAI,GAAA,CAAI,OAAO,CAAC,CAAA;AAChE,IAAA,IAAI,QAAQ,QAAA,EAAU;AAAC,MAAA,QAAA,GAAW,KAAA;AAAA,IAAM;AAAA,EAC1C;AAEA,EAAA,OAAO,QAAA;AACT;AAKA,eAAsB,oBAAA,CACpB,OAAA,EACA,QAAA,GAAW,KAAA,EACa;AAExB,EAAA,MAAM,KAAA,GAAQ,qBAAqB,OAAO,CAAA;AAG1C,EAAA,MAAM,gBAAA,GAAmB,MAAM,MAAA,CAAO,iBAAA,EAAmB;AAAA,IACvD,GAAA,EAAK,OAAA;AAAA,IACL,QAAQ,CAAC,oBAAA,EAAsB,YAAA,EAAc,WAAA,EAAa,cAAc,cAAc,CAAA;AAAA,IACtF,QAAA,EAAU,IAAA;AAAA,IACV,IAAA,EAAM;AAAA,GACP,CAAA;AAGD,EAAA,MAAM,OAAA,GAAU,MAAM,OAAA,CAAQ,GAAA;AAAA,IAC5B,gBAAA,CAAiB,GAAA,CAAI,OAAO,OAAA,KAAY;AACtC,MAAA,IAAI;AACF,QAAA,MAAM,OAAA,GAAU,MAAM,QAAA,CAAS,OAAA,EAAS,OAAO,CAAA;AAC/C,QAAA,MAAM,GAAA,GAAM,IAAA,CAAK,KAAA,CAAM,OAAO,CAAA;AAC9B,QAAA,MAAM,WAAA,GAAc,IAAA,CAAK,OAAA,EAAS,IAAI,CAAA;AACtC,QAAA,OAAO,MAAM,cAAA,CAAe,WAAA,EAAa,GAAA,EAAK,KAAK,CAAA;AAAA,MACrD,CAAA,CAAA,MAAQ;AACN,QAAA,OAAO,IAAA;AAAA,MACT;AAAA,IACF,CAAC;AAAA,GACH;AAEA,EAAA,MAAM,gBAAgC,OAAA,CAAQ,MAAA,CAAO,CAAC,CAAA,KAAyB,MAAM,IAAI,CAAA;AAGzF,EAAA,MAAM,WAAA,uBAAkB,GAAA,EAAY;AACpC,EAAA,KAAA,MAAW,YAAY,aAAA,EAAe;AACpC,IAAA,KAAA,MAAW,QAAA,IAAY,SAAS,gBAAA,EAAkB;AAChD,MAAA,WAAA,CAAY,IAAI,QAAQ,CAAA;AAAA,IAC1B;AAAA,EACF;AAGA,EAAA,IAAI,iBAA+B,EAAC;AACpC,EAAA,IAAI,QAAA,EAAU;AACZ,IAAA,cAAA,GAAiB,aAAA,CACd,OAAO,CAAA,GAAA,KAAO,GAAA,CAAI,gBAAgB,CAAC,CAAA,CACnC,IAAI,CAAA,GAAA,MAAQ;AAAA,MACX,MAAM,GAAA,CAAI,IAAA;AAAA,MACV,UAAU,GAAA,CAAI,gBAAA;AAAA,MACd,KAAA,EAAO,WAAA,CAAY,KAAA,EAAO,GAAA,CAAI,IAAI;AAAA,KACpC,CAAE,CAAA,CACD,IAAA,CAAK,CAAC,GAAG,CAAA,KAAM,CAAA,CAAE,QAAA,CAAS,MAAA,GAAS,EAAE,QAAA,CAAS,MAAM,CAAA,CACpD,KAAA,CAAM,GAAG,EAAE,CAAA;AAAA,EAChB;AAEA,EAAA,OAAO;AAAA,IACL,aAAA,EAAe,cAAc,IAAA,CAAK,CAAC,GAAG,CAAA,KAAM,CAAA,CAAE,aAAA,GAAgB,CAAA,CAAE,aAAa,CAAA;AAAA,IAC7E,YAAY,aAAA,CAAc,MAAA;AAAA,IAC1B,eAAe,WAAA,CAAY,IAAA;AAAA,IAC3B;AAAA,GACF;AACF","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","/**\n * Stale packages detection\n * Finds packages that need rebuilding due to source changes or dependency updates\n */\n\nimport { readFile, stat, access } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport globby from 'globby';\nimport { buildDependencyGraph } from '../graph/index.js';\n\nexport interface StalePackage {\n name: string;\n path: string;\n reason: 'source-newer' | 'dependency-rebuilt' | 'missing-dist';\n sourceModified?: string;\n distModified?: string;\n affectedPackages: string[];\n affectedCount: number;\n severity: 'critical' | 'high' | 'medium' | 'low';\n}\n\nexport interface StaleChain {\n root: string;\n affected: string[];\n depth: number;\n}\n\nexport interface StaleAnalysis {\n stalePackages: StalePackage[];\n totalStale: number;\n totalAffected: number;\n criticalChains: StaleChain[];\n}\n\n/**\n * Get latest modification time from a list of files\n */\nasync function getLatestModTime(files: string[]): Promise<Date | null> {\n if (files.length === 0) {return null;}\n\n let latest: Date | null = null;\n\n for (const file of files) {\n try {\n const stats = await stat(file);\n if (!latest || stats.mtime > latest) {\n latest = stats.mtime;\n }\n } catch {\n // Skip files that can't be accessed\n }\n }\n\n return latest;\n}\n\n/**\n * Check if dist directory exists\n */\nasync function distExists(packagePath: string): Promise<boolean> {\n try {\n await access(join(packagePath, 'dist'));\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Analyze a single package for staleness\n */\nasync function analyzePackage(\n packagePath: string,\n packageJson: any,\n graph: ReturnType<typeof buildDependencyGraph>\n): Promise<StalePackage | null> {\n const name = packageJson.name;\n\n // Check if dist exists\n const hasDistCheck = await distExists(packagePath);\n if (!hasDistCheck) {\n // Get affected packages\n const node = graph.nodes.get(name);\n const affectedPackages = node ? Array.from(node.dependents) : [];\n\n return {\n name,\n path: packagePath,\n reason: 'missing-dist',\n affectedPackages,\n affectedCount: affectedPackages.length,\n severity: affectedPackages.length > 20 ? 'critical' : affectedPackages.length > 10 ? 'high' : 'medium',\n };\n }\n\n // Get source files\n const srcFiles = await globby('src/**/*.{ts,tsx,js,jsx}', {\n cwd: packagePath,\n absolute: true,\n ignore: ['**/*.test.ts', '**/*.spec.ts', '**/__tests__/**'],\n });\n\n // Get dist files\n const distFiles = await globby('dist/**/*.{js,mjs,cjs,d.ts}', {\n cwd: packagePath,\n absolute: true,\n });\n\n const srcModTime = await getLatestModTime(srcFiles);\n const distModTime = await getLatestModTime(distFiles);\n\n // Check if source is newer than dist\n if (srcModTime && distModTime && srcModTime > distModTime) {\n const node = graph.nodes.get(name);\n const affectedPackages = node ? Array.from(node.dependents) : [];\n\n return {\n name,\n path: packagePath,\n reason: 'source-newer',\n sourceModified: srcModTime.toISOString(),\n distModified: distModTime.toISOString(),\n affectedPackages,\n affectedCount: affectedPackages.length,\n severity: affectedPackages.length > 20 ? 'critical' : affectedPackages.length > 10 ? 'high' : affectedPackages.length > 5 ? 'medium' : 'low',\n };\n }\n\n // Check if dependencies were rebuilt after this package\n if (distModTime) {\n const deps = packageJson.dependencies || {};\n const node = graph.nodes.get(name);\n\n if (node) {\n for (const depName of node.deps) {\n const depNode = graph.nodes.get(depName);\n if (depNode) {\n // Find dep package.json to get its dist time\n const depPackages = await globby('**/package.json', {\n cwd: join(packagePath, '../..'), // Go up to monorepo root\n absolute: true,\n ignore: ['**/node_modules/**'],\n });\n\n for (const depPkgPath of depPackages) {\n try {\n const depPkg = JSON.parse(await readFile(depPkgPath, 'utf-8'));\n if (depPkg.name === depName) {\n const depDir = join(depPkgPath, '..');\n const depDistFiles = await globby('dist/**/*.{js,mjs,cjs}', {\n cwd: depDir,\n absolute: true,\n });\n const depDistModTime = await getLatestModTime(depDistFiles);\n\n if (depDistModTime && depDistModTime > distModTime) {\n const affectedPackages = node ? Array.from(node.dependents) : [];\n\n return {\n name,\n path: packagePath,\n reason: 'dependency-rebuilt',\n sourceModified: srcModTime?.toISOString(),\n distModified: distModTime.toISOString(),\n affectedPackages,\n affectedCount: affectedPackages.length,\n severity: affectedPackages.length > 20 ? 'critical' : affectedPackages.length > 10 ? 'high' : affectedPackages.length > 5 ? 'medium' : 'low',\n };\n }\n }\n } catch {\n // Skip invalid package.json\n }\n }\n }\n }\n }\n }\n\n return null;\n}\n\n/**\n * Calculate max depth from a package in dependency graph\n */\nfunction getMaxDepth(\n graph: ReturnType<typeof buildDependencyGraph>,\n packageName: string,\n visited = new Set<string>()\n): number {\n if (visited.has(packageName)) {return 0;}\n visited.add(packageName);\n\n const node = graph.nodes.get(packageName);\n if (!node || node.dependents.size === 0) {return 0;}\n\n let maxDepth = 0;\n for (const dependent of node.dependents) {\n const depth = 1 + getMaxDepth(graph, dependent, new Set(visited));\n if (depth > maxDepth) {maxDepth = depth;}\n }\n\n return maxDepth;\n}\n\n/**\n * Analyze all packages for staleness\n */\nexport async function analyzeStalePackages(\n rootDir: string,\n detailed = false\n): Promise<StaleAnalysis> {\n // Build dependency graph first\n const graph = buildDependencyGraph(rootDir);\n\n // Find all package.json files (depth=6 covers workspace/subrepo/packages/pkg/...)\n const packageJsonFiles = await globby('**/package.json', {\n cwd: rootDir,\n ignore: ['**/node_modules/**', '**/.git/**', '**/.kb/**', '**/dist/**', 'package.json'],\n absolute: true,\n deep: 6,\n });\n\n // Analyze packages in parallel\n const results = await Promise.all(\n packageJsonFiles.map(async (pkgPath) => {\n try {\n const content = await readFile(pkgPath, 'utf-8');\n const pkg = JSON.parse(content);\n const packagePath = join(pkgPath, '..');\n return await analyzePackage(packagePath, pkg, graph);\n } catch {\n return null;\n }\n })\n );\n\n const stalePackages: StalePackage[] = results.filter((r): r is StalePackage => r !== null);\n\n // Calculate total affected (unique packages)\n const allAffected = new Set<string>();\n for (const stalePkg of stalePackages) {\n for (const affected of stalePkg.affectedPackages) {\n allAffected.add(affected);\n }\n }\n\n // Find critical chains (if detailed)\n let criticalChains: StaleChain[] = [];\n if (detailed) {\n criticalChains = stalePackages\n .filter(pkg => pkg.affectedCount > 5)\n .map(pkg => ({\n root: pkg.name,\n affected: pkg.affectedPackages,\n depth: getMaxDepth(graph, pkg.name),\n }))\n .sort((a, b) => b.affected.length - a.affected.length)\n .slice(0, 10); // Top 10 critical chains\n }\n\n return {\n stalePackages: stalePackages.sort((a, b) => b.affectedCount - a.affectedCount),\n totalStale: stalePackages.length,\n totalAffected: allAffected.size,\n criticalChains,\n };\n}\n"]}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calculate monorepo statistics
|
|
3
|
+
*
|
|
4
|
+
* Atomic functions for counting packages, LOC, size, etc.
|
|
5
|
+
*/
|
|
6
|
+
interface MonorepoStats {
|
|
7
|
+
packages: number;
|
|
8
|
+
loc: number;
|
|
9
|
+
size: number;
|
|
10
|
+
sizeFormatted: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Calculate total lines of code in all source files
|
|
14
|
+
*/
|
|
15
|
+
declare function calculateLinesOfCode(rootDir: string, sourceFiles?: string[]): Promise<number>;
|
|
16
|
+
/**
|
|
17
|
+
* Calculate total size of source files in bytes
|
|
18
|
+
*/
|
|
19
|
+
declare function calculateSize(rootDir: string, sourceFiles?: string[]): Promise<number>;
|
|
20
|
+
/**
|
|
21
|
+
* Format bytes to human-readable string
|
|
22
|
+
*/
|
|
23
|
+
declare function formatBytes(bytes: number): string;
|
|
24
|
+
/**
|
|
25
|
+
* Count packages in monorepo
|
|
26
|
+
*/
|
|
27
|
+
declare function countPackages(rootDir: string): Promise<number>;
|
|
28
|
+
/**
|
|
29
|
+
* Calculate all stats at once
|
|
30
|
+
*/
|
|
31
|
+
declare function calculateStats(rootDir: string): Promise<MonorepoStats>;
|
|
32
|
+
|
|
33
|
+
export { type MonorepoStats, calculateLinesOfCode, calculateSize, calculateStats, countPackages, formatBytes };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { readFile, stat } from 'fs/promises';
|
|
2
|
+
import globby from 'globby';
|
|
3
|
+
|
|
4
|
+
// src/stats/calculate-stats.ts
|
|
5
|
+
async function calculateLinesOfCode(rootDir, sourceFiles) {
|
|
6
|
+
const files = sourceFiles ?? await globby("**/*.{ts,tsx,js,jsx}", {
|
|
7
|
+
cwd: rootDir,
|
|
8
|
+
ignore: [
|
|
9
|
+
"**/node_modules/**",
|
|
10
|
+
"**/dist/**",
|
|
11
|
+
"**/.git/**",
|
|
12
|
+
"**/.kb/**",
|
|
13
|
+
"**/build/**",
|
|
14
|
+
"**/*.test.{ts,tsx,js,jsx}",
|
|
15
|
+
"**/*.spec.{ts,tsx,js,jsx}"
|
|
16
|
+
],
|
|
17
|
+
absolute: true,
|
|
18
|
+
deep: 8
|
|
19
|
+
});
|
|
20
|
+
const contents = await Promise.all(files.map((f) => readFile(f, "utf-8").catch(() => null)));
|
|
21
|
+
return contents.reduce((sum, c) => sum + (c ? c.split("\n").length : 0), 0);
|
|
22
|
+
}
|
|
23
|
+
async function calculateSize(rootDir, sourceFiles) {
|
|
24
|
+
const files = sourceFiles ?? await globby("**/*.{ts,tsx,js,jsx}", {
|
|
25
|
+
cwd: rootDir,
|
|
26
|
+
ignore: ["**/node_modules/**", "**/dist/**", "**/.git/**", "**/.kb/**", "**/build/**"],
|
|
27
|
+
absolute: true,
|
|
28
|
+
deep: 8
|
|
29
|
+
});
|
|
30
|
+
const stats = await Promise.all(files.map((f) => stat(f).catch(() => null)));
|
|
31
|
+
return stats.reduce((sum, s) => sum + (s ? s.size : 0), 0);
|
|
32
|
+
}
|
|
33
|
+
function formatBytes(bytes) {
|
|
34
|
+
if (bytes === 0) {
|
|
35
|
+
return "0 B";
|
|
36
|
+
}
|
|
37
|
+
const units = ["B", "KB", "MB", "GB"];
|
|
38
|
+
const k = 1024;
|
|
39
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
40
|
+
const value = bytes / Math.pow(k, i);
|
|
41
|
+
return `${value.toFixed(2)} ${units[i]}`;
|
|
42
|
+
}
|
|
43
|
+
async function countPackages(rootDir) {
|
|
44
|
+
const packageJsonFiles = await globby("**/package.json", {
|
|
45
|
+
cwd: rootDir,
|
|
46
|
+
ignore: ["**/node_modules/**", "**/.git/**", "**/.kb/**"],
|
|
47
|
+
absolute: false,
|
|
48
|
+
deep: 6
|
|
49
|
+
});
|
|
50
|
+
return packageJsonFiles.filter((p) => p !== "package.json").length;
|
|
51
|
+
}
|
|
52
|
+
async function calculateStats(rootDir) {
|
|
53
|
+
const sourceFiles = await globby("**/*.{ts,tsx,js,jsx}", {
|
|
54
|
+
cwd: rootDir,
|
|
55
|
+
ignore: ["**/node_modules/**", "**/dist/**", "**/.git/**", "**/.kb/**", "**/build/**"],
|
|
56
|
+
absolute: true,
|
|
57
|
+
deep: 8
|
|
58
|
+
});
|
|
59
|
+
const [packages, loc, size] = await Promise.all([
|
|
60
|
+
countPackages(rootDir),
|
|
61
|
+
calculateLinesOfCode(rootDir, sourceFiles),
|
|
62
|
+
calculateSize(rootDir, sourceFiles)
|
|
63
|
+
]);
|
|
64
|
+
return {
|
|
65
|
+
packages,
|
|
66
|
+
loc,
|
|
67
|
+
size,
|
|
68
|
+
sizeFormatted: formatBytes(size)
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export { calculateLinesOfCode, calculateSize, calculateStats, countPackages, formatBytes };
|
|
73
|
+
//# sourceMappingURL=index.js.map
|
|
74
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/stats/calculate-stats.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Calculate monorepo statistics\n *\n * Atomic functions for counting packages, LOC, size, etc.\n */\n\nimport { readFile, stat } from 'node:fs/promises';\nimport globby from 'globby';\n\nexport interface MonorepoStats {\n packages: number;\n loc: number;\n size: number;\n sizeFormatted: string;\n}\n\n/**\n * Calculate total lines of code in all source files\n */\nexport async function calculateLinesOfCode(rootDir: string, sourceFiles?: string[]): Promise<number> {\n const files = sourceFiles ?? await globby('**/*.{ts,tsx,js,jsx}', {\n cwd: rootDir,\n ignore: ['**/node_modules/**', '**/dist/**', '**/.git/**', '**/.kb/**', '**/build/**',\n '**/*.test.{ts,tsx,js,jsx}', '**/*.spec.{ts,tsx,js,jsx}'],\n absolute: true,\n deep: 8,\n });\n\n const contents = await Promise.all(files.map(f => readFile(f, 'utf-8').catch(() => null)));\n return contents.reduce((sum, c) => sum + (c ? c.split('\\n').length : 0), 0);\n}\n\n/**\n * Calculate total size of source files in bytes\n */\nexport async function calculateSize(rootDir: string, sourceFiles?: string[]): Promise<number> {\n const files = sourceFiles ?? await globby('**/*.{ts,tsx,js,jsx}', {\n cwd: rootDir,\n ignore: ['**/node_modules/**', '**/dist/**', '**/.git/**', '**/.kb/**', '**/build/**'],\n absolute: true,\n deep: 8,\n });\n\n const stats = await Promise.all(files.map(f => stat(f).catch(() => null)));\n return stats.reduce((sum, s) => sum + (s ? s.size : 0), 0);\n}\n\n/**\n * Format bytes to human-readable string\n */\nexport function formatBytes(bytes: number): string {\n if (bytes === 0) {return '0 B';}\n\n const units = ['B', 'KB', 'MB', 'GB'];\n const k = 1024;\n const i = Math.floor(Math.log(bytes) / Math.log(k));\n const value = bytes / Math.pow(k, i);\n\n return `${value.toFixed(2)} ${units[i]}`;\n}\n\n/**\n * Count packages in monorepo\n */\nexport async function countPackages(rootDir: string): Promise<number> {\n const packageJsonFiles = await globby('**/package.json', {\n cwd: rootDir,\n ignore: ['**/node_modules/**', '**/.git/**', '**/.kb/**'],\n absolute: false,\n deep: 6,\n });\n\n // Exclude root package.json if it exists\n return packageJsonFiles.filter(p => p !== 'package.json').length;\n}\n\n/**\n * Calculate all stats at once\n */\nexport async function calculateStats(rootDir: string): Promise<MonorepoStats> {\n // Single scan for source files shared by LOC and size\n const sourceFiles = await globby('**/*.{ts,tsx,js,jsx}', {\n cwd: rootDir,\n ignore: ['**/node_modules/**', '**/dist/**', '**/.git/**', '**/.kb/**', '**/build/**'],\n absolute: true,\n deep: 8,\n });\n\n const [packages, loc, size] = await Promise.all([\n countPackages(rootDir),\n calculateLinesOfCode(rootDir, sourceFiles),\n calculateSize(rootDir, sourceFiles),\n ]);\n\n return {\n packages,\n loc,\n size,\n sizeFormatted: formatBytes(size),\n };\n}\n"]}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test execution and coverage tracking
|
|
3
|
+
*
|
|
4
|
+
* Runs tests across monorepo packages and collects coverage statistics
|
|
5
|
+
*/
|
|
6
|
+
interface TestRunOptions {
|
|
7
|
+
packageFilter?: string;
|
|
8
|
+
timeout?: number;
|
|
9
|
+
withCoverage?: boolean;
|
|
10
|
+
coverageOnly?: boolean;
|
|
11
|
+
concurrency?: number;
|
|
12
|
+
}
|
|
13
|
+
interface TestRunResult {
|
|
14
|
+
totalPackages: number;
|
|
15
|
+
passing: number;
|
|
16
|
+
failing: number;
|
|
17
|
+
skipped: number;
|
|
18
|
+
failures: Array<{
|
|
19
|
+
package: string;
|
|
20
|
+
error: string;
|
|
21
|
+
exitCode: number;
|
|
22
|
+
failedTests?: number;
|
|
23
|
+
totalTests?: number;
|
|
24
|
+
}>;
|
|
25
|
+
summary: {
|
|
26
|
+
totalTests: number;
|
|
27
|
+
passedTests: number;
|
|
28
|
+
failedTests: number;
|
|
29
|
+
};
|
|
30
|
+
coverage: {
|
|
31
|
+
avgCoverage: number;
|
|
32
|
+
packages: Array<{
|
|
33
|
+
name: string;
|
|
34
|
+
lines: number;
|
|
35
|
+
statements: number;
|
|
36
|
+
functions: number;
|
|
37
|
+
branches: number;
|
|
38
|
+
}>;
|
|
39
|
+
};
|
|
40
|
+
duration: number;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Run tests across monorepo
|
|
44
|
+
*/
|
|
45
|
+
declare function runTests(rootDir: string, options?: TestRunOptions): Promise<TestRunResult>;
|
|
46
|
+
|
|
47
|
+
export { type TestRunOptions, type TestRunResult, runTests };
|