@reactgraph/cli 0.1.1

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 (178) hide show
  1. package/README.md +319 -0
  2. package/bun.lock +527 -0
  3. package/dist/cli/components/IndexProgress.d.ts +18 -0
  4. package/dist/cli/components/IndexProgress.d.ts.map +1 -0
  5. package/dist/cli/components/IndexProgress.js +26 -0
  6. package/dist/cli/components/IndexProgress.js.map +1 -0
  7. package/dist/cli/components/InitResult.d.ts +7 -0
  8. package/dist/cli/components/InitResult.d.ts.map +1 -0
  9. package/dist/cli/components/InitResult.js +6 -0
  10. package/dist/cli/components/InitResult.js.map +1 -0
  11. package/dist/cli/index-cmd.d.ts +7 -0
  12. package/dist/cli/index-cmd.d.ts.map +1 -0
  13. package/dist/cli/index-cmd.js +28 -0
  14. package/dist/cli/index-cmd.js.map +1 -0
  15. package/dist/cli/index.d.ts +3 -0
  16. package/dist/cli/index.d.ts.map +1 -0
  17. package/dist/cli/index.js +81 -0
  18. package/dist/cli/index.js.map +1 -0
  19. package/dist/cli/init.d.ts +8 -0
  20. package/dist/cli/init.d.ts.map +1 -0
  21. package/dist/cli/init.js +77 -0
  22. package/dist/cli/init.js.map +1 -0
  23. package/dist/cli/serve.d.ts +2 -0
  24. package/dist/cli/serve.d.ts.map +1 -0
  25. package/dist/cli/serve.js +28 -0
  26. package/dist/cli/serve.js.map +1 -0
  27. package/dist/cli/unused.d.ts +2 -0
  28. package/dist/cli/unused.d.ts.map +1 -0
  29. package/dist/cli/unused.js +56 -0
  30. package/dist/cli/unused.js.map +1 -0
  31. package/dist/graph/graph.d.ts +30 -0
  32. package/dist/graph/graph.d.ts.map +1 -0
  33. package/dist/graph/graph.js +166 -0
  34. package/dist/graph/graph.js.map +1 -0
  35. package/dist/graph/index.d.ts +5 -0
  36. package/dist/graph/index.d.ts.map +1 -0
  37. package/dist/graph/index.js +5 -0
  38. package/dist/graph/index.js.map +1 -0
  39. package/dist/graph/schema.d.ts +33 -0
  40. package/dist/graph/schema.d.ts.map +1 -0
  41. package/dist/graph/schema.js +3 -0
  42. package/dist/graph/schema.js.map +1 -0
  43. package/dist/graph/serialize.d.ts +7 -0
  44. package/dist/graph/serialize.d.ts.map +1 -0
  45. package/dist/graph/serialize.js +39 -0
  46. package/dist/graph/serialize.js.map +1 -0
  47. package/dist/graph/traverse.d.ts +14 -0
  48. package/dist/graph/traverse.d.ts.map +1 -0
  49. package/dist/graph/traverse.js +50 -0
  50. package/dist/graph/traverse.js.map +1 -0
  51. package/dist/mcp/formatter.d.ts +26 -0
  52. package/dist/mcp/formatter.d.ts.map +1 -0
  53. package/dist/mcp/formatter.js +691 -0
  54. package/dist/mcp/formatter.js.map +1 -0
  55. package/dist/mcp/server.d.ts +2 -0
  56. package/dist/mcp/server.d.ts.map +1 -0
  57. package/dist/mcp/server.js +45 -0
  58. package/dist/mcp/server.js.map +1 -0
  59. package/dist/mcp/tools.d.ts +9 -0
  60. package/dist/mcp/tools.d.ts.map +1 -0
  61. package/dist/mcp/tools.js +136 -0
  62. package/dist/mcp/tools.js.map +1 -0
  63. package/dist/output/ai-context.d.ts +7 -0
  64. package/dist/output/ai-context.d.ts.map +1 -0
  65. package/dist/output/ai-context.js +26 -0
  66. package/dist/output/ai-context.js.map +1 -0
  67. package/dist/parser/extractors/api-calls.d.ts +15 -0
  68. package/dist/parser/extractors/api-calls.d.ts.map +1 -0
  69. package/dist/parser/extractors/api-calls.js +168 -0
  70. package/dist/parser/extractors/api-calls.js.map +1 -0
  71. package/dist/parser/extractors/components.d.ts +5 -0
  72. package/dist/parser/extractors/components.d.ts.map +1 -0
  73. package/dist/parser/extractors/components.js +236 -0
  74. package/dist/parser/extractors/components.js.map +1 -0
  75. package/dist/parser/extractors/context.d.ts +14 -0
  76. package/dist/parser/extractors/context.d.ts.map +1 -0
  77. package/dist/parser/extractors/context.js +196 -0
  78. package/dist/parser/extractors/context.js.map +1 -0
  79. package/dist/parser/extractors/effects.d.ts +14 -0
  80. package/dist/parser/extractors/effects.d.ts.map +1 -0
  81. package/dist/parser/extractors/effects.js +175 -0
  82. package/dist/parser/extractors/effects.js.map +1 -0
  83. package/dist/parser/extractors/hooks.d.ts +5 -0
  84. package/dist/parser/extractors/hooks.d.ts.map +1 -0
  85. package/dist/parser/extractors/hooks.js +242 -0
  86. package/dist/parser/extractors/hooks.js.map +1 -0
  87. package/dist/parser/extractors/imports.d.ts +6 -0
  88. package/dist/parser/extractors/imports.d.ts.map +1 -0
  89. package/dist/parser/extractors/imports.js +148 -0
  90. package/dist/parser/extractors/imports.js.map +1 -0
  91. package/dist/parser/extractors/index.d.ts +12 -0
  92. package/dist/parser/extractors/index.d.ts.map +1 -0
  93. package/dist/parser/extractors/index.js +11 -0
  94. package/dist/parser/extractors/index.js.map +1 -0
  95. package/dist/parser/extractors/jsx-tree.d.ts +5 -0
  96. package/dist/parser/extractors/jsx-tree.d.ts.map +1 -0
  97. package/dist/parser/extractors/jsx-tree.js +226 -0
  98. package/dist/parser/extractors/jsx-tree.js.map +1 -0
  99. package/dist/parser/extractors/routes.d.ts +13 -0
  100. package/dist/parser/extractors/routes.d.ts.map +1 -0
  101. package/dist/parser/extractors/routes.js +275 -0
  102. package/dist/parser/extractors/routes.js.map +1 -0
  103. package/dist/parser/extractors/state.d.ts +14 -0
  104. package/dist/parser/extractors/state.d.ts.map +1 -0
  105. package/dist/parser/extractors/state.js +368 -0
  106. package/dist/parser/extractors/state.js.map +1 -0
  107. package/dist/parser/extractors/types.d.ts +22 -0
  108. package/dist/parser/extractors/types.d.ts.map +1 -0
  109. package/dist/parser/extractors/types.js +51 -0
  110. package/dist/parser/extractors/types.js.map +1 -0
  111. package/dist/parser/indexer.d.ts +14 -0
  112. package/dist/parser/indexer.d.ts.map +1 -0
  113. package/dist/parser/indexer.js +167 -0
  114. package/dist/parser/indexer.js.map +1 -0
  115. package/dist/parser/pipeline.d.ts +16 -0
  116. package/dist/parser/pipeline.d.ts.map +1 -0
  117. package/dist/parser/pipeline.js +63 -0
  118. package/dist/parser/pipeline.js.map +1 -0
  119. package/dist/parser/setup.d.ts +4 -0
  120. package/dist/parser/setup.d.ts.map +1 -0
  121. package/dist/parser/setup.js +29 -0
  122. package/dist/parser/setup.js.map +1 -0
  123. package/dist/parser/walker.d.ts +6 -0
  124. package/dist/parser/walker.d.ts.map +1 -0
  125. package/dist/parser/walker.js +45 -0
  126. package/dist/parser/walker.js.map +1 -0
  127. package/dist/watcher.d.ts +12 -0
  128. package/dist/watcher.d.ts.map +1 -0
  129. package/dist/watcher.js +72 -0
  130. package/dist/watcher.js.map +1 -0
  131. package/package.json +51 -0
  132. package/src/cli/components/IndexProgress.tsx +79 -0
  133. package/src/cli/components/InitResult.tsx +28 -0
  134. package/src/cli/index-cmd.ts +41 -0
  135. package/src/cli/index.ts +92 -0
  136. package/src/cli/init.ts +97 -0
  137. package/src/cli/serve.ts +29 -0
  138. package/src/cli/unused.ts +88 -0
  139. package/src/graph/graph.ts +179 -0
  140. package/src/graph/index.ts +4 -0
  141. package/src/graph/schema.ts +68 -0
  142. package/src/graph/serialize.ts +40 -0
  143. package/src/graph/traverse.ts +66 -0
  144. package/src/mcp/formatter.ts +757 -0
  145. package/src/mcp/server.ts +59 -0
  146. package/src/mcp/tools.ts +154 -0
  147. package/src/output/ai-context.ts +29 -0
  148. package/src/parser/extractors/api-calls.ts +192 -0
  149. package/src/parser/extractors/components.ts +273 -0
  150. package/src/parser/extractors/context.ts +216 -0
  151. package/src/parser/extractors/effects.ts +205 -0
  152. package/src/parser/extractors/hooks.ts +268 -0
  153. package/src/parser/extractors/imports.ts +192 -0
  154. package/src/parser/extractors/index.ts +11 -0
  155. package/src/parser/extractors/jsx-tree.ts +271 -0
  156. package/src/parser/extractors/routes.ts +331 -0
  157. package/src/parser/extractors/state.ts +392 -0
  158. package/src/parser/extractors/types.ts +71 -0
  159. package/src/parser/indexer.ts +197 -0
  160. package/src/parser/pipeline.ts +89 -0
  161. package/src/parser/setup.ts +33 -0
  162. package/src/parser/walker.ts +61 -0
  163. package/src/watcher.ts +91 -0
  164. package/templates/CLAUDE.md +7 -0
  165. package/tests/extractors.test.ts +164 -0
  166. package/tests/fixtures/basic/src/App.tsx +12 -0
  167. package/tests/fixtures/basic/src/components/Dashboard.tsx +24 -0
  168. package/tests/fixtures/basic/src/components/MetricsCard.tsx +15 -0
  169. package/tests/fixtures/basic/src/components/Sidebar.tsx +20 -0
  170. package/tests/fixtures/basic/src/contexts/ThemeContext.tsx +16 -0
  171. package/tests/fixtures/basic/src/hooks/useAuth.ts +25 -0
  172. package/tests/fixtures/basic/src/stores/authStore.ts +15 -0
  173. package/tests/fixtures/basic/src/utils.ts +7 -0
  174. package/tests/graph.test.ts +91 -0
  175. package/tests/phase2.test.ts +309 -0
  176. package/tests/smoke.test.ts +77 -0
  177. package/tsconfig.json +20 -0
  178. package/vitest.config.ts +8 -0
