modviz 0.1.2

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.
Files changed (109) hide show
  1. package/README.md +261 -0
  2. package/dist/client/_shell.html +0 -0
  3. package/dist/client/android-chrome-192x192.png +0 -0
  4. package/dist/client/android-chrome-512x512.png +0 -0
  5. package/dist/client/apple-touch-icon.png +0 -0
  6. package/dist/client/assets/app-Sjrldkrg.css +2 -0
  7. package/dist/client/assets/button-aOWckyNs.js +1 -0
  8. package/dist/client/assets/check-C0EQe2S8.js +1 -0
  9. package/dist/client/assets/chevron-down-DrspihmT.js +1 -0
  10. package/dist/client/assets/chevron-right-DIJHr8AN.js +1 -0
  11. package/dist/client/assets/colors-CQoWjU5E.js +1 -0
  12. package/dist/client/assets/command-kkF7_wdz.js +45 -0
  13. package/dist/client/assets/compare-K6jVFsiI.js +1 -0
  14. package/dist/client/assets/compare-TOnoe1EP.js +2 -0
  15. package/dist/client/assets/configure-DnlSnhtN.js +1 -0
  16. package/dist/client/assets/explorer-C7NclVKg.js +2 -0
  17. package/dist/client/assets/explorer-Xu2X6XXF.js +1 -0
  18. package/dist/client/assets/external-link-B9eNA-li.js +1 -0
  19. package/dist/client/assets/flamegraph-CRVZSAlj.js +13 -0
  20. package/dist/client/assets/floating-ui.dom-DLIT5tPE.js +1 -0
  21. package/dist/client/assets/formatting-CiC0SYI8.js +1 -0
  22. package/dist/client/assets/graph-6Vr74V1k.js +2 -0
  23. package/dist/client/assets/graph-CVzypIGU.js +2 -0
  24. package/dist/client/assets/graph-command-menu-D2MoVT2B.js +4 -0
  25. package/dist/client/assets/graph-command-menu-VWiiW3qy.css +1 -0
  26. package/dist/client/assets/hierarchy-C8xxGb_u.js +2 -0
  27. package/dist/client/assets/hierarchy-iO7d4oSK.js +2 -0
  28. package/dist/client/assets/import-display-D-jRyyjM.js +5 -0
  29. package/dist/client/assets/imports-CPggnrs-.js +2 -0
  30. package/dist/client/assets/imports-CodbPyUJ.js +1 -0
  31. package/dist/client/assets/index-Dj_rhLdR.js +12 -0
  32. package/dist/client/assets/input-BCFMF0aR.js +1 -0
  33. package/dist/client/assets/jsx-runtime-DWSWI4JT.js +1 -0
  34. package/dist/client/assets/lazyRouteComponent-PTSyFp1J.js +1 -0
  35. package/dist/client/assets/loading-state-CyC_hrTF.js +1 -0
  36. package/dist/client/assets/modviz-data-BiRqoDI5.js +1 -0
  37. package/dist/client/assets/modviz-layout-Do93E-IB.js +1 -0
  38. package/dist/client/assets/modviz-sigma-Xl8qHaxK.js +312 -0
  39. package/dist/client/assets/portal-BgAm3V3j.js +1 -0
  40. package/dist/client/assets/routes-DBtN8hrZ.js +1 -0
  41. package/dist/client/assets/schemas-B4zfTepZ.js +39 -0
  42. package/dist/client/assets/search-BYHxNrYn.js +1 -0
  43. package/dist/client/assets/search-params-BaZRBvGI.js +1 -0
  44. package/dist/client/assets/setup-view-j1o0TuZz.js +1 -0
  45. package/dist/client/assets/summary-D703Zh3x.js +1 -0
  46. package/dist/client/assets/tooltip-B1VDU9HG.js +1 -0
  47. package/dist/client/assets/trace-B67CM5s2.js +2 -0
  48. package/dist/client/assets/trace-Bwwdw3AM.js +1 -0
  49. package/dist/client/assets/treemap-BZf2shzY.js +5 -0
  50. package/dist/client/assets/treemap-Csroy8Gy.js +2 -0
  51. package/dist/client/assets/utils-DkkZd0ys.js +1 -0
  52. package/dist/client/favicon-16x16.png +0 -0
  53. package/dist/client/favicon-32x32.png +0 -0
  54. package/dist/client/favicon.ico +0 -0
  55. package/dist/client/favicon.png +0 -0
  56. package/dist/client/site.webmanifest +19 -0
  57. package/dist/mod/cli-options.js +225 -0
  58. package/dist/mod/cli.js +519 -0
  59. package/dist/mod/index.js +3 -0
  60. package/dist/mod/llm-analysis.js +29 -0
  61. package/dist/mod/llm-output.js +742 -0
  62. package/dist/mod/module-graph-plugins.js +60 -0
  63. package/dist/mod/production-server.js +103 -0
  64. package/dist/mod/runtime-host.js +217 -0
  65. package/dist/mod/snapshot-history.js +73 -0
  66. package/dist/mod/types.js +3 -0
  67. package/dist/server/assets/__23tanstack-start-plugin-adapters-3QxJs4a0.js +5 -0
  68. package/dist/server/assets/_tanstack-start-manifest_v-DMytuIue.js +188 -0
  69. package/dist/server/assets/button-Bqnnid5i.js +41 -0
  70. package/dist/server/assets/colors-DhAxrYua.js +100 -0
  71. package/dist/server/assets/command-SdxShIbL.js +138 -0
  72. package/dist/server/assets/compare-BFMiiUsB.js +562 -0
  73. package/dist/server/assets/compare-CpOqTpYu.js +10 -0
  74. package/dist/server/assets/configure-Bvd45DTI.js +288 -0
  75. package/dist/server/assets/explorer-C7dODpSv.js +379 -0
  76. package/dist/server/assets/explorer-CpSb0JTa.js +20 -0
  77. package/dist/server/assets/flamegraph-CdW-VG6I.js +198 -0
  78. package/dist/server/assets/formatting-iDlL4tA-.js +4 -0
  79. package/dist/server/assets/graph-C1G9H5O4.js +438 -0
  80. package/dist/server/assets/graph-DAGFGioS.js +45 -0
  81. package/dist/server/assets/graph-command-menu-BV5GtOWx.js +249 -0
  82. package/dist/server/assets/hierarchy-B4K-Zfn9.js +16 -0
  83. package/dist/server/assets/hierarchy-BGpWSG-f.js +104 -0
  84. package/dist/server/assets/import-display-BVIOWcsm.js +124 -0
  85. package/dist/server/assets/imports-B6JBDl_h.js +379 -0
  86. package/dist/server/assets/imports-BGe5tZJT.js +28 -0
  87. package/dist/server/assets/input-C5r-hBix.js +19 -0
  88. package/dist/server/assets/loading-state-CrvCWTtw.js +23 -0
  89. package/dist/server/assets/modviz-data-CUyTorv0.js +197 -0
  90. package/dist/server/assets/modviz-layout-BAH2ogse.js +253 -0
  91. package/dist/server/assets/modviz-server-DoMlAyFW.js +195 -0
  92. package/dist/server/assets/modviz-sigma-XYxARWqd.js +1441 -0
  93. package/dist/server/assets/rolldown-runtime-rSIU-vHC.js +13 -0
  94. package/dist/server/assets/router-DYJ-zDbU.js +353 -0
  95. package/dist/server/assets/routes-DInCacpY.js +244 -0
  96. package/dist/server/assets/search-params-BNApPgkX.js +26 -0
  97. package/dist/server/assets/setup-view-DjI49Iqr.js +91 -0
  98. package/dist/server/assets/start-Ba3KII43.js +4 -0
  99. package/dist/server/assets/summary-z3lXkLCQ.js +208 -0
  100. package/dist/server/assets/tooltip-Ck0DDfF7.js +24 -0
  101. package/dist/server/assets/trace-ColKOf9g.js +16 -0
  102. package/dist/server/assets/trace-eVs-hIZO.js +578 -0
  103. package/dist/server/assets/treemap-BbZ9M4GF.js +17 -0
  104. package/dist/server/assets/treemap-CrgWFoCF.js +912 -0
  105. package/dist/server/assets/utils-BQZm0uva.js +8 -0
  106. package/dist/server/server.js +5259 -0
  107. package/dist/shared/modviz-compare.js +120 -0
  108. package/dist/shared/modviz-trace.js +244 -0
  109. package/package.json +135 -0
