@titan-design/code-graph 0.11.0 → 0.13.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.
@@ -0,0 +1,1395 @@
1
+ import {
2
+ couplingFor
3
+ } from "./chunk-HIVVDGWE.js";
4
+
5
+ // src/extractors/symbol-id.ts
6
+ var SYMBOL_ID_SEP = "#";
7
+ function symbolId(fileId, qualifiedName) {
8
+ return `${fileId}${SYMBOL_ID_SEP}${qualifiedName}`;
9
+ }
10
+ function parseSymbolId(id) {
11
+ const idx = id.indexOf(SYMBOL_ID_SEP);
12
+ if (idx < 0) return null;
13
+ return { fileId: id.slice(0, idx), name: id.slice(idx + 1) };
14
+ }
15
+
16
+ // src/analysis/test-linker.ts
17
+ var DEFAULT_MIN_CO_EDIT_COUNT = 2;
18
+ var TEST_INFIX_RE = /\.(?:test|spec)(\.[^./]+)$/;
19
+ var TESTS_DIR_RE = /(^|\/)(?:__tests__|tests?)\//;
20
+ function pathCandidates(testId) {
21
+ const out = /* @__PURE__ */ new Set();
22
+ const deInfixed = testId.replace(TEST_INFIX_RE, "$1");
23
+ if (deInfixed !== testId) out.add(deInfixed);
24
+ const deDir = deInfixed.replace(TESTS_DIR_RE, "$1");
25
+ if (deDir !== testId) out.add(deDir);
26
+ return [...out];
27
+ }
28
+ function splitFileIds(nodes) {
29
+ const testIds = [];
30
+ const nonTestIds = /* @__PURE__ */ new Set();
31
+ for (const n of nodes) {
32
+ if (n.kind !== "file") continue;
33
+ if (n.role === "test") testIds.push(n.id);
34
+ else nonTestIds.add(n.id);
35
+ }
36
+ return { testIds, nonTestIds };
37
+ }
38
+ function linkTestsToSources(nodes, coEditPairs, options = {}) {
39
+ const minCount = options.minCoEditCount ?? DEFAULT_MIN_CO_EDIT_COUNT;
40
+ const { testIds, nonTestIds } = splitFileIds(nodes);
41
+ const links = [];
42
+ for (const testId of testIds) {
43
+ const matched = pathCandidates(testId).filter((c) => nonTestIds.has(c));
44
+ if (matched.length > 0) {
45
+ for (const sourceId of matched) {
46
+ links.push({ testId, sourceId, method: "path" });
47
+ }
48
+ continue;
49
+ }
50
+ const partner = couplingFor(coEditPairs, testId).find((p) => p.count >= minCount && nonTestIds.has(p.partner));
51
+ if (partner) {
52
+ links.push({ testId, sourceId: partner.partner, method: "coedit" });
53
+ }
54
+ }
55
+ return links;
56
+ }
57
+ function testCoverageCountMetrics(links) {
58
+ const testsBySource = groupTestsBySource(links);
59
+ const out = [];
60
+ for (const [sourceId, tests] of testsBySource) {
61
+ out.push({
62
+ nodeId: sourceId,
63
+ name: "linked_test_count",
64
+ value: tests.size,
65
+ unit: "count"
66
+ });
67
+ }
68
+ return out;
69
+ }
70
+ function groupTestsBySource(links) {
71
+ const out = /* @__PURE__ */ new Map();
72
+ for (const l of links) {
73
+ let set = out.get(l.sourceId);
74
+ if (!set) {
75
+ set = /* @__PURE__ */ new Set();
76
+ out.set(l.sourceId, set);
77
+ }
78
+ set.add(l.testId);
79
+ }
80
+ return out;
81
+ }
82
+
83
+ // src/history-recency.ts
84
+ var DAY_SECONDS = 86400;
85
+ function windowSuffix(window) {
86
+ return window === "lifetime" ? "lifetime" : `${window}d`;
87
+ }
88
+ function computeRecencyWindows(firstSeen, churnedIdsByWindow, nowEpoch) {
89
+ const out = [];
90
+ const ageEmitted = /* @__PURE__ */ new Set();
91
+ for (const [windowDays, ids] of churnedIdsByWindow) {
92
+ const suffix = windowSuffix(windowDays);
93
+ for (const id of ids) {
94
+ const seen = firstSeen.get(id);
95
+ let recency = 1;
96
+ if (seen !== void 0) {
97
+ const ageDays = Math.max(0, (nowEpoch - seen) / DAY_SECONDS);
98
+ if (windowDays !== "lifetime") recency = Math.min(1, ageDays / windowDays);
99
+ if (!ageEmitted.has(id)) {
100
+ out.push({ nodeId: id, name: "file_age_days", value: Math.round(ageDays), unit: "days" });
101
+ ageEmitted.add(id);
102
+ }
103
+ }
104
+ out.push({ nodeId: id, name: `recency_${suffix}`, value: round3(recency), unit: "ratio" });
105
+ }
106
+ }
107
+ return out;
108
+ }
109
+ function round3(n) {
110
+ return Math.round(n * 1e3) / 1e3;
111
+ }
112
+
113
+ // src/unimported-roles.ts
114
+ var UNIMPORTED_ROLES = /* @__PURE__ */ new Set([
115
+ "test",
116
+ "fixture",
117
+ "story",
118
+ "lab",
119
+ "config",
120
+ "script",
121
+ "entry"
122
+ ]);
123
+
124
+ // src/analysis/pagerank.ts
125
+ var DEFAULT_DAMPING = 0.85;
126
+ var DEFAULT_TOLERANCE = 1e-6;
127
+ var DEFAULT_MAX_ITERS = 100;
128
+ var DEFAULT_EDGE_WEIGHTS = {
129
+ imports: 1,
130
+ "re-exports": 0.5,
131
+ calls: 1.5,
132
+ extends: 1,
133
+ implements: 1,
134
+ references: 1,
135
+ "depends-on": 1
136
+ };
137
+ function getEdgeWeight(kind, overrides) {
138
+ return overrides?.[kind] ?? DEFAULT_EDGE_WEIGHTS[kind] ?? 1;
139
+ }
140
+ function computePageRank(nodes, edges, options = {}) {
141
+ const n = nodes.length;
142
+ if (n === 0) return { rows: [], iterations: 0, converged: true };
143
+ const damping = options.damping ?? DEFAULT_DAMPING;
144
+ const tolerance = options.tolerance ?? DEFAULT_TOLERANCE;
145
+ const maxIters = options.maxIterations ?? DEFAULT_MAX_ITERS;
146
+ const weights = { ...DEFAULT_EDGE_WEIGHTS, ...options.edgeWeights ?? {} };
147
+ const idx = buildIdIndex(nodes);
148
+ const adj = buildAdjacency(idx, edges, weights, n);
149
+ const pers = buildPersonalization(idx, n, options.personalization);
150
+ const { rank, iterations, converged } = powerIterate(
151
+ pers,
152
+ adj,
153
+ damping,
154
+ tolerance,
155
+ maxIters
156
+ );
157
+ return { rows: rankToRows(nodes, rank), iterations, converged };
158
+ }
159
+ function buildIdIndex(nodes) {
160
+ const idx = /* @__PURE__ */ new Map();
161
+ for (let i = 0; i < nodes.length; i++) idx.set(nodes[i].id, i);
162
+ return idx;
163
+ }
164
+ function buildAdjacency(idx, edges, weights, n) {
165
+ const outNeighbors = Array.from(
166
+ { length: n },
167
+ () => []
168
+ );
169
+ const outWeightSum = new Array(n).fill(0);
170
+ for (const e of edges) {
171
+ const si = idx.get(e.srcId);
172
+ const di = idx.get(e.dstId);
173
+ if (si === void 0 || di === void 0) continue;
174
+ const w = weights[e.kind] ?? 1;
175
+ if (w <= 0) continue;
176
+ outNeighbors[si].push({ to: di, w });
177
+ outWeightSum[si] += w;
178
+ }
179
+ return { outNeighbors, outWeightSum };
180
+ }
181
+ function powerIterate(pers, adj, damping, tolerance, maxIters) {
182
+ const n = pers.length;
183
+ let rank = pers.slice();
184
+ let next = new Array(n).fill(0);
185
+ let iterations = 0;
186
+ let converged = false;
187
+ for (let iter = 0; iter < maxIters; iter++) {
188
+ iterations = iter + 1;
189
+ const dangling = sumDangling(rank, adj.outWeightSum);
190
+ const teleport = 1 - damping + damping * dangling;
191
+ seedTeleport(next, pers, teleport);
192
+ distributeRank(next, rank, adj, damping);
193
+ const diff = l1Diff(rank, next);
194
+ [rank, next] = [next, rank];
195
+ next.fill(0);
196
+ if (diff < tolerance) {
197
+ converged = true;
198
+ break;
199
+ }
200
+ }
201
+ return { rank, iterations, converged };
202
+ }
203
+ function sumDangling(rank, outWeightSum) {
204
+ let total = 0;
205
+ for (let i = 0; i < rank.length; i++) {
206
+ if (outWeightSum[i] === 0) total += rank[i];
207
+ }
208
+ return total;
209
+ }
210
+ function seedTeleport(next, pers, teleport) {
211
+ for (let i = 0; i < next.length; i++) next[i] = teleport * pers[i];
212
+ }
213
+ function distributeRank(next, rank, adj, damping) {
214
+ for (let i = 0; i < rank.length; i++) {
215
+ const s = adj.outWeightSum[i];
216
+ if (s === 0 || rank[i] === 0) continue;
217
+ const factor = damping * rank[i] / s;
218
+ const list = adj.outNeighbors[i];
219
+ for (let k = 0; k < list.length; k++) {
220
+ next[list[k].to] += factor * list[k].w;
221
+ }
222
+ }
223
+ }
224
+ function l1Diff(a, b) {
225
+ let diff = 0;
226
+ for (let i = 0; i < a.length; i++) diff += Math.abs(b[i] - a[i]);
227
+ return diff;
228
+ }
229
+ function rankToRows(nodes, rank) {
230
+ return nodes.map((node, i) => ({ nodeId: node.id, score: rank[i] })).sort(compareRows);
231
+ }
232
+ function compareRows(a, b) {
233
+ if (b.score !== a.score) return b.score - a.score;
234
+ return a.nodeId < b.nodeId ? -1 : a.nodeId > b.nodeId ? 1 : 0;
235
+ }
236
+ function buildPersonalization(idx, n, pers) {
237
+ if (!pers || pers.size === 0) return uniformVector(n);
238
+ const out = new Array(n).fill(0);
239
+ const total = applySeedWeights(out, idx, pers);
240
+ if (total === 0) return uniformVector(n);
241
+ for (let i = 0; i < n; i++) out[i] /= total;
242
+ return out;
243
+ }
244
+ function uniformVector(n) {
245
+ return new Array(n).fill(1 / n);
246
+ }
247
+ function applySeedWeights(out, idx, pers) {
248
+ let total = 0;
249
+ for (const [id, w] of pers) {
250
+ if (!isValidWeight(w)) continue;
251
+ const i = idx.get(id);
252
+ if (i === void 0) continue;
253
+ out[i] = w;
254
+ total += w;
255
+ }
256
+ return total;
257
+ }
258
+ function isValidWeight(w) {
259
+ return Number.isFinite(w) && w > 0;
260
+ }
261
+
262
+ // src/analysis/dashboard-node-metrics.ts
263
+ var METRIC_FIELD = {
264
+ loc: "loc",
265
+ cognitive_max: "cognitiveMax",
266
+ cyclomatic_max: "cyclomaticMax",
267
+ max_nesting_depth: "maxNesting",
268
+ fan_in: "fanIn",
269
+ fan_out: "fanOut",
270
+ utilization: "utilization",
271
+ linked_test_count: "linkedTests",
272
+ // Per-symbol complexity (C-58) lands on `symbol` nodes; map onto the same
273
+ // fields so a symbol node reads its OWN complexity (file nodes never carry the
274
+ // `symbol_*` names, so there's no collision on a shared field).
275
+ symbol_cognitive: "cognitiveMax",
276
+ symbol_cyclomatic: "cyclomaticMax"
277
+ };
278
+ function collectNodeMetrics(rows) {
279
+ const byNode = /* @__PURE__ */ new Map();
280
+ for (const m of rows) {
281
+ const field = METRIC_FIELD[m.name];
282
+ if (!field || m.value === null) continue;
283
+ const entry = byNode.get(m.nodeId) ?? {};
284
+ entry[field] = m.value;
285
+ byNode.set(m.nodeId, entry);
286
+ }
287
+ return byNode;
288
+ }
289
+ function referencedNodes(report) {
290
+ const referenced = /* @__PURE__ */ new Set();
291
+ for (const h of report.hotspots) referenced.add(h.nodeId);
292
+ for (const b of report.busFactorRisks) referenced.add(b.nodeId);
293
+ for (const t of report.testCoverageRisks) referenced.add(t.nodeId);
294
+ for (const c of report.couplingClusters) {
295
+ referenced.add(c.fileA);
296
+ referenced.add(c.fileB);
297
+ }
298
+ for (const c of report.centralFiles) referenced.add(c.nodeId);
299
+ const drift = report.drift;
300
+ if (drift) {
301
+ for (const h of drift.newHotspots) referenced.add(h.nodeId);
302
+ for (const d of drift.worsenedHotspots) referenced.add(d.nodeId);
303
+ for (const c of drift.newCoupling) {
304
+ referenced.add(c.fileA);
305
+ referenced.add(c.fileB);
306
+ }
307
+ }
308
+ return referenced;
309
+ }
310
+ function buildNodeMetrics(report, metrics) {
311
+ const out = {};
312
+ for (const id of referencedNodes(report)) {
313
+ const m = metrics.get(id);
314
+ if (m) out[id] = m;
315
+ }
316
+ return out;
317
+ }
318
+ function buildCentralFiles(report, centrality) {
319
+ const byId = /* @__PURE__ */ new Map();
320
+ for (const c of report.centralFiles) byId.set(c.nodeId, c.score);
321
+ const referenced = /* @__PURE__ */ new Set();
322
+ for (const h of report.hotspots) referenced.add(h.nodeId);
323
+ for (const b of report.busFactorRisks) referenced.add(b.nodeId);
324
+ for (const c of report.couplingClusters) {
325
+ referenced.add(c.fileA);
326
+ referenced.add(c.fileB);
327
+ }
328
+ for (const id of referenced) if (!byId.has(id)) byId.set(id, centrality.get(id) ?? 0);
329
+ return [...byId].map(([nodeId, score]) => ({ nodeId, score })).sort((a, b) => b.score - a.score);
330
+ }
331
+ function collectSymbolUtil(nodes, metrics) {
332
+ const out = [];
333
+ for (const n of nodes) {
334
+ if (n.kind !== "symbol" || !n.parentId) continue;
335
+ out.push({
336
+ symbolId: n.id,
337
+ name: n.name,
338
+ fileId: n.parentId,
339
+ utilization: metrics.get(n.id)?.utilization ?? 0,
340
+ exported: n.attrs?.exported !== false
341
+ });
342
+ }
343
+ return out;
344
+ }
345
+ function buildHotExports(symbols, referenced, metrics, consumersBySymbol) {
346
+ const byFile = /* @__PURE__ */ new Map();
347
+ for (const s of symbols) {
348
+ if (!referenced.has(s.fileId)) continue;
349
+ const bucket = byFile.get(s.fileId) ?? [];
350
+ bucket.push({
351
+ name: s.name,
352
+ utilization: s.utilization,
353
+ cognitive: metrics.get(s.symbolId)?.cognitiveMax,
354
+ consumers: consumersBySymbol.get(s.symbolId) ?? 0,
355
+ exported: s.exported
356
+ });
357
+ byFile.set(s.fileId, bucket);
358
+ }
359
+ const out = {};
360
+ for (const [fileId, syms] of byFile) {
361
+ const exports = syms.filter((s) => s.exported).sort((a, b) => b.utilization - a.utilization || (b.cognitive ?? 0) - (a.cognitive ?? 0)).slice(0, 8);
362
+ const internal = syms.filter((s) => !s.exported).sort((a, b) => (b.cognitive ?? 0) - (a.cognitive ?? 0) || a.name.localeCompare(b.name)).slice(0, 8);
363
+ out[fileId] = [...exports, ...internal];
364
+ }
365
+ return out;
366
+ }
367
+ function buildBlastRadius(symbols, metrics, churnByFile, limit = 15) {
368
+ const out = [];
369
+ for (const s of symbols) {
370
+ const complexity = metrics.get(s.symbolId)?.cognitiveMax ?? metrics.get(s.fileId)?.cognitiveMax ?? 0;
371
+ const churn = churnByFile.get(s.fileId) ?? 0;
372
+ const score = s.utilization * complexity * churn;
373
+ if (score <= 0) continue;
374
+ out.push({ symbolId: s.symbolId, name: s.name, fileId: s.fileId, utilization: s.utilization, complexity, churn, score });
375
+ }
376
+ return out.sort((a, b) => b.score - a.score).slice(0, limit);
377
+ }
378
+
379
+ // src/analysis/symbol-coupling.ts
380
+ var DEFAULT_MIN_CO_IMPORTS = 2;
381
+ var DEFAULT_LARGE_IMPORTER_THRESHOLD = 40;
382
+ function computeSymbolConsumers(edges) {
383
+ const bySymbol = /* @__PURE__ */ new Map();
384
+ for (const e of edges) {
385
+ let set = bySymbol.get(e.dstId);
386
+ if (!set) {
387
+ set = /* @__PURE__ */ new Set();
388
+ bySymbol.set(e.dstId, set);
389
+ }
390
+ set.add(e.srcId);
391
+ }
392
+ const out = [];
393
+ for (const [id, consumers] of bySymbol) {
394
+ const parsed = parseSymbolId(id);
395
+ if (!parsed) continue;
396
+ out.push({
397
+ symbolId: id,
398
+ fileId: parsed.fileId,
399
+ name: parsed.name,
400
+ consumers: [...consumers].sort()
401
+ });
402
+ }
403
+ return out.sort(compareConsumers);
404
+ }
405
+ function compareConsumers(a, b) {
406
+ if (b.consumers.length !== a.consumers.length) {
407
+ return b.consumers.length - a.consumers.length;
408
+ }
409
+ return a.symbolId < b.symbolId ? -1 : a.symbolId > b.symbolId ? 1 : 0;
410
+ }
411
+ function computeSymbolCoupling(edges, options = {}) {
412
+ const minCoImports = options.minCoImports ?? DEFAULT_MIN_CO_IMPORTS;
413
+ const largeThreshold = options.largeImporterThreshold ?? DEFAULT_LARGE_IMPORTER_THRESHOLD;
414
+ const bySource = groupSymbolsBySource(edges);
415
+ const counts = /* @__PURE__ */ new Map();
416
+ for (const symbols of bySource.values()) {
417
+ if (symbols.length < 2 || symbols.length > largeThreshold) continue;
418
+ accumulateSymbolPairs(symbols, counts);
419
+ }
420
+ return finalizePairs(counts, minCoImports);
421
+ }
422
+ function groupSymbolsBySource(edges) {
423
+ const perSource = /* @__PURE__ */ new Map();
424
+ for (const e of edges) {
425
+ if (!parseSymbolId(e.dstId)) continue;
426
+ let set = perSource.get(e.srcId);
427
+ if (!set) {
428
+ set = /* @__PURE__ */ new Set();
429
+ perSource.set(e.srcId, set);
430
+ }
431
+ set.add(e.dstId);
432
+ }
433
+ const out = /* @__PURE__ */ new Map();
434
+ for (const [src, set] of perSource) out.set(src, [...set].sort());
435
+ return out;
436
+ }
437
+ function accumulateSymbolPairs(symbols, counts) {
438
+ for (let i = 0; i < symbols.length; i++) {
439
+ for (let j = i + 1; j < symbols.length; j++) {
440
+ const key = `${symbols[i]} ${symbols[j]}`;
441
+ counts.set(key, (counts.get(key) ?? 0) + 1);
442
+ }
443
+ }
444
+ }
445
+ function finalizePairs(counts, minCoImports) {
446
+ const out = [];
447
+ for (const [key, coImports] of counts) {
448
+ if (coImports < minCoImports) continue;
449
+ const [aId, bId] = key.split(" ");
450
+ const a = parseSymbolId(aId);
451
+ const b = parseSymbolId(bId);
452
+ out.push({
453
+ aId,
454
+ aFile: a.fileId,
455
+ aName: a.name,
456
+ bId,
457
+ bFile: b.fileId,
458
+ bName: b.name,
459
+ coImports,
460
+ crossFile: a.fileId !== b.fileId
461
+ });
462
+ }
463
+ return out.sort(comparePairs);
464
+ }
465
+ function comparePairs(a, b) {
466
+ if (b.coImports !== a.coImports) return b.coImports - a.coImports;
467
+ if (a.crossFile !== b.crossFile) return a.crossFile ? -1 : 1;
468
+ if (a.aId !== b.aId) return a.aId < b.aId ? -1 : 1;
469
+ return a.bId < b.bId ? -1 : a.bId > b.bId ? 1 : 0;
470
+ }
471
+
472
+ // src/analysis/partition-quality.ts
473
+ var FOUNDATION_INSTABILITY_MAX = 0.3;
474
+ var TOP_INSTABILITY_MIN = 0.9;
475
+ var WEAK_COHESION_MAX = 0.5;
476
+ var TIGHT_INTENSITY_MIN = 0.6;
477
+ var MODERATE_INTENSITY_MIN = 0.3;
478
+ function computePartitionQuality(input) {
479
+ const resolveBarrels = input.resolveBarrels ?? false;
480
+ const pkgByFile = invertBuckets(input.fileByPackage);
481
+ const barrelTargets = resolveBarrels ? buildBarrelTargetMap(input.nodes, input.edges) : /* @__PURE__ */ new Map();
482
+ const resolvedPairs = buildResolvedPairs(
483
+ input.edges,
484
+ pkgByFile,
485
+ barrelTargets,
486
+ resolveBarrels
487
+ );
488
+ const counts = countByPackage(resolvedPairs);
489
+ const totalEdges = resolvedPairs.length;
490
+ const filesByPkg = filesPerPackage(input.fileByPackage);
491
+ const abstractByPkg = countAbstractFiles(input.nodes, pkgByFile);
492
+ const perPackage = input.packages.map(
493
+ (p) => buildPackageStats(p.id, counts, filesByPkg, abstractByPkg)
494
+ );
495
+ const pairCoupling = buildPairCoupling(resolvedPairs, filesByPkg);
496
+ const modularityQ = computeModularity(perPackage, totalEdges);
497
+ const flagsCount = perPackage.reduce((acc, p) => acc + p.flags.length, 0) + pairCoupling.filter((c) => c.flag === "tight").length;
498
+ return { modularityQ, totalEdges, perPackage, pairCoupling, flagsCount };
499
+ }
500
+ function invertBuckets(fileByPackage) {
501
+ const out = /* @__PURE__ */ new Map();
502
+ for (const [pkgId, files] of fileByPackage) {
503
+ if (pkgId === "") continue;
504
+ for (const f of files) out.set(f, pkgId);
505
+ }
506
+ return out;
507
+ }
508
+ function filesPerPackage(fileByPackage) {
509
+ const out = /* @__PURE__ */ new Map();
510
+ for (const [pkgId, files] of fileByPackage) {
511
+ out.set(pkgId, files.length);
512
+ }
513
+ return out;
514
+ }
515
+ function buildBarrelTargetMap(nodes, edges) {
516
+ const barrelIds = new Set(
517
+ nodes.filter((n) => n.role === "barrel").map((n) => n.id)
518
+ );
519
+ if (barrelIds.size === 0) return /* @__PURE__ */ new Map();
520
+ const directReExports = /* @__PURE__ */ new Map();
521
+ for (const e of edges) {
522
+ if (!barrelIds.has(e.srcId)) continue;
523
+ if (e.kind !== "re-exports") continue;
524
+ let list = directReExports.get(e.srcId);
525
+ if (!list) {
526
+ list = [];
527
+ directReExports.set(e.srcId, list);
528
+ }
529
+ list.push(e.dstId);
530
+ }
531
+ const resolved = /* @__PURE__ */ new Map();
532
+ const resolve = (barrel, visiting) => {
533
+ if (resolved.has(barrel)) return resolved.get(barrel);
534
+ if (visiting.has(barrel)) return [];
535
+ visiting.add(barrel);
536
+ const out = [];
537
+ for (const target of directReExports.get(barrel) ?? []) {
538
+ if (barrelIds.has(target)) {
539
+ for (const inner of resolve(target, visiting)) out.push(inner);
540
+ } else {
541
+ out.push(target);
542
+ }
543
+ }
544
+ visiting.delete(barrel);
545
+ resolved.set(barrel, out);
546
+ return out;
547
+ };
548
+ for (const id of barrelIds) resolve(id, /* @__PURE__ */ new Set());
549
+ return resolved;
550
+ }
551
+ function buildResolvedPairs(edges, pkgByFile, barrelTargets, resolveBarrels) {
552
+ const out = [];
553
+ for (const e of edges) {
554
+ if (resolveBarrels && e.kind === "re-exports") continue;
555
+ const fromPkg = pkgByFile.get(e.srcId);
556
+ if (!fromPkg) continue;
557
+ const targets = barrelTargets.get(e.dstId) ?? [e.dstId];
558
+ for (const dst of targets) {
559
+ const toPkg = pkgByFile.get(dst);
560
+ if (toPkg) out.push({ from: fromPkg, to: toPkg });
561
+ }
562
+ }
563
+ return out;
564
+ }
565
+ function countByPackage(pairs) {
566
+ const out = /* @__PURE__ */ new Map();
567
+ const get = (id) => {
568
+ let c = out.get(id);
569
+ if (!c) {
570
+ c = { internal: 0, outgoing: 0, incoming: 0 };
571
+ out.set(id, c);
572
+ }
573
+ return c;
574
+ };
575
+ for (const p of pairs) {
576
+ if (p.from === p.to) {
577
+ get(p.from).internal += 1;
578
+ } else {
579
+ get(p.from).outgoing += 1;
580
+ get(p.to).incoming += 1;
581
+ }
582
+ }
583
+ return out;
584
+ }
585
+ function buildPackageStats(pkgId, counts, filesByPkg, abstractByPkg) {
586
+ const c = counts.get(pkgId) ?? { internal: 0, outgoing: 0, incoming: 0 };
587
+ const cohesion = ratioOrZero(c.internal, c.internal + c.outgoing);
588
+ const instability = ratioOrZero(c.outgoing, c.outgoing + c.incoming);
589
+ const fileCount = filesByPkg.get(pkgId) ?? 0;
590
+ const abstractness = ratioOrZero(abstractByPkg.get(pkgId) ?? 0, fileCount);
591
+ const layer = classifyLayer(instability);
592
+ const flags = [];
593
+ if (layer !== "top" && cohesion < WEAK_COHESION_MAX && c.internal + c.outgoing > 0) {
594
+ flags.push("weak-boundary");
595
+ }
596
+ return {
597
+ pkgId,
598
+ fileCount,
599
+ internalEdges: c.internal,
600
+ outgoingEdges: c.outgoing,
601
+ incomingEdges: c.incoming,
602
+ cohesion,
603
+ instability,
604
+ abstractness,
605
+ layer,
606
+ flags
607
+ };
608
+ }
609
+ function countAbstractFiles(nodes, pkgByFile) {
610
+ const out = /* @__PURE__ */ new Map();
611
+ for (const n of nodes) {
612
+ if (n.kind !== "file" || n.role !== "types") continue;
613
+ const pkg = pkgByFile.get(n.id);
614
+ if (pkg) out.set(pkg, (out.get(pkg) ?? 0) + 1);
615
+ }
616
+ return out;
617
+ }
618
+ function ratioOrZero(num, denom) {
619
+ return denom === 0 ? 0 : num / denom;
620
+ }
621
+ function classifyLayer(instability) {
622
+ if (instability <= FOUNDATION_INSTABILITY_MAX) return "foundation";
623
+ if (instability >= TOP_INSTABILITY_MIN) return "top";
624
+ return "middle";
625
+ }
626
+ function buildPairCoupling(pairs, filesByPkg) {
627
+ const counts = /* @__PURE__ */ new Map();
628
+ for (const p of pairs) {
629
+ if (p.from === p.to) continue;
630
+ const key = JSON.stringify([p.from, p.to]);
631
+ const entry = counts.get(key);
632
+ if (entry) entry.edges += 1;
633
+ else counts.set(key, { from: p.from, to: p.to, edges: 1 });
634
+ }
635
+ const out = [];
636
+ for (const { from, to, edges } of counts.values()) {
637
+ const filesFrom = filesByPkg.get(from) ?? 0;
638
+ const intensity = filesFrom === 0 ? 0 : edges / filesFrom;
639
+ const flag = classifyPairIntensity(intensity);
640
+ out.push({ from, to, edges, intensity, flag });
641
+ }
642
+ return out.sort(comparePairs2);
643
+ }
644
+ function classifyPairIntensity(intensity) {
645
+ if (intensity >= TIGHT_INTENSITY_MIN) return "tight";
646
+ if (intensity >= MODERATE_INTENSITY_MIN) return "moderate";
647
+ return "none";
648
+ }
649
+ function comparePairs2(a, b) {
650
+ if (a.from !== b.from) return a.from < b.from ? -1 : 1;
651
+ return a.to < b.to ? -1 : 1;
652
+ }
653
+ function computeModularity(perPackage, totalEdges) {
654
+ if (totalEdges === 0) return 0;
655
+ let Q = 0;
656
+ for (const p of perPackage) {
657
+ const e_cc = p.internalEdges / totalEdges;
658
+ const touching = p.internalEdges * 2 + p.outgoingEdges + p.incomingEdges;
659
+ const a_c = touching / (2 * totalEdges);
660
+ Q += e_cc - a_c * a_c;
661
+ }
662
+ return Q;
663
+ }
664
+
665
+ // src/check/patterns.ts
666
+ function patternToRegex(pattern) {
667
+ if (!pattern.includes("*")) {
668
+ return new RegExp(escapeRegex(pattern));
669
+ }
670
+ let out = "";
671
+ for (let i = 0; i < pattern.length; i++) {
672
+ const c = pattern[i];
673
+ if (c === "*" && pattern[i + 1] === "*") {
674
+ out += ".*";
675
+ i++;
676
+ } else if (c === "*") {
677
+ out += "[^/]*";
678
+ } else {
679
+ out += escapeRegex(c);
680
+ }
681
+ }
682
+ return new RegExp(`^${out}$`);
683
+ }
684
+ function compilePatterns(patterns) {
685
+ if (!patterns) return [];
686
+ return patterns.map(patternToRegex);
687
+ }
688
+ function matchesAny(value, patterns) {
689
+ for (const rx of patterns) {
690
+ if (rx.test(value)) return true;
691
+ }
692
+ return false;
693
+ }
694
+ function escapeRegex(s) {
695
+ return s.replace(/[.+?(){}|^$\\[\]]/g, "\\$&");
696
+ }
697
+
698
+ // src/analysis/graph-report-sections.ts
699
+ var COMPLEXITY_METRICS = ["cognitive_max", "cyclomatic_max"];
700
+ function buildReportContext(input) {
701
+ const metricsByName = /* @__PURE__ */ new Map();
702
+ for (const m of input.metrics) {
703
+ if (m.value === null) continue;
704
+ let bucket = metricsByName.get(m.name);
705
+ if (!bucket) {
706
+ bucket = /* @__PURE__ */ new Map();
707
+ metricsByName.set(m.name, bucket);
708
+ }
709
+ bucket.set(m.nodeId, m.value);
710
+ }
711
+ return {
712
+ nodes: input.nodes,
713
+ nodeById: new Map(input.nodes.map((n) => [n.id, n])),
714
+ metricsByName,
715
+ excluders: input.excluders,
716
+ excludedRoles: input.excludedRoles,
717
+ windowDays: input.windowDays,
718
+ windowSuffix: windowSuffix(input.windowDays)
719
+ };
720
+ }
721
+ function keepNode(ctx, nodeId) {
722
+ if (matchesAny(nodeId, ctx.excluders)) return false;
723
+ const node = ctx.nodeById.get(nodeId);
724
+ if (!node || node.kind !== "file") return false;
725
+ if (node.role === "generated") return false;
726
+ if (node.role && ctx.excludedRoles.has(node.role)) return false;
727
+ return true;
728
+ }
729
+ function lookupMetric(ctx, name, nodeId) {
730
+ return ctx.metricsByName.get(name)?.get(nodeId);
731
+ }
732
+ function pickComplexityMetric(ctx) {
733
+ for (const m of COMPLEXITY_METRICS) {
734
+ if (ctx.metricsByName.has(m)) return m;
735
+ }
736
+ return "cyclomatic_max";
737
+ }
738
+ function topHotspots(ctx, limit) {
739
+ const churnName = `churn_${ctx.windowSuffix}`;
740
+ const complexityName = pickComplexityMetric(ctx);
741
+ const rows = [];
742
+ for (const node of ctx.nodes) {
743
+ if (!keepNode(ctx, node.id)) continue;
744
+ const churn = lookupMetric(ctx, churnName, node.id) ?? 0;
745
+ const complexity = lookupMetric(ctx, complexityName, node.id) ?? 0;
746
+ if (churn === 0 || complexity === 0) continue;
747
+ const recency = lookupMetric(ctx, `recency_${ctx.windowSuffix}`, node.id) ?? 1;
748
+ rows.push({ nodeId: node.id, churn, complexity, recency, score: Math.round(churn * complexity * recency) });
749
+ }
750
+ rows.sort((a, b) => b.score - a.score);
751
+ return rows.slice(0, limit);
752
+ }
753
+ function hotspotScoreOf(ctx, nodeId) {
754
+ if (!keepNode(ctx, nodeId)) return 0;
755
+ const churn = lookupMetric(ctx, `churn_${ctx.windowSuffix}`, nodeId) ?? 0;
756
+ const complexity = lookupMetric(ctx, pickComplexityMetric(ctx), nodeId) ?? 0;
757
+ if (churn === 0 || complexity === 0) return 0;
758
+ return hotspotScore(ctx, nodeId, churn, complexity);
759
+ }
760
+ function hotspotComplexityOf(ctx, nodeId) {
761
+ return lookupMetric(ctx, pickComplexityMetric(ctx), nodeId);
762
+ }
763
+ function hotspotScore(ctx, nodeId, churn, complexity) {
764
+ const recency = lookupMetric(ctx, `recency_${ctx.windowSuffix}`, nodeId) ?? 1;
765
+ return Math.round(churn * complexity * recency);
766
+ }
767
+ function busFactorOf(ctx, nodeId) {
768
+ return lookupMetric(ctx, `bus_factor_${ctx.windowSuffix}`, nodeId);
769
+ }
770
+ function topBusFactorRisks(ctx, limit) {
771
+ const churnName = `churn_${ctx.windowSuffix}`;
772
+ const bfName = `bus_factor_${ctx.windowSuffix}`;
773
+ const shareName = `top_author_share_${ctx.windowSuffix}`;
774
+ const rows = [];
775
+ for (const node of ctx.nodes) {
776
+ if (!keepNode(ctx, node.id)) continue;
777
+ const bf = lookupMetric(ctx, bfName, node.id);
778
+ if (bf === void 0 || bf > 1) continue;
779
+ rows.push({
780
+ nodeId: node.id,
781
+ busFactor: bf,
782
+ topAuthorShare: lookupMetric(ctx, shareName, node.id) ?? 1,
783
+ churn: lookupMetric(ctx, churnName, node.id) ?? 0
784
+ });
785
+ }
786
+ rows.sort((a, b) => b.churn - a.churn);
787
+ return rows.slice(0, limit);
788
+ }
789
+ function topTestCoverageRisks(ctx, limit) {
790
+ const bfName = `test_bus_factor_${ctx.windowSuffix}`;
791
+ const shareName = `test_top_author_share_${ctx.windowSuffix}`;
792
+ const rows = [];
793
+ for (const node of ctx.nodes) {
794
+ if (!keepNode(ctx, node.id)) continue;
795
+ const bf = lookupMetric(ctx, bfName, node.id);
796
+ if (bf === void 0 || bf > 1) continue;
797
+ rows.push({
798
+ nodeId: node.id,
799
+ testBusFactor: bf,
800
+ testTopAuthorShare: lookupMetric(ctx, shareName, node.id) ?? 1,
801
+ linkedTests: lookupMetric(ctx, "linked_test_count", node.id) ?? 0
802
+ });
803
+ }
804
+ rows.sort((a, b) => b.linkedTests - a.linkedTests);
805
+ return rows.slice(0, limit);
806
+ }
807
+ function topCentralFiles(nodes, edges, ctx, limit) {
808
+ const pageRank = computePageRank(nodes, edges, {});
809
+ const rows = [];
810
+ for (const r of pageRank.rows) {
811
+ if (!keepNode(ctx, r.nodeId)) continue;
812
+ rows.push({ nodeId: r.nodeId, score: r.score });
813
+ if (rows.length >= limit) break;
814
+ }
815
+ return rows;
816
+ }
817
+
818
+ // src/analysis/unused-exports.ts
819
+ function publicApiFiles(nodes, edges) {
820
+ const barrels = new Set(
821
+ nodes.filter((n) => n.role === "barrel").map((n) => n.id)
822
+ );
823
+ const out = /* @__PURE__ */ new Set();
824
+ for (const e of edges) {
825
+ if (e.kind === "re-exports" && barrels.has(e.srcId)) out.add(e.dstId);
826
+ }
827
+ return out;
828
+ }
829
+ function topUnusedExports(symbolNodes, publicApi, ctx, limit) {
830
+ const rows = [];
831
+ for (const n of symbolNodes) {
832
+ if (n.kind !== "symbol" || n.attrs?.exported !== true) continue;
833
+ if ((lookupMetric(ctx, "utilization", n.id) ?? 0) > 0) continue;
834
+ const fileId = n.parentId;
835
+ if (!fileId || !keepNode(ctx, fileId)) continue;
836
+ rows.push({
837
+ nodeId: n.id,
838
+ name: n.name,
839
+ fileId,
840
+ cognitive: lookupMetric(ctx, "symbol_cognitive", n.id) ?? 0,
841
+ publicApi: publicApi.has(fileId)
842
+ });
843
+ }
844
+ rows.sort(
845
+ (a, b) => Number(a.publicApi) - Number(b.publicApi) || b.cognitive - a.cognitive || a.nodeId.localeCompare(b.nodeId)
846
+ );
847
+ return rows.slice(0, limit);
848
+ }
849
+
850
+ // src/analysis/dead-modules.ts
851
+ var ENTRY_ROOT_ROLES = /* @__PURE__ */ new Set([...UNIMPORTED_ROLES, "barrel"]);
852
+ var ENTRY_FILE_RE = /(?:^|\/)main\.[jt]sx?$/;
853
+ function isEntryRoot(node) {
854
+ return node.kind === "file" && (node.role !== void 0 && ENTRY_ROOT_ROLES.has(node.role) || ENTRY_FILE_RE.test(node.id));
855
+ }
856
+ function topDeadModules(nodes, edges, ctx, limit) {
857
+ const reached = reachableFromEntryRoots(nodes, edges);
858
+ const rows = [];
859
+ for (const n of nodes) {
860
+ if (n.kind !== "file" || reached.has(n.id) || isEntryRoot(n)) continue;
861
+ if (!keepNode(ctx, n.id)) continue;
862
+ rows.push({ nodeId: n.id, loc: lookupMetric(ctx, "loc", n.id) ?? 0, role: n.role ?? "source" });
863
+ }
864
+ rows.sort((a, b) => b.loc - a.loc || a.nodeId.localeCompare(b.nodeId));
865
+ return rows.slice(0, limit);
866
+ }
867
+ function outgoingModuleEdges(edges) {
868
+ const out = /* @__PURE__ */ new Map();
869
+ for (const e of edges) {
870
+ if (e.kind !== "imports" && e.kind !== "re-exports") continue;
871
+ const bucket = out.get(e.srcId);
872
+ if (bucket) bucket.push(e.dstId);
873
+ else out.set(e.srcId, [e.dstId]);
874
+ }
875
+ return out;
876
+ }
877
+ function reachableFromEntryRoots(nodes, edges) {
878
+ const out = outgoingModuleEdges(edges);
879
+ const reached = /* @__PURE__ */ new Set();
880
+ const queue = [];
881
+ for (const n of nodes) {
882
+ if (isEntryRoot(n)) {
883
+ reached.add(n.id);
884
+ queue.push(n.id);
885
+ }
886
+ }
887
+ for (let i = 0; i < queue.length; i++) {
888
+ for (const dst of out.get(queue[i]) ?? []) {
889
+ if (reached.has(dst)) continue;
890
+ reached.add(dst);
891
+ queue.push(dst);
892
+ }
893
+ }
894
+ return reached;
895
+ }
896
+
897
+ // src/analysis/growth-risks.ts
898
+ var LOOP_SHAPES = { 2: "quadratic-shaped", 3: "cubic-shaped" };
899
+ function loopShape(depth) {
900
+ return LOOP_SHAPES[depth] ?? `${depth}-deep loop nesting`;
901
+ }
902
+ function fileSmells(ctx, nodeId) {
903
+ const smells = [];
904
+ const depth = lookupMetric(ctx, "loop_depth", nodeId) ?? 0;
905
+ if (depth >= 2) smells.push(`${loopShape(depth)} loop nesting`);
906
+ const rec = lookupMetric(ctx, "recursive_functions", nodeId) ?? 0;
907
+ if (rec > 0) smells.push(`${rec} recursive function${rec === 1 ? "" : "s"}`);
908
+ const search = lookupMetric(ctx, "search_in_loop", nodeId) ?? 0;
909
+ if (search > 0) smells.push(`${search} linear search${search === 1 ? "" : "es"} in loops`);
910
+ return smells;
911
+ }
912
+ function topGrowthRisks(ctx, limit) {
913
+ const rows = [];
914
+ for (const node of ctx.nodes) {
915
+ if (!keepNode(ctx, node.id)) continue;
916
+ const smells = fileSmells(ctx, node.id);
917
+ if (smells.length === 0) continue;
918
+ rows.push({
919
+ nodeId: node.id,
920
+ loopDepth: lookupMetric(ctx, "loop_depth", node.id) ?? 0,
921
+ smells
922
+ });
923
+ }
924
+ rows.sort(
925
+ (a, b) => b.loopDepth - a.loopDepth || b.smells.length - a.smells.length || a.nodeId.localeCompare(b.nodeId)
926
+ );
927
+ return rows.slice(0, limit);
928
+ }
929
+
930
+ // src/analysis/untested-risks.ts
931
+ function topUntestedRisks(ctx, limit) {
932
+ const rows = [];
933
+ for (const node of ctx.nodes) {
934
+ if (!keepNode(ctx, node.id)) continue;
935
+ const coverage = lookupMetric(ctx, "coverage_pct", node.id);
936
+ if (coverage === void 0 || coverage >= 100) continue;
937
+ const hotspot = hotspotScoreOf(ctx, node.id);
938
+ const score = Math.round(hotspot * (1 - coverage / 100));
939
+ if (score <= 0) continue;
940
+ rows.push({ nodeId: node.id, coverage, hotspot, score });
941
+ }
942
+ rows.sort((a, b) => b.score - a.score || a.nodeId.localeCompare(b.nodeId));
943
+ return rows.slice(0, limit);
944
+ }
945
+
946
+ // src/analysis/graph-report-drift.ts
947
+ function computeReportDrift(input) {
948
+ const hot = diffHotspots(
949
+ input.currentHotspots,
950
+ input.baselineHotspots,
951
+ input.currentHotspotScore,
952
+ input.baselineHotspotScore ?? (() => void 0)
953
+ );
954
+ const silos = diffSilos(
955
+ input.currentSilos,
956
+ input.baselineSilos,
957
+ input.currentBusFactor
958
+ );
959
+ const coupling = diffCoupling(input.currentCoupling, input.baselineCoupling);
960
+ return {
961
+ baselineSnapshot: input.baselineSnapshot,
962
+ newHotspots: hot.added,
963
+ resolvedHotspots: hot.resolved,
964
+ displacedHotspots: hot.displaced,
965
+ worsenedHotspots: hot.worsened,
966
+ improvedHotspots: hot.improved,
967
+ newSilos: silos.added,
968
+ resolvedSilos: silos.resolved,
969
+ displacedSilos: silos.displaced,
970
+ newCoupling: coupling.added,
971
+ intensifiedCoupling: coupling.intensified
972
+ };
973
+ }
974
+ function diffHotspots(cur, base, currentScore, baselineScore) {
975
+ const baseById = new Map(base.map((r) => [r.nodeId, r]));
976
+ const curById = new Map(cur.map((r) => [r.nodeId, r]));
977
+ const added = [];
978
+ const resolved = [];
979
+ const displaced = [];
980
+ const worsened = [];
981
+ const improved = [];
982
+ for (const r of cur) {
983
+ const b = baseById.get(r.nodeId);
984
+ if (!b) {
985
+ added.push({ ...r, before: baselineScore(r.nodeId) });
986
+ continue;
987
+ }
988
+ const delta = r.score - b.score;
989
+ if (delta > 0) worsened.push({ nodeId: r.nodeId, before: b.score, after: r.score, delta });
990
+ else if (delta < 0) improved.push({ nodeId: r.nodeId, before: b.score, after: r.score, delta });
991
+ }
992
+ for (const b of base) {
993
+ if (curById.has(b.nodeId)) continue;
994
+ const after = currentScore(b.nodeId);
995
+ const delta = after - b.score;
996
+ const row = { nodeId: b.nodeId, before: b.score, after, delta };
997
+ if (after < b.score) resolved.push(row);
998
+ else displaced.push(row);
999
+ }
1000
+ worsened.sort((a, b) => b.delta - a.delta);
1001
+ improved.sort((a, b) => a.delta - b.delta);
1002
+ resolved.sort((a, b) => a.delta - b.delta);
1003
+ displaced.sort((a, b) => b.before - a.before);
1004
+ return { added, resolved, displaced, worsened, improved };
1005
+ }
1006
+ function diffSilos(cur, base, currentBusFactor) {
1007
+ const baseIds = new Set(base.map((r) => r.nodeId));
1008
+ const curIds = new Set(cur.map((r) => r.nodeId));
1009
+ const added = [];
1010
+ const resolved = [];
1011
+ const displaced = [];
1012
+ for (const r of cur) {
1013
+ if (!baseIds.has(r.nodeId)) added.push({ nodeId: r.nodeId, churn: r.churn });
1014
+ }
1015
+ for (const r of base) {
1016
+ if (curIds.has(r.nodeId)) continue;
1017
+ const bf = currentBusFactor(r.nodeId);
1018
+ if (bf === void 0 || bf > 1) {
1019
+ resolved.push({ nodeId: r.nodeId, churn: r.churn });
1020
+ } else {
1021
+ displaced.push({ nodeId: r.nodeId, churn: r.churn });
1022
+ }
1023
+ }
1024
+ added.sort((a, b) => b.churn - a.churn);
1025
+ resolved.sort((a, b) => b.churn - a.churn);
1026
+ displaced.sort((a, b) => b.churn - a.churn);
1027
+ return { added, resolved, displaced };
1028
+ }
1029
+ function diffCoupling(cur, base) {
1030
+ const key = (r) => `${r.fileA} ${r.fileB}`;
1031
+ const baseByKey = new Map(base.map((r) => [key(r), r]));
1032
+ const added = [];
1033
+ const intensified = [];
1034
+ for (const r of cur) {
1035
+ const b = baseByKey.get(key(r));
1036
+ if (!b) added.push(r);
1037
+ else if (r.count > b.count) {
1038
+ intensified.push({
1039
+ fileA: r.fileA,
1040
+ fileB: r.fileB,
1041
+ before: b.count,
1042
+ after: r.count
1043
+ });
1044
+ }
1045
+ }
1046
+ intensified.sort((a, b) => b.after - b.before - (a.after - a.before));
1047
+ return { added, intensified };
1048
+ }
1049
+
1050
+ // src/analysis/dashboard-health.ts
1051
+ var DEFAULT_HEALTH_WEIGHTS = {
1052
+ hotspots: { each: 10, cap: 30 },
1053
+ findings: { eachNew: 8, eachCarry: 3, cap: 20 },
1054
+ complexity: { each: 1, budget: 30, cap: 15 },
1055
+ hiddenCoupling: { each: 2, cap: 10 }
1056
+ };
1057
+ function healthComponents(x, w) {
1058
+ const over = Math.max(0, x.maxComplexity - w.complexity.budget);
1059
+ return [
1060
+ { key: "hotspots", label: "scary hotspots", penalty: Math.min(w.hotspots.cap, x.scary * w.hotspots.each), cap: w.hotspots.cap, detail: `${x.scary} file(s) \u2265 ${x.scaryCutoff ?? 3e3}` },
1061
+ {
1062
+ key: "findings",
1063
+ label: "fitness violations",
1064
+ penalty: Math.min(w.findings.cap, x.newViolations * w.findings.eachNew + x.carryViolations * w.findings.eachCarry),
1065
+ cap: w.findings.cap,
1066
+ detail: `${x.newViolations} new, ${x.carryViolations} parked (non-hotspot rules)`
1067
+ },
1068
+ { key: "complexity", label: "complexity over budget", penalty: Math.min(w.complexity.cap, over * w.complexity.each), cap: w.complexity.cap, detail: `max ${x.maxComplexity} vs budget ${w.complexity.budget}` },
1069
+ { key: "hidden-coupling", label: "hidden coupling", penalty: Math.min(w.hiddenCoupling.cap, x.hiddenCoupling * w.hiddenCoupling.each), cap: w.hiddenCoupling.cap, detail: `${x.hiddenCoupling} pair(s) co-change without an import` }
1070
+ ];
1071
+ }
1072
+ function computeHealth(x, weights = DEFAULT_HEALTH_WEIGHTS) {
1073
+ const breakdown = healthComponents(x, weights);
1074
+ const total = breakdown.reduce((s, c) => s + c.penalty, 0);
1075
+ return { health: Math.max(0, 100 - total), healthBreakdown: breakdown };
1076
+ }
1077
+
1078
+ // src/analysis/dashboard-coupling.ts
1079
+ function classifyCoupling(a, b, ctx) {
1080
+ const unindexed = !ctx.connectedNodes.has(a) || !ctx.connectedNodes.has(b);
1081
+ if (unindexed) return { hidden: false, unindexed: true };
1082
+ return { hidden: !ctx.linkedPairs.has(pairKey(a, b)), unindexed: false };
1083
+ }
1084
+ function pairKey(a, b) {
1085
+ return a < b ? JSON.stringify([a, b]) : JSON.stringify([b, a]);
1086
+ }
1087
+
1088
+ // src/analysis/package-buckets.ts
1089
+ function bucketFilesByPackage(fileIds, packages) {
1090
+ const sorted = [...packages].sort((a, b) => b.id.length - a.id.length);
1091
+ const out = /* @__PURE__ */ new Map();
1092
+ for (const id of fileIds) {
1093
+ const pkg = matchPackage(id, sorted);
1094
+ const key = pkg?.id ?? "";
1095
+ let list = out.get(key);
1096
+ if (!list) {
1097
+ list = [];
1098
+ out.set(key, list);
1099
+ }
1100
+ list.push(id);
1101
+ }
1102
+ return out;
1103
+ }
1104
+ function matchPackage(fileId, packagesByLongestId) {
1105
+ for (const p of packagesByLongestId) {
1106
+ if (p.id === "") continue;
1107
+ if (fileId === p.id) return p;
1108
+ if (fileId.startsWith(`${p.id}/`)) return p;
1109
+ }
1110
+ return null;
1111
+ }
1112
+
1113
+ // src/analysis/graph-arch-compute.ts
1114
+ var EXTERNAL_BUCKET = "(external)";
1115
+ var DEFAULT_MAX_PACKAGE_SIZE = 30;
1116
+ function computeArch(input) {
1117
+ const fileIds = filteredFileIds(input.nodes, input);
1118
+ const fileByPackage = bucketFilesByPackage(fileIds, input.packages);
1119
+ const drill = planDrill(input, fileByPackage);
1120
+ const nodeByFile = buildNodeByFile(fileByPackage, drill);
1121
+ const pkgByFile = invertBuckets(fileByPackage);
1122
+ const externalIds = new Set(
1123
+ input.nodes.filter((n) => n.kind === "external").map((n) => n.id)
1124
+ );
1125
+ const counts = aggregateEdges(
1126
+ input.edges,
1127
+ pkgByFile,
1128
+ nodeByFile,
1129
+ externalIds,
1130
+ Boolean(input.includeExternal)
1131
+ );
1132
+ const minEdges = Math.max(1, input.minEdges ?? 1);
1133
+ return {
1134
+ snapshot: input.snapshot,
1135
+ packages: activePackages(
1136
+ input.packages,
1137
+ fileByPackage,
1138
+ Boolean(input.includeExternal),
1139
+ counts,
1140
+ drill
1141
+ ),
1142
+ edges: toSortedEdges(counts, minEdges),
1143
+ includesExternal: Boolean(input.includeExternal)
1144
+ };
1145
+ }
1146
+ var DEFAULT_GRAPH_EXCLUDED_ROLES = ["test", "fixture"];
1147
+ function filteredFileIds(nodes, options) {
1148
+ const excluders = compilePatterns(options.exclude);
1149
+ const excludedRoles = /* @__PURE__ */ new Set([
1150
+ ...DEFAULT_GRAPH_EXCLUDED_ROLES,
1151
+ ...options.excludeRole ?? []
1152
+ ]);
1153
+ return nodes.filter((n) => n.kind === "file").filter((n) => !excludedRoles.has(n.role ?? "")).filter((n) => !matchesAny(n.id, excluders)).map((n) => n.id);
1154
+ }
1155
+ function aggregateEdges(edges, pkgByFile, nodeByFile, externalIds, includeExternal) {
1156
+ const counts = /* @__PURE__ */ new Map();
1157
+ for (const e of edges) {
1158
+ const fromPkg = pkgByFile.get(e.srcId);
1159
+ if (!fromPkg) continue;
1160
+ const dest = resolveDestination(
1161
+ e.dstId,
1162
+ pkgByFile,
1163
+ nodeByFile,
1164
+ externalIds,
1165
+ includeExternal
1166
+ );
1167
+ if (!dest || dest.pkg === fromPkg) continue;
1168
+ bump(counts, nodeByFile.get(e.srcId) ?? fromPkg, dest.node);
1169
+ }
1170
+ return counts;
1171
+ }
1172
+ function resolveDestination(dstId, pkgByFile, nodeByFile, externalIds, includeExternal) {
1173
+ const pkg = pkgByFile.get(dstId);
1174
+ if (pkg !== void 0) return { pkg, node: nodeByFile.get(dstId) ?? pkg };
1175
+ if (externalIds.has(dstId) && includeExternal) {
1176
+ return { pkg: EXTERNAL_BUCKET, node: EXTERNAL_BUCKET };
1177
+ }
1178
+ return null;
1179
+ }
1180
+ function bump(counts, from, to) {
1181
+ let row = counts.get(from);
1182
+ if (!row) {
1183
+ row = /* @__PURE__ */ new Map();
1184
+ counts.set(from, row);
1185
+ }
1186
+ row.set(to, (row.get(to) ?? 0) + 1);
1187
+ }
1188
+ function toSortedEdges(counts, minEdges) {
1189
+ const out = [];
1190
+ for (const [from, row] of counts) {
1191
+ for (const [to, count] of row) {
1192
+ if (count < minEdges) continue;
1193
+ out.push({ from, to, count });
1194
+ }
1195
+ }
1196
+ out.sort(compareArchEdges);
1197
+ return out;
1198
+ }
1199
+ function compareArchEdges(a, b) {
1200
+ if (a.from !== b.from) return a.from < b.from ? -1 : 1;
1201
+ return a.to < b.to ? -1 : 1;
1202
+ }
1203
+ function activePackages(all, fileByPackage, includeExternal, counts, drill) {
1204
+ const referenced = packagesReferencedByEdges(counts);
1205
+ const out = [];
1206
+ for (const p of all) {
1207
+ const files = fileByPackage.get(p.id)?.length ?? 0;
1208
+ if (files === 0 && !referenced.has(p.id)) continue;
1209
+ const plan = drill.get(p.id);
1210
+ out.push(
1211
+ plan ? { id: p.id, name: p.name, files, subNodes: plan.subNodes } : { id: p.id, name: p.name, files }
1212
+ );
1213
+ }
1214
+ if (includeExternal && referenced.has(EXTERNAL_BUCKET)) {
1215
+ out.push({ id: EXTERNAL_BUCKET, name: EXTERNAL_BUCKET, files: 0 });
1216
+ }
1217
+ return out;
1218
+ }
1219
+ function packagesReferencedByEdges(counts) {
1220
+ const referenced = /* @__PURE__ */ new Set();
1221
+ for (const [from, row] of counts) {
1222
+ referenced.add(from);
1223
+ for (const to of row.keys()) referenced.add(to);
1224
+ }
1225
+ return referenced;
1226
+ }
1227
+ function planDrill(input, fileByPackage) {
1228
+ const enabled = input.depth === "modules" || input.maxPackageSize !== void 0;
1229
+ if (!enabled) return /* @__PURE__ */ new Map();
1230
+ const threshold = Math.max(1, input.maxPackageSize ?? DEFAULT_MAX_PACKAGE_SIZE);
1231
+ const out = /* @__PURE__ */ new Map();
1232
+ for (const [pkgId, files] of fileByPackage) {
1233
+ if (pkgId === "" || files.length <= threshold) continue;
1234
+ out.set(pkgId, buildDrillPlan(pkgId, files));
1235
+ }
1236
+ return out;
1237
+ }
1238
+ function buildNodeByFile(fileByPackage, drill) {
1239
+ const out = /* @__PURE__ */ new Map();
1240
+ for (const [pkgId, files] of fileByPackage) {
1241
+ if (pkgId === "") continue;
1242
+ const plan = drill.get(pkgId);
1243
+ for (const f of files) out.set(f, plan?.fileToSub.get(f) ?? pkgId);
1244
+ }
1245
+ return out;
1246
+ }
1247
+ function buildDrillPlan(pkgId, files) {
1248
+ const rels = files.map((f) => stripPkgPrefix(f, pkgId));
1249
+ const commonRoot = drillRoot(rels.map(dirSegments));
1250
+ const fileToSub = /* @__PURE__ */ new Map();
1251
+ const groups = /* @__PURE__ */ new Map();
1252
+ files.forEach((file, i) => {
1253
+ const { id, label } = subNodeFor(pkgId, rels[i], commonRoot);
1254
+ fileToSub.set(file, id);
1255
+ const g = groups.get(id);
1256
+ if (g) g.count += 1;
1257
+ else groups.set(id, { label, count: 1 });
1258
+ });
1259
+ const subNodes = [...groups.entries()].map(([id, g]) => ({ id, label: g.label, files: g.count })).sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
1260
+ return { subNodes, fileToSub };
1261
+ }
1262
+ function subNodeFor(pkgId, rel, commonRoot) {
1263
+ const segment = dirSegments(rel).slice(commonRoot.length)[0] ?? "";
1264
+ if (segment !== "") {
1265
+ return { id: `${pkgId}/${[...commonRoot, segment].join("/")}`, label: segment };
1266
+ }
1267
+ const id = commonRoot.length ? `${pkgId}/${commonRoot.join("/")}` : `${pkgId}/(root)`;
1268
+ return { id, label: "(root)" };
1269
+ }
1270
+ function stripPkgPrefix(fileId, pkgId) {
1271
+ if (fileId === pkgId) return "";
1272
+ return fileId.startsWith(`${pkgId}/`) ? fileId.slice(pkgId.length + 1) : fileId;
1273
+ }
1274
+ function dirSegments(rel) {
1275
+ const i = rel.lastIndexOf("/");
1276
+ return i < 0 ? [] : rel.slice(0, i).split("/");
1277
+ }
1278
+ function drillRoot(dirs) {
1279
+ const root = [];
1280
+ for (; ; ) {
1281
+ const deeper = dirs.filter((d) => d.length > root.length);
1282
+ if (deeper.length === 0) break;
1283
+ const seg = deeper[0][root.length];
1284
+ const singleChild = deeper.every((d) => d[root.length] === seg);
1285
+ if (!singleChild || countGroups(dirs, root.length + 1) < 2) break;
1286
+ root.push(seg);
1287
+ }
1288
+ return root;
1289
+ }
1290
+ function countGroups(dirs, rootLen) {
1291
+ const groups = /* @__PURE__ */ new Set();
1292
+ for (const d of dirs) groups.add(d.length <= rootLen ? "(root)" : d[rootLen]);
1293
+ return groups.size;
1294
+ }
1295
+
1296
+ // src/check/violation-key.ts
1297
+ function violationKey(v) {
1298
+ return v.destinationId ? `${v.ruleId}|${v.nodeId}|${v.destinationId}` : `${v.ruleId}|${v.nodeId}`;
1299
+ }
1300
+ function rebasedViolationKey(v, resolve) {
1301
+ const destinationId = v.destinationId ? resolve(v.destinationId) : v.destinationId;
1302
+ return violationKey({ ...v, nodeId: resolve(v.nodeId), destinationId });
1303
+ }
1304
+
1305
+ // src/diff/violation-buckets.ts
1306
+ function bucketViolations(from, to, resolve) {
1307
+ const fromByKey = indexByKey(from, resolve);
1308
+ const toByKey = indexByKey(to);
1309
+ const buckets = { newViolations: [], resolvedViolations: [], unchanged: [], worsened: [], improved: [] };
1310
+ for (const [key, toV] of toByKey) {
1311
+ const fromV = fromByKey.get(key);
1312
+ if (!fromV) {
1313
+ buckets.newViolations.push(toV);
1314
+ continue;
1315
+ }
1316
+ const entry = { from: fromV, to: toV, delta: computeDelta(fromV, toV) };
1317
+ buckets.unchanged.push(entry);
1318
+ if (entry.delta !== null && entry.delta > 0) buckets.worsened.push(entry);
1319
+ else if (entry.delta !== null && entry.delta < 0) buckets.improved.push(entry);
1320
+ }
1321
+ for (const [key, fromV] of fromByKey) {
1322
+ if (!toByKey.has(key)) buckets.resolvedViolations.push(fromV);
1323
+ }
1324
+ return buckets;
1325
+ }
1326
+ function indexByKey(violations, resolve) {
1327
+ const out = /* @__PURE__ */ new Map();
1328
+ for (const v of violations) out.set(resolve ? rebasedViolationKey(v, resolve) : violationKey(v), v);
1329
+ return out;
1330
+ }
1331
+ function computeDelta(from, to) {
1332
+ if (from.value === void 0 || to.value === void 0) return null;
1333
+ return to.value - from.value;
1334
+ }
1335
+
1336
+ export {
1337
+ SYMBOL_ID_SEP,
1338
+ symbolId,
1339
+ parseSymbolId,
1340
+ linkTestsToSources,
1341
+ testCoverageCountMetrics,
1342
+ groupTestsBySource,
1343
+ windowSuffix,
1344
+ computeRecencyWindows,
1345
+ round3,
1346
+ UNIMPORTED_ROLES,
1347
+ getEdgeWeight,
1348
+ computePageRank,
1349
+ collectNodeMetrics,
1350
+ referencedNodes,
1351
+ buildNodeMetrics,
1352
+ buildCentralFiles,
1353
+ collectSymbolUtil,
1354
+ buildHotExports,
1355
+ buildBlastRadius,
1356
+ computeSymbolConsumers,
1357
+ computeSymbolCoupling,
1358
+ computePartitionQuality,
1359
+ invertBuckets,
1360
+ patternToRegex,
1361
+ compilePatterns,
1362
+ matchesAny,
1363
+ buildReportContext,
1364
+ keepNode,
1365
+ lookupMetric,
1366
+ topHotspots,
1367
+ hotspotScoreOf,
1368
+ hotspotComplexityOf,
1369
+ busFactorOf,
1370
+ topBusFactorRisks,
1371
+ topTestCoverageRisks,
1372
+ topCentralFiles,
1373
+ publicApiFiles,
1374
+ topUnusedExports,
1375
+ topDeadModules,
1376
+ topGrowthRisks,
1377
+ topUntestedRisks,
1378
+ computeReportDrift,
1379
+ DEFAULT_HEALTH_WEIGHTS,
1380
+ computeHealth,
1381
+ classifyCoupling,
1382
+ pairKey,
1383
+ bucketFilesByPackage,
1384
+ EXTERNAL_BUCKET,
1385
+ DEFAULT_MAX_PACKAGE_SIZE,
1386
+ computeArch,
1387
+ filteredFileIds,
1388
+ aggregateEdges,
1389
+ toSortedEdges,
1390
+ packagesReferencedByEdges,
1391
+ violationKey,
1392
+ rebasedViolationKey,
1393
+ bucketViolations
1394
+ };
1395
+ //# sourceMappingURL=chunk-L5KU543A.js.map