@@ -0,0 +1,691 @@
1
+ import { getSubgraph } from '../graph/traverse.js';
2
+ /**
3
+ * Generate the full project map — token-optimized structured text.
4
+ */
5
+ export function formatMap(graph, maxComponents = 30) {
6
+ const stats = graph.stats();
7
+ const lines = [];
8
+ const routeCount = stats.Route ?? 0;
9
+ const storeCount = stats.Store ?? 0;
10
+ const endpointCount = stats.ApiEndpoint ?? 0;
11
+ // Header
12
+ lines.push(`# REACTGRAPH MAP | ${stats.Component ?? 0} components, ${stats.Hook ?? 0} hooks, ${storeCount} stores, ${routeCount} routes`);
13
+ lines.push(`# Indexed: ${new Date().toISOString()} | ${stats.files} files`);
14
+ lines.push('');
15
+ // Routes section
16
+ const routes = graph.getNodesByKind('Route');
17
+ const endpoints = graph.getNodesByKind('ApiEndpoint');
18
+ if (routes.length > 0 || endpoints.length > 0) {
19
+ lines.push('## ROUTES');
20
+ for (const route of routes) {
21
+ const rendersEdges = graph.getEdgesFrom(route.id, ['renders']);
22
+ const component = rendersEdges.length > 0 ? resolveNodeName(graph, rendersEdges[0].target) : '';
23
+ const role = route.meta.role ? ` [${route.meta.role}]` : '';
24
+ lines.push(`${route.name.padEnd(25)} → ${component}${role} [${route.file}]`);
25
+ }
26
+ for (const ep of endpoints) {
27
+ lines.push(`${ep.name.padEnd(25)} [${ep.file}]`);
28
+ }
29
+ lines.push('');
30
+ }
31
+ // Components section
32
+ const components = graph.getNodesByKind('Component')
33
+ .map(c => ({ node: c, conn: graph.connectivity(c.id) }))
34
+ .sort((a, b) => b.conn - a.conn)
35
+ .slice(0, maxComponents);
36
+ if (components.length > 0) {
37
+ lines.push(`## COMPONENTS (top ${Math.min(maxComponents, components.length)} by connectivity)`);
38
+ for (const { node } of components) {
39
+ lines.push(`${node.name} [${node.file}:${node.line}]`);
40
+ // What it renders
41
+ const rendersEdges = graph.getEdgesFrom(node.id, ['renders']);
42
+ if (rendersEdges.length > 0) {
43
+ const rendered = rendersEdges
44
+ .map(e => resolveNodeName(graph, e.target))
45
+ .filter(Boolean);
46
+ lines.push(` renders: ${rendered.join(', ')}`);
47
+ }
48
+ // Hooks used
49
+ const hookEdges = graph.getEdgesFrom(node.id, ['uses_hook']);
50
+ if (hookEdges.length > 0) {
51
+ const hooks = hookEdges
52
+ .map(e => e.meta.hookName ?? resolveNodeName(graph, e.target))
53
+ .filter(h => !h.startsWith('builtin:'));
54
+ if (hooks.length > 0) {
55
+ lines.push(` hooks: ${hooks.join(', ')}`);
56
+ }
57
+ }
58
+ // Context
59
+ const provides = graph.getEdgesFrom(node.id, ['provides']);
60
+ const consumes = graph.getEdgesFrom(node.id, ['consumes']);
61
+ if (provides.length > 0 || consumes.length > 0) {
62
+ const parts = [];
63
+ for (const e of provides)
64
+ parts.push(`${resolveNodeName(graph, e.target)}(provides)`);
65
+ for (const e of consumes)
66
+ parts.push(`${resolveNodeName(graph, e.target)}(read)`);
67
+ lines.push(` context: ${parts.join(', ')}`);
68
+ }
69
+ // Store reads/writes
70
+ const reads = graph.getEdgesFrom(node.id, ['reads_store']);
71
+ const writes = graph.getEdgesFrom(node.id, ['writes_store']);
72
+ if (reads.length > 0 || writes.length > 0) {
73
+ const parts = [];
74
+ const seen = new Set();
75
+ for (const e of reads) {
76
+ const name = resolveNodeName(graph, e.target);
77
+ if (!seen.has(name)) {
78
+ parts.push(`${name}(read)`);
79
+ seen.add(name);
80
+ }
81
+ }
82
+ for (const e of writes) {
83
+ const name = resolveNodeName(graph, e.target);
84
+ if (!seen.has(name)) {
85
+ parts.push(`${name}(write)`);
86
+ seen.add(name);
87
+ }
88
+ else {
89
+ // Upgrade to read/write
90
+ const idx = parts.findIndex(p => p.startsWith(name));
91
+ if (idx >= 0)
92
+ parts[idx] = `${name}(read/write)`;
93
+ }
94
+ }
95
+ lines.push(` state: ${parts.join(', ')}`);
96
+ }
97
+ // API calls
98
+ const fetches = graph.getEdgesFrom(node.id, ['fetches']);
99
+ if (fetches.length > 0) {
100
+ const apis = fetches.map(e => `${e.meta.method} ${e.meta.url}`);
101
+ lines.push(` fetches: ${apis.join(', ')}`);
102
+ }
103
+ // Props
104
+ if (node.props && node.props.length > 0) {
105
+ lines.push(` props: ${node.props.join(', ')}`);
106
+ }
107
+ else if (node.exportType === 'default') {
108
+ lines.push(` props: none (page component)`);
109
+ }
110
+ lines.push('');
111
+ }
112
+ }
113
+ // Hooks section
114
+ const hooks = graph.getNodesByKind('Hook');
115
+ if (hooks.length > 0) {
116
+ lines.push('## HOOKS');
117
+ for (const hook of hooks) {
118
+ lines.push(`${hook.name} [${hook.file}:${hook.line}]`);
119
+ if (hook.returns)
120
+ lines.push(` returns: ${hook.returns}`);
121
+ // Store reads/writes
122
+ const reads = graph.getEdgesFrom(hook.id, ['reads_store']);
123
+ const writes = graph.getEdgesFrom(hook.id, ['writes_store']);
124
+ if (reads.length > 0 || writes.length > 0) {
125
+ const parts = [];
126
+ for (const e of reads)
127
+ parts.push(`${resolveNodeName(graph, e.target)}(read)`);
128
+ for (const e of writes)
129
+ parts.push(`${resolveNodeName(graph, e.target)}(write)`);
130
+ lines.push(` state: ${parts.join(', ')}`);
131
+ }
132
+ // API calls
133
+ const fetches = graph.getEdgesFrom(hook.id, ['fetches']);
134
+ if (fetches.length > 0) {
135
+ const apis = fetches.map(e => `${e.meta.method} ${e.meta.url}`);
136
+ lines.push(` fetches: ${apis.join(', ')}`);
137
+ }
138
+ // Who uses this hook
139
+ const usedBy = graph.getEdgesTo(hook.id, ['uses_hook']);
140
+ if (usedBy.length > 0) {
141
+ const users = usedBy.map(e => resolveNodeName(graph, e.source)).filter(Boolean);
142
+ const display = users.length > 5
143
+ ? `${users.slice(0, 5).join(', ')} (${users.length} total)`
144
+ : users.join(', ');
145
+ lines.push(` used-by: ${display}`);
146
+ }
147
+ // Hook deps
148
+ const deps = graph.getEdgesFrom(hook.id, ['uses_hook']);
149
+ const customDeps = deps.filter(e => !e.target.startsWith('builtin:'));
150
+ if (customDeps.length > 0) {
151
+ const depNames = customDeps
152
+ .map(e => e.meta.hookName ?? resolveNodeName(graph, e.target))
153
+ .filter(Boolean);
154
+ lines.push(` deps: ${depNames.join(', ')}`);
155
+ }
156
+ lines.push('');
157
+ }
158
+ }
159
+ // State stores section
160
+ const stores = graph.getNodesByKind('Store');
161
+ if (stores.length > 0) {
162
+ lines.push('## STATE STORES');
163
+ for (const store of stores) {
164
+ const lib = store.meta.library ? `[${store.meta.library}]` : '';
165
+ lines.push(`${store.name} ${lib} [${store.file}]`);
166
+ const shape = store.meta.shape;
167
+ if (shape && shape.length > 0) {
168
+ lines.push(` shape: { ${shape.join(', ')} }`);
169
+ }
170
+ const writers = graph.getEdgesTo(store.id, ['writes_store']);
171
+ if (writers.length > 0) {
172
+ lines.push(` writers: ${writers.map(e => resolveNodeName(graph, e.source)).join(', ')}`);
173
+ }
174
+ const readers = graph.getEdgesTo(store.id, ['reads_store']);
175
+ if (readers.length > 0) {
176
+ lines.push(` readers: ${readers.map(e => resolveNodeName(graph, e.source)).join(', ')}`);
177
+ }
178
+ lines.push('');
179
+ }
180
+ }
181
+ // Context providers section
182
+ const contexts = graph.getNodesByKind('Context');
183
+ if (contexts.length > 0) {
184
+ lines.push('## CONTEXT PROVIDERS');
185
+ for (const ctx of contexts) {
186
+ const providers = graph.getEdgesTo(ctx.id, ['provides']);
187
+ const providedAt = providers.length > 0
188
+ ? ` provided-at: ${providers.map(e => resolveNodeName(graph, e.source)).join(', ')}`
189
+ : '';
190
+ lines.push(`${ctx.name} [${ctx.file}]${providedAt}`);
191
+ const consumers = graph.getEdgesTo(ctx.id, ['consumes']);
192
+ if (consumers.length > 0) {
193
+ const names = consumers.map(e => resolveNodeName(graph, e.source));
194
+ const display = names.length > 5
195
+ ? `${names.slice(0, 5).join(', ')} (${names.length} total)`
196
+ : names.join(', ');
197
+ lines.push(` consumers: ${display}`);
198
+ }
199
+ lines.push('');
200
+ }
201
+ }
202
+ // Shared modules
203
+ const modules = graph.getNodesByKind('Module');
204
+ const highFanOut = modules
205
+ .map(m => ({ node: m, importers: graph.getEdgesTo(m.id, ['imports']).length }))
206
+ .filter(m => m.importers >= 5)
207
+ .sort((a, b) => b.importers - a.importers);
208
+ if (highFanOut.length > 0) {
209
+ lines.push('## SHARED MODULES (high fan-out)');
210
+ for (const { node, importers } of highFanOut) {
211
+ lines.push(`${node.name} [${node.file}] → ${importers} importers`);
212
+ }
213
+ lines.push('');
214
+ }
215
+ return lines.join('\n');
216
+ }
217
+ /**
218
+ * Format a subgraph around a specific node — tree-style rendering.
219
+ */
220
+ export function formatSubgraph(graph, target, depth = 2) {
221
+ const node = graph.findNode(target) ?? graph.getNode(target);
222
+ if (!node)
223
+ return `Node not found: ${target}`;
224
+ const sub = getSubgraph(graph, node.id, depth);
225
+ if (!sub)
226
+ return `Could not build subgraph for: ${target}`;
227
+ const lines = [];
228
+ lines.push(`${node.name} [${node.file}:${node.line}]`);
229
+ const outgoing = graph.getEdgesFrom(node.id);
230
+ const byKind = groupBy(outgoing, e => e.kind);
231
+ for (const [kind, edges] of byKind) {
232
+ for (const edge of edges) {
233
+ const targetNode = graph.getNode(edge.target);
234
+ const targetName = targetNode ? `${targetNode.name} [${targetNode.file}]` : edge.target;
235
+ const metaStr = formatEdgeMeta(edge);
236
+ lines.push(` ├─ ${kind} → ${targetName}${metaStr}`);
237
+ if (targetNode && depth > 1) {
238
+ const deeper = graph.getEdgesFrom(targetNode.id);
239
+ for (const d of deeper.slice(0, 5)) {
240
+ const dTarget = graph.getNode(d.target);
241
+ const dName = dTarget ? dTarget.name : d.target;
242
+ lines.push(` │ └─ ${d.kind} → ${dName}`);
243
+ }
244
+ if (deeper.length > 5) {
245
+ lines.push(` │ └─ ... (${deeper.length - 5} more)`);
246
+ }
247
+ }
248
+ }
249
+ }
250
+ const incoming = graph.getEdgesTo(node.id);
251
+ if (incoming.length > 0) {
252
+ lines.push(` used-by (${incoming.length}):`);
253
+ const grouped = groupBy(incoming, e => e.kind);
254
+ for (const [kind, edges] of grouped) {
255
+ for (const edge of edges.slice(0, 10)) {
256
+ const sourceNode = graph.getNode(edge.source);
257
+ const sourceName = sourceNode ? `${sourceNode.name} [${sourceNode.file}]` : edge.source;
258
+ lines.push(` │ ← ${kind} ← ${sourceName}`);
259
+ }
260
+ if (edges.length > 10) {
261
+ lines.push(` │ ... (${edges.length - 10} more)`);
262
+ }
263
+ }
264
+ }
265
+ return lines.join('\n');
266
+ }
267
+ /**
268
+ * Format everything about a specific file.
269
+ */
270
+ export function formatFileContext(graph, filePath) {
271
+ const nodes = graph.getNodesByFile(filePath);
272
+ if (nodes.length === 0)
273
+ return `No nodes found for file: ${filePath}`;
274
+ const lines = [];
275
+ lines.push(`FILE: ${filePath}`);
276
+ // Exports
277
+ const exports = nodes.filter(n => n.exportType !== 'none');
278
+ if (exports.length > 0) {
279
+ lines.push(`EXPORTS: ${exports.map(n => `${n.name} (${n.exportType})`).join(', ')}`);
280
+ }
281
+ // Imports
282
+ const moduleNode = nodes.find(n => n.kind === 'Module');
283
+ if (moduleNode) {
284
+ const importEdges = graph.getEdgesFrom(moduleNode.id, ['imports']);
285
+ if (importEdges.length > 0) {
286
+ lines.push('IMPORTS:');
287
+ for (const edge of importEdges) {
288
+ const names = edge.meta.names ?? [];
289
+ lines.push(` ${names.join(', ')} ← ${edge.target}`);
290
+ }
291
+ }
292
+ }
293
+ // Components
294
+ for (const comp of nodes.filter(n => n.kind === 'Component')) {
295
+ lines.push('');
296
+ lines.push(`COMPONENT: ${comp.name} (line ${comp.line})`);
297
+ const renders = graph.getEdgesFrom(comp.id, ['renders']);
298
+ if (renders.length > 0) {
299
+ lines.push(` RENDERS: ${renders.map(e => resolveNodeName(graph, e.target)).join(', ')}`);
300
+ }
301
+ const hookEdges = graph.getEdgesFrom(comp.id, ['uses_hook']);
302
+ const customHooks = hookEdges.filter(e => !e.target.startsWith('builtin:'));
303
+ if (customHooks.length > 0) {
304
+ const hookDetails = customHooks.map(e => {
305
+ const name = e.meta.hookName ?? resolveNodeName(graph, e.target);
306
+ const destructured = e.meta.destructured;
307
+ return destructured ? `${name} → { ${destructured.join(', ')} }` : name;
308
+ });
309
+ lines.push(` HOOKS: ${hookDetails.join(', ')}`);
310
+ }
311
+ const provides = graph.getEdgesFrom(comp.id, ['provides']);
312
+ const consumes = graph.getEdgesFrom(comp.id, ['consumes']);
313
+ if (provides.length > 0 || consumes.length > 0) {
314
+ const parts = [];
315
+ for (const e of provides)
316
+ parts.push(`${resolveNodeName(graph, e.target)}(provides)`);
317
+ for (const e of consumes)
318
+ parts.push(`${resolveNodeName(graph, e.target)}(read)`);
319
+ lines.push(` CONTEXT: ${parts.join(', ')}`);
320
+ }
321
+ const storeReads = graph.getEdgesFrom(comp.id, ['reads_store']);
322
+ const storeWrites = graph.getEdgesFrom(comp.id, ['writes_store']);
323
+ if (storeReads.length > 0 || storeWrites.length > 0) {
324
+ const parts = [];
325
+ for (const e of storeReads)
326
+ parts.push(`${resolveNodeName(graph, e.target)}(read)`);
327
+ for (const e of storeWrites)
328
+ parts.push(`${resolveNodeName(graph, e.target)}(write)`);
329
+ lines.push(` STATE: ${parts.join(', ')}`);
330
+ }
331
+ if (comp.props && comp.props.length > 0) {
332
+ lines.push(` PROPS: ${comp.props.join(', ')}`);
333
+ }
334
+ const renderedBy = graph.getEdgesTo(comp.id, ['renders']);
335
+ if (renderedBy.length > 0) {
336
+ lines.push(` RENDERED BY: ${renderedBy.map(e => resolveNodeName(graph, e.source)).join(', ')}`);
337
+ }
338
+ }
339
+ // Hooks
340
+ for (const hook of nodes.filter(n => n.kind === 'Hook')) {
341
+ lines.push('');
342
+ lines.push(`HOOK: ${hook.name} (line ${hook.line})`);
343
+ if (hook.returns)
344
+ lines.push(` RETURNS: ${hook.returns}`);
345
+ const usedBy = graph.getEdgesTo(hook.id, ['uses_hook']);
346
+ if (usedBy.length > 0) {
347
+ lines.push(` USED BY: ${usedBy.map(e => resolveNodeName(graph, e.source)).join(', ')}`);
348
+ }
349
+ const effects = hook.meta.effects;
350
+ if (effects?.length) {
351
+ for (const eff of effects) {
352
+ const depsStr = Array.isArray(eff.deps) ? `[${eff.deps.join(', ')}]` : eff.deps;
353
+ lines.push(` EFFECT: ${eff.type} deps=${depsStr}${eff.hasCleanup ? ' +cleanup' : ''}`);
354
+ }
355
+ }
356
+ }
357
+ // Stores
358
+ for (const store of nodes.filter(n => n.kind === 'Store')) {
359
+ lines.push('');
360
+ lines.push(`STORE: ${store.name} [${store.meta.library}]`);
361
+ const shape = store.meta.shape;
362
+ if (shape?.length)
363
+ lines.push(` SHAPE: { ${shape.join(', ')} }`);
364
+ }
365
+ // Contexts
366
+ for (const ctx of nodes.filter(n => n.kind === 'Context')) {
367
+ lines.push('');
368
+ lines.push(`CONTEXT: ${ctx.name}`);
369
+ }
370
+ // Routes
371
+ for (const route of nodes.filter(n => n.kind === 'Route')) {
372
+ lines.push('');
373
+ lines.push(`ROUTE: ${route.name}`);
374
+ }
375
+ // API Endpoints
376
+ for (const ep of nodes.filter(n => n.kind === 'ApiEndpoint')) {
377
+ lines.push('');
378
+ lines.push(`ENDPOINT: ${ep.name}`);
379
+ }
380
+ return lines.join('\n');
381
+ }
382
+ /**
383
+ * Trace the data flow path from node A to node B.
384
+ */
385
+ export function formatTraceFlow(graph, from, to) {
386
+ const fromNode = graph.findNode(from) ?? graph.getNode(from);
387
+ const toNode = graph.findNode(to) ?? graph.getNode(to);
388
+ if (!fromNode)
389
+ return `Source not found: ${from}`;
390
+ if (!toNode)
391
+ return `Target not found: ${to}`;
392
+ // BFS to find shortest path
393
+ const path = findPath(graph, fromNode.id, toNode.id);
394
+ if (!path)
395
+ return `No path found from ${fromNode.name} to ${toNode.name}`;
396
+ const lines = [];
397
+ lines.push(`${fromNode.name} → ${toNode.name}`);
398
+ lines.push('');
399
+ for (let i = 0; i < path.length; i++) {
400
+ const step = path[i];
401
+ const node = graph.getNode(step.nodeId);
402
+ const indent = ' '.repeat(i);
403
+ const arrow = i === 0 ? '' : `└─ ${step.edgeKind} → `;
404
+ const name = node ? `${node.name} [${node.file}]` : step.nodeId;
405
+ lines.push(`${indent}${arrow}${name}`);
406
+ }
407
+ return lines.join('\n');
408
+ }
409
+ /**
410
+ * Show impact analysis — what breaks if target changes.
411
+ */
412
+ export function formatImpact(graph, target) {
413
+ const node = graph.findNode(target) ?? graph.getNode(target);
414
+ if (!node)
415
+ return `Node not found: ${target}`;
416
+ const lines = [];
417
+ lines.push(`IMPACT ANALYSIS: ${node.name} [${node.file}]`);
418
+ lines.push('');
419
+ // Direct dependents (things that import/use this node)
420
+ const directEdges = graph.getEdgesTo(node.id);
421
+ const directByKind = groupBy(directEdges, e => e.kind);
422
+ if (directEdges.length > 0) {
423
+ lines.push(`DIRECT (${directEdges.length} dependents):`);
424
+ for (const [kind, edges] of directByKind) {
425
+ for (const edge of edges) {
426
+ const source = graph.getNode(edge.source);
427
+ const name = source ? `${source.name} [${source.file}]` : edge.source;
428
+ lines.push(` ${kind} ← ${name}`);
429
+ }
430
+ }
431
+ lines.push('');
432
+ }
433
+ // Indirect: if this is a hook, find all components that use it,
434
+ // then find what renders those components
435
+ if (node.kind === 'Hook') {
436
+ const users = graph.getEdgesTo(node.id, ['uses_hook']);
437
+ const indirect = new Set();
438
+ for (const edge of users) {
439
+ const userNode = graph.getNode(edge.source);
440
+ if (!userNode)
441
+ continue;
442
+ // Who renders this component?
443
+ const renderers = graph.getEdgesTo(userNode.id, ['renders']);
444
+ for (const r of renderers) {
445
+ indirect.add(r.source);
446
+ }
447
+ }
448
+ if (indirect.size > 0) {
449
+ lines.push(`INDIRECT (via components using ${node.name}):`);
450
+ for (const id of indirect) {
451
+ const n = graph.getNode(id);
452
+ lines.push(` ${n ? `${n.name} [${n.file}]` : id}`);
453
+ }
454
+ lines.push('');
455
+ }
456
+ }
457
+ // If this is a store, find all readers and writers
458
+ if (node.kind === 'Store') {
459
+ const readers = graph.getEdgesTo(node.id, ['reads_store']);
460
+ const writers = graph.getEdgesTo(node.id, ['writes_store']);
461
+ if (readers.length > 0) {
462
+ lines.push('READERS (will see changed state shape):');
463
+ for (const e of readers) {
464
+ const n = graph.getNode(e.source);
465
+ lines.push(` ${n ? `${n.name} [${n.file}]` : e.source}`);
466
+ }
467
+ lines.push('');
468
+ }
469
+ if (writers.length > 0) {
470
+ lines.push('WRITERS (may need signature update):');
471
+ for (const e of writers) {
472
+ const n = graph.getNode(e.source);
473
+ lines.push(` ${n ? `${n.name} [${n.file}]` : e.source}`);
474
+ }
475
+ lines.push('');
476
+ }
477
+ }
478
+ // Safe to change analysis
479
+ if (node.kind === 'Component') {
480
+ const renders = graph.getEdgesFrom(node.id, ['renders']);
481
+ const renderedBy = graph.getEdgesTo(node.id, ['renders']);
482
+ lines.push('SAFE TO CHANGE:');
483
+ lines.push(' Internal implementation (same props + same renders = no breakage)');
484
+ if (node.props && node.props.length > 0) {
485
+ lines.push(`RISKY:`);
486
+ lines.push(` Props shape — ${renderedBy.length} parents pass: ${node.props.join(', ')}`);
487
+ }
488
+ }
489
+ return lines.join('\n');
490
+ }
491
+ /**
492
+ * Query the graph with filters.
493
+ */
494
+ export function formatFindNodes(graph, filters) {
495
+ // If query is provided, find a node by name and show it + all connections.
496
+ // This is the most common usage: "find_nodes({ query: 'useUIStore' })"
497
+ if (filters.query) {
498
+ return formatNodeQuery(graph, filters.query);
499
+ }
500
+ let results = graph.getAllNodes();
501
+ let hasFilter = false;
502
+ // Filter by kind
503
+ if (filters.kind) {
504
+ hasFilter = true;
505
+ const kind = filters.kind;
506
+ results = results.filter(n => n.kind === kind);
507
+ }
508
+ // Filter by name (substring match)
509
+ if (filters.name) {
510
+ hasFilter = true;
511
+ const name = filters.name.toLowerCase();
512
+ results = results.filter(n => n.name.toLowerCase().includes(name));
513
+ }
514
+ // Filter by hook usage
515
+ if (filters.using_hook) {
516
+ hasFilter = true;
517
+ const hookName = filters.using_hook;
518
+ results = results.filter(n => {
519
+ const hookEdges = graph.getEdgesFrom(n.id, ['uses_hook']);
520
+ return hookEdges.some(e => {
521
+ const target = graph.getNode(e.target);
522
+ return target?.name === hookName || e.meta.hookName === hookName;
523
+ });
524
+ });
525
+ }
526
+ // Filter by fetches (components/hooks that make API calls)
527
+ if (filters.fetches) {
528
+ hasFilter = true;
529
+ results = results.filter(n => {
530
+ return graph.getEdgesFrom(n.id, ['fetches']).length > 0;
531
+ });
532
+ }
533
+ // Filter orphans (components rendered by nothing)
534
+ if (filters.orphan) {
535
+ hasFilter = true;
536
+ results = results.filter(n => {
537
+ if (n.kind !== 'Component')
538
+ return false;
539
+ const renderedBy = graph.getEdgesTo(n.id, ['renders']);
540
+ return renderedBy.length === 0;
541
+ });
542
+ }
543
+ // Filter by store usage
544
+ if (filters.using_store) {
545
+ hasFilter = true;
546
+ const storeName = filters.using_store;
547
+ results = results.filter(n => {
548
+ const reads = graph.getEdgesFrom(n.id, ['reads_store']);
549
+ const writes = graph.getEdgesFrom(n.id, ['writes_store']);
550
+ return [...reads, ...writes].some(e => {
551
+ const target = graph.getNode(e.target);
552
+ return target?.name === storeName;
553
+ });
554
+ });
555
+ }
556
+ // Safety: if no recognized filter was applied, don't dump the entire graph
557
+ if (!hasFilter) {
558
+ return 'No filters provided. Use: query, name, kind, using_hook, using_store, fetches, or orphan.';
559
+ }
560
+ if (results.length === 0)
561
+ return 'No matching nodes found.';
562
+ const lines = [];
563
+ lines.push(`Found ${results.length} nodes:`);
564
+ for (const node of results) {
565
+ lines.push(` ${node.name} [${node.kind}] [${node.file}:${node.line}]`);
566
+ }
567
+ return lines.join('\n');
568
+ }
569
+ /**
570
+ * Smart query: find a node by name and return it + all connected nodes,
571
+ * grouped by relationship type. This is what Claude actually needs when
572
+ * asking "who uses useUIStore?"
573
+ */
574
+ function formatNodeQuery(graph, query) {
575
+ const node = graph.findNode(query) ?? graph.getNode(query);
576
+ if (!node) {
577
+ // Try substring match
578
+ const all = graph.getAllNodes();
579
+ const matches = all.filter(n => n.name.toLowerCase().includes(query.toLowerCase()));
580
+ if (matches.length === 0)
581
+ return `No nodes matching "${query}".`;
582
+ if (matches.length > 20) {
583
+ return `${matches.length} nodes match "${query}". Be more specific, or use kind filter.\n` +
584
+ matches.slice(0, 20).map(n => ` ${n.name} [${n.kind}] [${n.file}]`).join('\n');
585
+ }
586
+ const lines = [`Found ${matches.length} nodes matching "${query}":`];
587
+ for (const m of matches)
588
+ lines.push(` ${m.name} [${m.kind}] [${m.file}:${m.line}]`);
589
+ return lines.join('\n');
590
+ }
591
+ const lines = [];
592
+ lines.push(`${node.name} [${node.kind}] [${node.file}:${node.line}]`);
593
+ lines.push('');
594
+ // Outgoing connections
595
+ const outgoing = graph.getEdgesFrom(node.id);
596
+ if (outgoing.length > 0) {
597
+ const byKind = groupBy(outgoing, e => e.kind);
598
+ for (const [kind, edges] of byKind) {
599
+ const targets = edges.map(e => {
600
+ const t = graph.getNode(e.target);
601
+ const meta = formatEdgeMeta(e);
602
+ return t ? `${t.name} [${t.file}]${meta}` : `${e.target}${meta}`;
603
+ });
604
+ lines.push(`${kind} → ${targets.join(', ')}`);
605
+ }
606
+ lines.push('');
607
+ }
608
+ // Incoming connections (who depends on this node)
609
+ const incoming = graph.getEdgesTo(node.id);
610
+ if (incoming.length > 0) {
611
+ const byKind = groupBy(incoming, e => e.kind);
612
+ for (const [kind, edges] of byKind) {
613
+ const sources = edges.map(e => {
614
+ const s = graph.getNode(e.source);
615
+ return s ? `${s.name} [${s.file}]` : e.source;
616
+ });
617
+ lines.push(`${kind} ← ${sources.join(', ')}`);
618
+ }
619
+ }
620
+ if (outgoing.length === 0 && incoming.length === 0) {
621
+ lines.push('No connections found (isolated node).');
622
+ }
623
+ return lines.join('\n');
624
+ }
625
+ function findPath(graph, fromId, toId) {
626
+ const visited = new Set();
627
+ const queue = [
628
+ { nodeId: fromId, path: [{ nodeId: fromId, edgeKind: '' }] },
629
+ ];
630
+ while (queue.length > 0) {
631
+ const { nodeId, path } = queue.shift();
632
+ if (nodeId === toId)
633
+ return path;
634
+ if (visited.has(nodeId))
635
+ continue;
636
+ visited.add(nodeId);
637
+ // Follow outgoing edges
638
+ for (const edge of graph.getEdgesFrom(nodeId)) {
639
+ if (!visited.has(edge.target)) {
640
+ queue.push({
641
+ nodeId: edge.target,
642
+ path: [...path, { nodeId: edge.target, edgeKind: edge.kind }],
643
+ });
644
+ }
645
+ }
646
+ }
647
+ return null;
648
+ }
649
+ // ---- Helpers ----
650
+ function resolveNodeName(graph, id) {
651
+ const node = graph.getNode(id);
652
+ if (node)
653
+ return node.name;
654
+ if (id.startsWith('builtin:'))
655
+ return id.slice(8);
656
+ if (id.startsWith('external:'))
657
+ return id.slice(9);
658
+ if (id.startsWith('unresolved:'))
659
+ return id.slice(11);
660
+ if (id.startsWith('endpoint:'))
661
+ return id.slice(9);
662
+ const colonIdx = id.lastIndexOf(':');
663
+ if (colonIdx > 0)
664
+ return id.slice(colonIdx + 1);
665
+ return id;
666
+ }
667
+ function formatEdgeMeta(edge) {
668
+ const parts = [];
669
+ if (edge.meta.prop)
670
+ parts.push(`prop=${edge.meta.prop}`);
671
+ if (edge.meta.hookName)
672
+ parts.push(edge.meta.hookName);
673
+ if (edge.meta.method && edge.meta.url)
674
+ parts.push(`${edge.meta.method} ${edge.meta.url}`);
675
+ if (edge.meta.library)
676
+ parts.push(edge.meta.library);
677
+ if (parts.length === 0)
678
+ return '';
679
+ return ` (${parts.join(', ')})`;
680
+ }
681
+ function groupBy(items, keyFn) {
682
+ const map = new Map();
683
+ for (const item of items) {
684
+ const key = keyFn(item);
685
+ if (!map.has(key))
686
+ map.set(key, []);
687
+ map.get(key).push(item);
688
+ }
689
+ return map;
690
+ }
691
+ //# sourceMappingURL=formatter.js.map