@@ -0,0 +1,742 @@
1
+ import path from "node:path";
2
+ export function buildModvizLlmOutput(output) {
3
+ const metadata = output.metadata;
4
+ const nodeMap = new Map(output.nodes.map((node) => [node.path, node]));
5
+ const externalDependencies = output.nodes
6
+ .filter((node) => node.type === "external")
7
+ .map((node) => buildExternalDependencyReport(node, nodeMap, metadata));
8
+ const barrelFiles = output.nodes
9
+ .filter((node) => node.isBarrelFile && node.type !== "external")
10
+ .map((node) => buildBarrelFileReport(node, nodeMap, metadata));
11
+ const hotspots = output.nodes
12
+ .map((node) => buildHotspot(node, nodeMap, metadata))
13
+ .filter((node) => node.reachableModulesCount > 0 &&
14
+ (node.reachableModulesCount > 1 ||
15
+ node.reachableNodeModulesCount > 0 ||
16
+ node.directImporterCount > 1 ||
17
+ node.isBarrelFile))
18
+ .sort((left, right) => compareHotspots(right, left));
19
+ const externalPackages = buildExternalPackages(externalDependencies, metadata);
20
+ return {
21
+ format: "modviz-llm-v1",
22
+ metadata: {
23
+ ...metadata,
24
+ pathFormat: "relative-to-basePath",
25
+ },
26
+ summary: {
27
+ totalNodes: output.nodes.length,
28
+ internalNodes: output.nodes.filter((node) => node.type !== "external").length,
29
+ barrelFiles: barrelFiles.length,
30
+ externalDependencies: externalDependencies.length,
31
+ externalPackages: externalPackages.length,
32
+ nodesWithMultipleOrigins: output.nodes.filter((node) => node.chain.length > 1).length,
33
+ topHotspots: hotspots.slice(0, 10).map((hotspot) => ({
34
+ path: hotspot.path,
35
+ displayPath: hotspot.displayPath,
36
+ reachableModulesCount: hotspot.reachableModulesCount,
37
+ reachableNodeModulesCount: hotspot.reachableNodeModulesCount,
38
+ directImporterCount: hotspot.directImporterCount,
39
+ isBarrelFile: hotspot.isBarrelFile,
40
+ })),
41
+ topExternalPackagesBySourceCount: externalPackages
42
+ .slice()
43
+ .sort((left, right) => {
44
+ if (right.sourceCount !== left.sourceCount) {
45
+ return right.sourceCount - left.sourceCount;
46
+ }
47
+ return right.modulePaths.length - left.modulePaths.length;
48
+ })
49
+ .slice(0, 10)
50
+ .map((pkg) => ({
51
+ packageName: pkg.packageName,
52
+ sourceCount: pkg.sourceCount,
53
+ moduleCount: pkg.modulePaths.length,
54
+ })),
55
+ },
56
+ hotspots,
57
+ barrelFiles,
58
+ externalDependencies,
59
+ externalPackages,
60
+ };
61
+ }
62
+ export function renderModvizLlmMarkdown(report, options = {}) {
63
+ const auditHotspots = report.hotspots.filter((hotspot) => hotspot.type !== "entry" && hotspot.type !== "external");
64
+ const internalFanoutCulprits = report.hotspots.filter((hotspot) => hotspot.type !== "external" &&
65
+ hotspot.type !== "entry" &&
66
+ (hotspot.directImporterCount > 1 || hotspot.isBarrelFile) &&
67
+ (hotspot.reachableNodeModulesCount > 0 || hotspot.reachableModulesCount >= 25));
68
+ const multiSourcePackages = report.externalPackages.filter((pkg) => pkg.sourceGroupCount > 1);
69
+ const lines = [
70
+ "# modviz LLM report",
71
+ "",
72
+ ...(options.focus?.packageName || options.focus?.nodeQuery
73
+ ? [
74
+ "## Active focus",
75
+ ...(options.focus.packageName ? [`- Package query: ${options.focus.packageName}`] : []),
76
+ ...(options.focus.nodeQuery ? [`- Node query: ${options.focus.nodeQuery}`] : []),
77
+ "",
78
+ ]
79
+ : []),
80
+ "## Summary",
81
+ `- Total nodes: ${report.summary.totalNodes}`,
82
+ `- Internal nodes: ${report.summary.internalNodes}`,
83
+ `- Explicit barrel files: ${report.summary.barrelFiles}`,
84
+ `- External dependency modules: ${report.summary.externalDependencies}`,
85
+ `- External packages: ${report.summary.externalPackages}`,
86
+ `- Nodes with multiple origin chains: ${report.summary.nodesWithMultipleOrigins}`,
87
+ "",
88
+ "## Import triggers to audit",
89
+ ];
90
+ if (report.hotspots.length === 0) {
91
+ lines.push("- None");
92
+ }
93
+ else {
94
+ for (const hotspot of auditHotspots.slice(0, 10)) {
95
+ const kindLabel = hotspot.isBarrelFile ? `explicit barrel, ${hotspot.type}` : hotspot.type;
96
+ lines.push(`- ${hotspot.displayPath} (${kindLabel}) reaches ${hotspot.reachableModulesCount} modules, including ${hotspot.reachableNodeModulesCount} node_modules modules; direct importers: ${formatPreviewList(hotspot.directImporters.map((importer) => formatPathForLlm(importer, report.metadata)), 5)}`);
97
+ if (hotspot.topExternalPackages.length > 0) {
98
+ lines.push(` Pulls in: ${formatPreviewList(hotspot.topExternalPackages, 5)}`);
99
+ }
100
+ if (hotspot.signals.length > 0) {
101
+ lines.push(` Signals: ${formatPreviewList(hotspot.signals, 4)}`);
102
+ }
103
+ if (hotspot.originChains.length > 0) {
104
+ lines.push(` Origin chains: ${hotspot.originChains
105
+ .slice(0, 2)
106
+ .map((chain) => formatChainForLlm(chain, report.metadata))
107
+ .join(" | ")}${hotspot.originChains.length > 2 ? ` | +${hotspot.originChains.length - 2} more` : ""}`);
108
+ }
109
+ }
110
+ }
111
+ lines.push("", "## Internal fan-out culprits");
112
+ if (internalFanoutCulprits.length === 0) {
113
+ lines.push("- None");
114
+ }
115
+ else {
116
+ for (const hotspot of internalFanoutCulprits.slice(0, 10)) {
117
+ lines.push(`- ${hotspot.displayPath}${hotspot.isBarrelFile ? " [explicit barrel]" : ""} reaches ${hotspot.reachableModulesCount} modules, including ${hotspot.reachableNodeModulesCount} node_modules modules; direct importers: ${formatPreviewList(hotspot.directImporters.map((importer) => formatPathForLlm(importer, report.metadata)), 5)}`);
118
+ if (hotspot.topExternalPackages.length > 0) {
119
+ lines.push(` Pulls in: ${formatPreviewList(hotspot.topExternalPackages, 5)}`);
120
+ }
121
+ lines.push(` Signals: ${formatPreviewList(hotspot.signals, 4)}`);
122
+ }
123
+ }
124
+ lines.push("", "## node_modules with multiple sources");
125
+ if (multiSourcePackages.length === 0) {
126
+ lines.push("- None");
127
+ }
128
+ else {
129
+ for (const pkg of multiSourcePackages.slice(0, 10)) {
130
+ const sourceSummary = pkg.sourceGroupCount === pkg.sourceCount
131
+ ? `${pkg.sourceCount} sources`
132
+ : `${pkg.sourceCount} source files across ${pkg.sourceGroupCount} source groups`;
133
+ lines.push(`- ${pkg.packageName} is introduced by ${sourceSummary}`);
134
+ if (pkg.sourceGroups.length > 0) {
135
+ lines.push(` Top source groups: ${formatSourceGroupPreview(pkg.sourceGroups, 6)}`);
136
+ }
137
+ if (pkg.barrelSources.length > 0) {
138
+ const fanoutSources = buildSourceGroups(pkg.barrelSources, report.metadata).map((sourceGroup) => sourceGroup.label);
139
+ lines.push(` Fan-out sources: ${formatPreviewList(fanoutSources, 5)}`);
140
+ }
141
+ if (pkg.modulePaths.length > 0) {
142
+ lines.push(` Representative modules: ${formatPreviewList(pkg.modulePaths
143
+ .slice(0, 3)
144
+ .map((modulePath) => formatModulePathWithinPackage(modulePath, pkg.packageName, report.metadata)), 3)}`);
145
+ }
146
+ if (pkg.originChains.length > 0) {
147
+ lines.push(` Origin chains: ${pkg.originChains
148
+ .slice(0, 2)
149
+ .map((chain) => formatChainForLlm(chain, report.metadata))
150
+ .join(" | ")}${pkg.originChains.length > 2 ? ` | +${pkg.originChains.length - 2} more` : ""}`);
151
+ }
152
+ }
153
+ }
154
+ if (options.commandHints) {
155
+ lines.push("", "## Follow-up commands", `- Inspect one package: ${options.commandHints.packageCommand}`, `- Inspect one node: ${options.commandHints.nodeCommand}`);
156
+ }
157
+ if (options.focusedDrilldown) {
158
+ lines.push("", "## Focused drilldown", ...trimTrailingBlankLines(options.focusedDrilldown
159
+ .split("\n")
160
+ .filter((line, index) => !(index === 0 && line.trim() === "# modviz LLM drilldown"))));
161
+ }
162
+ return `${lines.join("\n")}\n`;
163
+ }
164
+ export function renderModvizLlmDrilldown(report, options) {
165
+ const limit = Math.max(options.limit ?? 20, 1);
166
+ const lines = ["# modviz LLM drilldown", ""];
167
+ let renderedSectionCount = 0;
168
+ if (options.packageName) {
169
+ const packageMatches = findExternalPackageMatches(report.externalPackages, options.packageName);
170
+ if (packageMatches.length === 0) {
171
+ lines.push(`## Package: ${options.packageName}`, "- No matching package");
172
+ }
173
+ else if (packageMatches.length > 1) {
174
+ lines.push(`## Package: ${options.packageName}`, `- Multiple matches: ${formatPreviewList(packageMatches.map((pkg) => pkg.packageName), limit)}`);
175
+ }
176
+ else {
177
+ renderPackageDrilldown(lines, packageMatches[0], report.metadata, limit);
178
+ renderedSectionCount += 1;
179
+ }
180
+ lines.push("");
181
+ }
182
+ if (options.nodeQuery) {
183
+ const nodeMatches = findNodeMatches(report, options.nodeQuery);
184
+ if (nodeMatches.length === 0) {
185
+ lines.push(`## Node: ${options.nodeQuery}`, "- No matching node");
186
+ }
187
+ else if (nodeMatches.length > 1) {
188
+ lines.push(`## Node: ${options.nodeQuery}`, `- Multiple matches: ${formatPreviewList(nodeMatches.map((node) => node.displayPath), limit)}`);
189
+ }
190
+ else {
191
+ renderNodeDrilldown(lines, nodeMatches[0], report.metadata, limit);
192
+ renderedSectionCount += 1;
193
+ }
194
+ lines.push("");
195
+ }
196
+ if (renderedSectionCount === 0 && !options.packageName && !options.nodeQuery) {
197
+ lines.push("- No drilldown query provided", "");
198
+ }
199
+ return `${trimTrailingBlankLines(lines).join("\n")}\n`;
200
+ }
201
+ export function resolveModvizFocus(report, options) {
202
+ const includedPaths = new Set();
203
+ const matchedPackageNames = options.packageName
204
+ ? findExternalPackageMatches(report.externalPackages, options.packageName).map((pkg) => pkg.packageName)
205
+ : [];
206
+ const matchedNodePaths = options.nodeQuery
207
+ ? findNodeMatches(report, options.nodeQuery).map((node) => node.path)
208
+ : [];
209
+ for (const packageName of matchedPackageNames) {
210
+ const pkg = report.externalPackages.find((externalPackage) => externalPackage.packageName === packageName);
211
+ if (!pkg) {
212
+ continue;
213
+ }
214
+ for (const modulePath of pkg.modulePaths) {
215
+ includedPaths.add(modulePath);
216
+ }
217
+ for (const source of pkg.sources) {
218
+ includedPaths.add(source);
219
+ }
220
+ for (const source of pkg.barrelSources) {
221
+ includedPaths.add(source);
222
+ }
223
+ for (const chain of pkg.originChains) {
224
+ for (const nodePath of chain) {
225
+ includedPaths.add(nodePath);
226
+ }
227
+ }
228
+ }
229
+ for (const nodePath of matchedNodePaths) {
230
+ includedPaths.add(nodePath);
231
+ const node = findNodeMatches(report, nodePath)[0];
232
+ if (!node) {
233
+ continue;
234
+ }
235
+ const directImporters = node.hotspot?.directImporters ?? node.externalDependency?.directImporters ?? [];
236
+ for (const importer of directImporters) {
237
+ includedPaths.add(importer);
238
+ }
239
+ const originChains = node.hotspot?.originChains ??
240
+ node.barrel?.originChains ??
241
+ node.externalDependency?.originChains ??
242
+ [];
243
+ for (const chain of originChains) {
244
+ for (const chainNode of chain) {
245
+ includedPaths.add(chainNode);
246
+ }
247
+ }
248
+ }
249
+ return {
250
+ matchedPackageNames,
251
+ matchedNodePaths,
252
+ includedPaths: Array.from(includedPaths),
253
+ };
254
+ }
255
+ export function inferLlmOutputPaths(outputFile) {
256
+ const parsed = path.parse(outputFile);
257
+ const llmBase = path.join(parsed.dir, `${parsed.name}.llm`);
258
+ return {
259
+ json: `${llmBase}.json`,
260
+ markdown: `${llmBase}.md`,
261
+ };
262
+ }
263
+ function buildHotspot(node, nodeMap, metadata) {
264
+ const basePath = metadata.basePath;
265
+ const reachablePaths = collectReachablePaths(node, nodeMap);
266
+ const reachableNodes = Array.from(reachablePaths)
267
+ .map((nodePath) => nodeMap.get(nodePath))
268
+ .filter(Boolean);
269
+ const reachableNodeModulesCount = reachableNodes.filter((reachableNode) => reachableNode.type === "external").length;
270
+ const reachableBarrelFilesCount = reachableNodes.filter((reachableNode) => reachableNode.isBarrelFile).length;
271
+ const signals = [];
272
+ if (node.isBarrelFile) {
273
+ signals.push("barrel-file");
274
+ }
275
+ if (reachableNodeModulesCount > 0) {
276
+ signals.push(`pulls ${reachableNodeModulesCount} node_modules modules transitively`);
277
+ }
278
+ if (node.importedBy.length > 1) {
279
+ signals.push(`shared by ${node.importedBy.length} direct importers`);
280
+ }
281
+ if (reachablePaths.size > node.importees.length + 5) {
282
+ signals.push(`fans out to ${reachablePaths.size} transitive modules`);
283
+ }
284
+ return {
285
+ path: toDisplayPath(node.path, basePath),
286
+ displayPath: formatPathForLlm(node.path, metadata),
287
+ type: node.type,
288
+ isBarrelFile: node.isBarrelFile,
289
+ directImporterCount: node.importedBy.length,
290
+ directImporters: sortStrings(node.importedBy.map((importer) => toDisplayPath(importer, basePath))),
291
+ directImporteeCount: node.importees.length,
292
+ originChains: normalizeChains(node.chain, basePath),
293
+ reachableModulesCount: reachablePaths.size,
294
+ reachableInternalModulesCount: reachableNodes.filter((reachableNode) => reachableNode.type !== "external").length,
295
+ reachableNodeModulesCount,
296
+ reachableBarrelFilesCount,
297
+ topExternalPackages: collectExternalPackageNames(reachableNodes),
298
+ signals,
299
+ };
300
+ }
301
+ function buildBarrelFileReport(node, nodeMap, metadata) {
302
+ const basePath = metadata.basePath;
303
+ const reachablePaths = collectReachablePaths(node, nodeMap);
304
+ const reachableNodes = Array.from(reachablePaths)
305
+ .map((nodePath) => nodeMap.get(nodePath))
306
+ .filter(Boolean);
307
+ const nodeModulesIntroduced = reachableNodes
308
+ .filter((reachableNode) => reachableNode.type === "external")
309
+ .map((dependency) => ({
310
+ path: toDisplayPath(dependency.path, basePath),
311
+ packageName: getPackageNameFromNodeModulesPath(dependency.path),
312
+ chainsFromBarrel: normalizeChains(dependency.chain
313
+ .filter((chain) => chain.includes(node.path))
314
+ .map((chain) => chain.slice(chain.indexOf(node.path))), basePath),
315
+ }))
316
+ .sort((left, right) => left.path.localeCompare(right.path));
317
+ return {
318
+ path: toDisplayPath(node.path, basePath),
319
+ displayPath: formatPathForLlm(node.path, metadata),
320
+ directImporterCount: node.importedBy.length,
321
+ directImporters: sortStrings(node.importedBy.map((importer) => toDisplayPath(importer, basePath))),
322
+ originChains: normalizeChains(node.chain, basePath),
323
+ impact: {
324
+ directImporteeCount: node.importees.length,
325
+ reachableModulesCount: reachablePaths.size,
326
+ reachableInternalModulesCount: reachableNodes.filter((reachableNode) => reachableNode.type !== "external").length,
327
+ reachableNodeModulesCount: reachableNodes.filter((reachableNode) => reachableNode.type === "external").length,
328
+ reachableBarrelFilesCount: reachableNodes.filter((reachableNode) => reachableNode.isBarrelFile).length,
329
+ },
330
+ nodeModulesIntroduced,
331
+ };
332
+ }
333
+ function buildExternalDependencyReport(node, nodeMap, metadata) {
334
+ const basePath = metadata.basePath;
335
+ const directImporters = sortStrings(node.importedBy.map((importer) => toDisplayPath(importer, basePath)));
336
+ const originChains = normalizeChains(node.chain, basePath);
337
+ const introducedThrough = directImporters.map((importer) => ({
338
+ path: importer,
339
+ originChains: originChains.filter((chain) => chain.at(-2) === importer),
340
+ }));
341
+ const barrelSources = sortStrings(Array.from(new Set(node.chain
342
+ .flatMap((chain) => chain.filter((nodePath) => nodeMap.get(nodePath)?.isBarrelFile))
343
+ .filter((nodePath) => nodePath !== node.path)
344
+ .map((nodePath) => toDisplayPath(nodePath, basePath)))));
345
+ return {
346
+ path: toDisplayPath(node.path, basePath),
347
+ displayPath: formatPathForLlm(node.path, metadata),
348
+ packageName: getPackageNameFromNodeModulesPath(node.path),
349
+ directImporterCount: directImporters.length,
350
+ directImporters,
351
+ originChains,
352
+ introducedThrough,
353
+ barrelSources,
354
+ };
355
+ }
356
+ function buildExternalPackages(externalDependencies, metadata) {
357
+ const packageMap = new Map();
358
+ for (const dependency of externalDependencies) {
359
+ const packageName = dependency.packageName ?? dependency.path;
360
+ const current = packageMap.get(packageName);
361
+ if (!current) {
362
+ const sourceGroups = buildSourceGroups(dependency.directImporters, metadata);
363
+ packageMap.set(packageName, {
364
+ packageName,
365
+ modulePaths: [dependency.path],
366
+ sourceCount: dependency.directImporters.length,
367
+ sourceGroupCount: sourceGroups.length,
368
+ sources: [...dependency.directImporters],
369
+ originChains: [...dependency.originChains],
370
+ barrelSources: [...dependency.barrelSources],
371
+ sourceGroups,
372
+ });
373
+ continue;
374
+ }
375
+ current.modulePaths = sortStrings([...current.modulePaths, dependency.path]);
376
+ current.sources = sortStrings([...current.sources, ...dependency.directImporters]);
377
+ current.sourceCount = current.sources.length;
378
+ current.sourceGroups = buildSourceGroups(current.sources, metadata);
379
+ current.sourceGroupCount = current.sourceGroups.length;
380
+ current.originChains = dedupeChains([...current.originChains, ...dependency.originChains]);
381
+ current.barrelSources = sortStrings([...current.barrelSources, ...dependency.barrelSources]);
382
+ }
383
+ return Array.from(packageMap.values()).sort((left, right) => {
384
+ if (right.sourceCount !== left.sourceCount) {
385
+ return right.sourceCount - left.sourceCount;
386
+ }
387
+ return left.packageName.localeCompare(right.packageName);
388
+ });
389
+ }
390
+ function collectReachablePaths(startNode, nodeMap) {
391
+ const reachable = new Set();
392
+ const stack = [...startNode.importees];
393
+ while (stack.length > 0) {
394
+ const currentPath = stack.pop();
395
+ if (!currentPath || reachable.has(currentPath) || currentPath === startNode.path) {
396
+ continue;
397
+ }
398
+ const currentNode = nodeMap.get(currentPath);
399
+ if (!currentNode) {
400
+ continue;
401
+ }
402
+ reachable.add(currentPath);
403
+ for (const importee of currentNode.importees) {
404
+ if (!reachable.has(importee)) {
405
+ stack.push(importee);
406
+ }
407
+ }
408
+ }
409
+ return reachable;
410
+ }
411
+ function normalizeChains(chains, basePath) {
412
+ return dedupeChains(chains.map((chain) => chain.map((nodePath) => toDisplayPath(nodePath, basePath))));
413
+ }
414
+ function dedupeChains(chains) {
415
+ const seen = new Set();
416
+ const deduped = [];
417
+ for (const chain of chains) {
418
+ const key = chain.join("\u0000");
419
+ if (seen.has(key)) {
420
+ continue;
421
+ }
422
+ seen.add(key);
423
+ deduped.push(chain);
424
+ }
425
+ return deduped;
426
+ }
427
+ function compareHotspots(left, right) {
428
+ if (left.reachableNodeModulesCount !== right.reachableNodeModulesCount) {
429
+ return left.reachableNodeModulesCount - right.reachableNodeModulesCount;
430
+ }
431
+ if (left.reachableModulesCount !== right.reachableModulesCount) {
432
+ return left.reachableModulesCount - right.reachableModulesCount;
433
+ }
434
+ if (left.directImporterCount !== right.directImporterCount) {
435
+ return left.directImporterCount - right.directImporterCount;
436
+ }
437
+ if (left.isBarrelFile !== right.isBarrelFile) {
438
+ return Number(left.isBarrelFile) - Number(right.isBarrelFile);
439
+ }
440
+ return left.path.localeCompare(right.path);
441
+ }
442
+ function toDisplayPath(filePath, basePath) {
443
+ if (!path.isAbsolute(filePath)) {
444
+ return filePath;
445
+ }
446
+ const relativePath = path.relative(basePath, filePath);
447
+ return relativePath && !relativePath.startsWith("..") ? relativePath : filePath;
448
+ }
449
+ function getPackageNameFromNodeModulesPath(filePath) {
450
+ const parts = filePath.split(/[\\/]/g);
451
+ const nodeModulesIndex = parts.lastIndexOf("node_modules");
452
+ if (nodeModulesIndex === -1) {
453
+ return undefined;
454
+ }
455
+ const firstPart = parts[nodeModulesIndex + 1];
456
+ if (!firstPart) {
457
+ return undefined;
458
+ }
459
+ if (firstPart.startsWith("@")) {
460
+ const secondPart = parts[nodeModulesIndex + 2];
461
+ return secondPart ? `${firstPart}/${secondPart}` : firstPart;
462
+ }
463
+ return firstPart;
464
+ }
465
+ function collectExternalPackageNames(nodes) {
466
+ const counts = new Map();
467
+ for (const node of nodes) {
468
+ if (node.type !== "external") {
469
+ continue;
470
+ }
471
+ const packageName = getPackageNameFromNodeModulesPath(node.path);
472
+ if (!packageName) {
473
+ continue;
474
+ }
475
+ counts.set(packageName, (counts.get(packageName) ?? 0) + 1);
476
+ }
477
+ return Array.from(counts.entries())
478
+ .sort((left, right) => {
479
+ if (right[1] !== left[1]) {
480
+ return right[1] - left[1];
481
+ }
482
+ return left[0].localeCompare(right[0]);
483
+ })
484
+ .map(([packageName]) => packageName);
485
+ }
486
+ function buildSourceGroups(sources, metadata) {
487
+ const groups = new Map();
488
+ for (const source of sortStrings(sources)) {
489
+ const workspacePackage = getWorkspacePackageForDisplayPath(source, metadata);
490
+ const externalPackage = getPackageNameFromNodeModulesPath(source);
491
+ const label = workspacePackage?.name ?? externalPackage ?? source;
492
+ const kind = workspacePackage
493
+ ? "workspace-package"
494
+ : externalPackage
495
+ ? "external-package"
496
+ : "file";
497
+ const current = groups.get(label);
498
+ if (!current) {
499
+ groups.set(label, {
500
+ kind,
501
+ label,
502
+ paths: [source],
503
+ });
504
+ continue;
505
+ }
506
+ current.paths = sortStrings([...current.paths, source]);
507
+ }
508
+ return Array.from(groups.values()).sort(compareSourceGroups);
509
+ }
510
+ function formatChainForLlm(chain, metadata) {
511
+ return chain.map((nodePath) => formatPathForLlm(nodePath, metadata)).join(" -> ");
512
+ }
513
+ function formatPathForLlm(filePath, metadata) {
514
+ const displayPath = toDisplayPath(filePath, metadata.basePath);
515
+ const workspacePackage = getWorkspacePackageForDisplayPath(displayPath, metadata);
516
+ if (workspacePackage) {
517
+ return `${workspacePackage.name} (${displayPath})`;
518
+ }
519
+ const externalPackageName = getPackageNameFromNodeModulesPath(displayPath);
520
+ if (externalPackageName && externalPackageName !== displayPath) {
521
+ return `${externalPackageName} (${displayPath})`;
522
+ }
523
+ return displayPath;
524
+ }
525
+ function getWorkspacePackageForDisplayPath(displayPath, metadata) {
526
+ const normalizedDisplayPath = normalizePath(displayPath);
527
+ const packages = metadata.packages
528
+ .map((pkg) => ({
529
+ ...pkg,
530
+ normalizedPath: normalizePath(pkg.path),
531
+ }))
532
+ .filter((pkg) => normalizedDisplayPath === pkg.normalizedPath ||
533
+ normalizedDisplayPath.startsWith(`${pkg.normalizedPath}/`))
534
+ .sort((left, right) => right.normalizedPath.length - left.normalizedPath.length);
535
+ return packages[0];
536
+ }
537
+ function normalizePath(filePath) {
538
+ return filePath.replace(/\\/g, "/").replace(/\/$/, "");
539
+ }
540
+ function sortStrings(values) {
541
+ return Array.from(new Set(values)).sort((left, right) => left.localeCompare(right));
542
+ }
543
+ function formatPreviewList(values, limit) {
544
+ const uniqueValues = Array.from(new Set(values));
545
+ if (uniqueValues.length === 0) {
546
+ return "none";
547
+ }
548
+ const preview = uniqueValues.slice(0, limit).join(", ");
549
+ return uniqueValues.length > limit ? `${preview}, +${uniqueValues.length - limit} more` : preview;
550
+ }
551
+ function formatSourceGroupPreview(sourceGroups, limit) {
552
+ return formatPreviewList(sourceGroups.map((group) => group.paths.length > 1 ? `${group.label} (${group.paths.length} files)` : group.label), limit);
553
+ }
554
+ function formatModulePathWithinPackage(modulePath, packageName, metadata) {
555
+ const displayPath = toDisplayPath(modulePath, metadata.basePath);
556
+ const parts = normalizePath(displayPath).split("/");
557
+ const nodeModulesIndex = parts.lastIndexOf("node_modules");
558
+ if (nodeModulesIndex === -1) {
559
+ return displayPath;
560
+ }
561
+ const packageParts = packageName.startsWith("@") ? packageName.split("/") : [packageName];
562
+ const startIndex = nodeModulesIndex + 1 + packageParts.length;
563
+ const withinPackage = parts.slice(startIndex).join("/");
564
+ return withinPackage || packageName;
565
+ }
566
+ function findExternalPackageMatches(packages, query) {
567
+ const normalizedQuery = normalizeSearchQuery(query);
568
+ const exactMatches = packages.filter((pkg) => normalizeSearchQuery(pkg.packageName) === normalizedQuery);
569
+ if (exactMatches.length > 0) {
570
+ return exactMatches;
571
+ }
572
+ return packages.filter((pkg) => normalizeSearchQuery(pkg.packageName).includes(normalizedQuery));
573
+ }
574
+ function findNodeMatches(report, query) {
575
+ const nodesByPath = new Map();
576
+ for (const hotspot of report.hotspots) {
577
+ nodesByPath.set(hotspot.path, {
578
+ path: hotspot.path,
579
+ displayPath: hotspot.displayPath,
580
+ hotspot,
581
+ barrel: report.barrelFiles.find((barrel) => barrel.path === hotspot.path),
582
+ externalDependency: report.externalDependencies.find((dependency) => dependency.path === hotspot.path),
583
+ });
584
+ }
585
+ for (const barrel of report.barrelFiles) {
586
+ const existing = nodesByPath.get(barrel.path);
587
+ if (existing) {
588
+ existing.barrel = barrel;
589
+ continue;
590
+ }
591
+ nodesByPath.set(barrel.path, {
592
+ path: barrel.path,
593
+ displayPath: barrel.displayPath,
594
+ barrel,
595
+ });
596
+ }
597
+ for (const dependency of report.externalDependencies) {
598
+ const existing = nodesByPath.get(dependency.path);
599
+ if (existing) {
600
+ existing.externalDependency = dependency;
601
+ continue;
602
+ }
603
+ nodesByPath.set(dependency.path, {
604
+ path: dependency.path,
605
+ displayPath: dependency.displayPath,
606
+ externalDependency: dependency,
607
+ });
608
+ }
609
+ const normalizedQuery = normalizeSearchQuery(query);
610
+ const nodes = Array.from(nodesByPath.values());
611
+ const exactMatches = nodes.filter((node) => buildNodeSearchTerms(node).some((term) => normalizeSearchQuery(term) === normalizedQuery));
612
+ if (exactMatches.length > 0) {
613
+ return exactMatches;
614
+ }
615
+ return nodes.filter((node) => buildNodeSearchTerms(node).some((term) => normalizeSearchQuery(term).includes(normalizedQuery)));
616
+ }
617
+ function buildNodeSearchTerms(node) {
618
+ return [node.path, node.displayPath, node.externalDependency?.packageName]
619
+ .filter(Boolean)
620
+ .map((term) => term);
621
+ }
622
+ function renderPackageDrilldown(lines, pkg, metadata, limit) {
623
+ lines.push(`## Package: ${pkg.packageName}`);
624
+ lines.push(`- Source files: ${pkg.sourceCount}`);
625
+ lines.push(`- Source groups: ${pkg.sourceGroupCount}`);
626
+ lines.push(`- Matched external modules: ${pkg.modulePaths.length}`);
627
+ if (pkg.barrelSources.length > 0) {
628
+ lines.push(`- Fan-out sources: ${formatPreviewList(buildSourceGroups(pkg.barrelSources, metadata).map((group) => group.label), limit)}`);
629
+ }
630
+ lines.push("", "### Source groups");
631
+ for (const group of pkg.sourceGroups.slice(0, limit)) {
632
+ lines.push(`- ${group.label} (${group.paths.length} file${group.paths.length === 1 ? "" : "s"}): ${formatPreviewList(group.paths, limit)}`);
633
+ }
634
+ if (pkg.sourceGroups.length > limit) {
635
+ lines.push(`- +${pkg.sourceGroups.length - limit} more source groups`);
636
+ }
637
+ lines.push("", "### Representative modules");
638
+ for (const modulePath of pkg.modulePaths.slice(0, limit)) {
639
+ lines.push(`- ${formatModulePathWithinPackage(modulePath, pkg.packageName, metadata)}`);
640
+ }
641
+ if (pkg.modulePaths.length > limit) {
642
+ lines.push(`- +${pkg.modulePaths.length - limit} more modules`);
643
+ }
644
+ lines.push("", "### Origin chains");
645
+ for (const chain of pkg.originChains.slice(0, limit)) {
646
+ lines.push(`- ${formatChainForLlm(chain, metadata)}`);
647
+ }
648
+ if (pkg.originChains.length > limit) {
649
+ lines.push(`- +${pkg.originChains.length - limit} more origin chains`);
650
+ }
651
+ }
652
+ function renderNodeDrilldown(lines, node, metadata, limit) {
653
+ lines.push(`## Node: ${node.displayPath}`);
654
+ if (node.hotspot) {
655
+ lines.push(`- Type: ${node.hotspot.type}`);
656
+ lines.push(`- Reachable modules: ${node.hotspot.reachableModulesCount}`);
657
+ lines.push(`- Reachable node_modules modules: ${node.hotspot.reachableNodeModulesCount}`);
658
+ lines.push(`- Direct importers: ${node.hotspot.directImporterCount}`);
659
+ lines.push(`- Direct importees: ${node.hotspot.directImporteeCount}`);
660
+ if (node.hotspot.topExternalPackages.length > 0) {
661
+ lines.push(`- Top external packages: ${formatPreviewList(node.hotspot.topExternalPackages, limit)}`);
662
+ }
663
+ if (node.hotspot.signals.length > 0) {
664
+ lines.push(`- Signals: ${formatPreviewList(node.hotspot.signals, limit)}`);
665
+ }
666
+ }
667
+ if (node.barrel) {
668
+ lines.push(`- Explicit barrel file: yes`);
669
+ lines.push(`- node_modules introduced: ${node.barrel.nodeModulesIntroduced.length}`);
670
+ }
671
+ if (node.externalDependency) {
672
+ lines.push(`- External package: ${node.externalDependency.packageName ?? node.externalDependency.path}`);
673
+ if (node.externalDependency.barrelSources.length > 0) {
674
+ lines.push(`- Fan-out sources: ${formatPreviewList(node.externalDependency.barrelSources, limit)}`);
675
+ }
676
+ }
677
+ if (node.hotspot?.directImporters.length || node.externalDependency?.directImporters.length) {
678
+ const importers = node.hotspot?.directImporters ?? node.externalDependency?.directImporters ?? [];
679
+ lines.push("", "### Direct importers");
680
+ for (const importer of importers.slice(0, limit)) {
681
+ lines.push(`- ${importer}`);
682
+ }
683
+ if (importers.length > limit) {
684
+ lines.push(`- +${importers.length - limit} more importers`);
685
+ }
686
+ }
687
+ if (node.externalDependency?.introducedThrough.length) {
688
+ lines.push("", "### Introduced through");
689
+ for (const source of node.externalDependency.introducedThrough.slice(0, limit)) {
690
+ lines.push(`- ${source.path}: ${source.originChains.length} chain${source.originChains.length === 1 ? "" : "s"}`);
691
+ }
692
+ if (node.externalDependency.introducedThrough.length > limit) {
693
+ lines.push(`- +${node.externalDependency.introducedThrough.length - limit} more introducers`);
694
+ }
695
+ }
696
+ const originChains = node.hotspot?.originChains ??
697
+ node.barrel?.originChains ??
698
+ node.externalDependency?.originChains ??
699
+ [];
700
+ if (originChains.length > 0) {
701
+ lines.push("", "### Origin chains");
702
+ for (const chain of originChains.slice(0, limit)) {
703
+ lines.push(`- ${formatChainForLlm(chain, metadata)}`);
704
+ }
705
+ if (originChains.length > limit) {
706
+ lines.push(`- +${originChains.length - limit} more origin chains`);
707
+ }
708
+ }
709
+ }
710
+ function normalizeSearchQuery(value) {
711
+ return normalizePath(value).toLowerCase();
712
+ }
713
+ function trimTrailingBlankLines(lines) {
714
+ const nextLines = [...lines];
715
+ while (nextLines.length > 0 && nextLines.at(-1) === "") {
716
+ nextLines.pop();
717
+ }
718
+ return nextLines;
719
+ }
720
+ function compareSourceGroups(left, right) {
721
+ const leftRank = getSourceGroupRank(left.kind);
722
+ const rightRank = getSourceGroupRank(right.kind);
723
+ if (leftRank !== rightRank) {
724
+ return leftRank - rightRank;
725
+ }
726
+ if (right.paths.length !== left.paths.length) {
727
+ return right.paths.length - left.paths.length;
728
+ }
729
+ return left.label.localeCompare(right.label);
730
+ }
731
+ function getSourceGroupRank(kind) {
732
+ switch (kind) {
733
+ case "workspace-package":
734
+ return 0;
735
+ case "file":
736
+ return 1;
737
+ case "external-package":
738
+ return 2;
739
+ default:
740
+ return 3;
741
+ }
742
+ }