spec-gen-cli 1.0.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.
Files changed (303) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1078 -0
  3. package/dist/api/analyze.d.ts +17 -0
  4. package/dist/api/analyze.d.ts.map +1 -0
  5. package/dist/api/analyze.js +109 -0
  6. package/dist/api/analyze.js.map +1 -0
  7. package/dist/api/drift.d.ts +21 -0
  8. package/dist/api/drift.d.ts.map +1 -0
  9. package/dist/api/drift.js +145 -0
  10. package/dist/api/drift.js.map +1 -0
  11. package/dist/api/generate.d.ts +18 -0
  12. package/dist/api/generate.d.ts.map +1 -0
  13. package/dist/api/generate.js +251 -0
  14. package/dist/api/generate.js.map +1 -0
  15. package/dist/api/index.d.ts +39 -0
  16. package/dist/api/index.d.ts.map +1 -0
  17. package/dist/api/index.js +32 -0
  18. package/dist/api/index.js.map +1 -0
  19. package/dist/api/init.d.ts +18 -0
  20. package/dist/api/init.d.ts.map +1 -0
  21. package/dist/api/init.js +82 -0
  22. package/dist/api/init.js.map +1 -0
  23. package/dist/api/run.d.ts +19 -0
  24. package/dist/api/run.d.ts.map +1 -0
  25. package/dist/api/run.js +291 -0
  26. package/dist/api/run.js.map +1 -0
  27. package/dist/api/specs.d.ts +49 -0
  28. package/dist/api/specs.d.ts.map +1 -0
  29. package/dist/api/specs.js +136 -0
  30. package/dist/api/specs.js.map +1 -0
  31. package/dist/api/types.d.ts +176 -0
  32. package/dist/api/types.d.ts.map +1 -0
  33. package/dist/api/types.js +9 -0
  34. package/dist/api/types.js.map +1 -0
  35. package/dist/api/verify.d.ts +20 -0
  36. package/dist/api/verify.d.ts.map +1 -0
  37. package/dist/api/verify.js +117 -0
  38. package/dist/api/verify.js.map +1 -0
  39. package/dist/cli/commands/analyze.d.ts +27 -0
  40. package/dist/cli/commands/analyze.d.ts.map +1 -0
  41. package/dist/cli/commands/analyze.js +485 -0
  42. package/dist/cli/commands/analyze.js.map +1 -0
  43. package/dist/cli/commands/drift.d.ts +9 -0
  44. package/dist/cli/commands/drift.d.ts.map +1 -0
  45. package/dist/cli/commands/drift.js +540 -0
  46. package/dist/cli/commands/drift.js.map +1 -0
  47. package/dist/cli/commands/generate.d.ts +9 -0
  48. package/dist/cli/commands/generate.d.ts.map +1 -0
  49. package/dist/cli/commands/generate.js +633 -0
  50. package/dist/cli/commands/generate.js.map +1 -0
  51. package/dist/cli/commands/init.d.ts +9 -0
  52. package/dist/cli/commands/init.d.ts.map +1 -0
  53. package/dist/cli/commands/init.js +171 -0
  54. package/dist/cli/commands/init.js.map +1 -0
  55. package/dist/cli/commands/mcp.d.ts +638 -0
  56. package/dist/cli/commands/mcp.d.ts.map +1 -0
  57. package/dist/cli/commands/mcp.js +574 -0
  58. package/dist/cli/commands/mcp.js.map +1 -0
  59. package/dist/cli/commands/run.d.ts +24 -0
  60. package/dist/cli/commands/run.d.ts.map +1 -0
  61. package/dist/cli/commands/run.js +546 -0
  62. package/dist/cli/commands/run.js.map +1 -0
  63. package/dist/cli/commands/verify.d.ts +9 -0
  64. package/dist/cli/commands/verify.d.ts.map +1 -0
  65. package/dist/cli/commands/verify.js +417 -0
  66. package/dist/cli/commands/verify.js.map +1 -0
  67. package/dist/cli/commands/view.d.ts +9 -0
  68. package/dist/cli/commands/view.d.ts.map +1 -0
  69. package/dist/cli/commands/view.js +511 -0
  70. package/dist/cli/commands/view.js.map +1 -0
  71. package/dist/cli/index.d.ts +9 -0
  72. package/dist/cli/index.d.ts.map +1 -0
  73. package/dist/cli/index.js +83 -0
  74. package/dist/cli/index.js.map +1 -0
  75. package/dist/core/analyzer/architecture-writer.d.ts +67 -0
  76. package/dist/core/analyzer/architecture-writer.d.ts.map +1 -0
  77. package/dist/core/analyzer/architecture-writer.js +209 -0
  78. package/dist/core/analyzer/architecture-writer.js.map +1 -0
  79. package/dist/core/analyzer/artifact-generator.d.ts +222 -0
  80. package/dist/core/analyzer/artifact-generator.d.ts.map +1 -0
  81. package/dist/core/analyzer/artifact-generator.js +726 -0
  82. package/dist/core/analyzer/artifact-generator.js.map +1 -0
  83. package/dist/core/analyzer/call-graph.d.ts +83 -0
  84. package/dist/core/analyzer/call-graph.d.ts.map +1 -0
  85. package/dist/core/analyzer/call-graph.js +827 -0
  86. package/dist/core/analyzer/call-graph.js.map +1 -0
  87. package/dist/core/analyzer/code-shaper.d.ts +33 -0
  88. package/dist/core/analyzer/code-shaper.d.ts.map +1 -0
  89. package/dist/core/analyzer/code-shaper.js +149 -0
  90. package/dist/core/analyzer/code-shaper.js.map +1 -0
  91. package/dist/core/analyzer/dependency-graph.d.ts +179 -0
  92. package/dist/core/analyzer/dependency-graph.d.ts.map +1 -0
  93. package/dist/core/analyzer/dependency-graph.js +574 -0
  94. package/dist/core/analyzer/dependency-graph.js.map +1 -0
  95. package/dist/core/analyzer/duplicate-detector.d.ts +52 -0
  96. package/dist/core/analyzer/duplicate-detector.d.ts.map +1 -0
  97. package/dist/core/analyzer/duplicate-detector.js +279 -0
  98. package/dist/core/analyzer/duplicate-detector.js.map +1 -0
  99. package/dist/core/analyzer/embedding-service.d.ts +50 -0
  100. package/dist/core/analyzer/embedding-service.d.ts.map +1 -0
  101. package/dist/core/analyzer/embedding-service.js +104 -0
  102. package/dist/core/analyzer/embedding-service.js.map +1 -0
  103. package/dist/core/analyzer/file-walker.d.ts +78 -0
  104. package/dist/core/analyzer/file-walker.d.ts.map +1 -0
  105. package/dist/core/analyzer/file-walker.js +531 -0
  106. package/dist/core/analyzer/file-walker.js.map +1 -0
  107. package/dist/core/analyzer/import-parser.d.ts +91 -0
  108. package/dist/core/analyzer/import-parser.d.ts.map +1 -0
  109. package/dist/core/analyzer/import-parser.js +720 -0
  110. package/dist/core/analyzer/import-parser.js.map +1 -0
  111. package/dist/core/analyzer/index.d.ts +10 -0
  112. package/dist/core/analyzer/index.d.ts.map +1 -0
  113. package/dist/core/analyzer/index.js +10 -0
  114. package/dist/core/analyzer/index.js.map +1 -0
  115. package/dist/core/analyzer/refactor-analyzer.d.ts +80 -0
  116. package/dist/core/analyzer/refactor-analyzer.d.ts.map +1 -0
  117. package/dist/core/analyzer/refactor-analyzer.js +339 -0
  118. package/dist/core/analyzer/refactor-analyzer.js.map +1 -0
  119. package/dist/core/analyzer/repository-mapper.d.ts +150 -0
  120. package/dist/core/analyzer/repository-mapper.d.ts.map +1 -0
  121. package/dist/core/analyzer/repository-mapper.js +731 -0
  122. package/dist/core/analyzer/repository-mapper.js.map +1 -0
  123. package/dist/core/analyzer/signature-extractor.d.ts +31 -0
  124. package/dist/core/analyzer/signature-extractor.d.ts.map +1 -0
  125. package/dist/core/analyzer/signature-extractor.js +387 -0
  126. package/dist/core/analyzer/signature-extractor.js.map +1 -0
  127. package/dist/core/analyzer/significance-scorer.d.ts +79 -0
  128. package/dist/core/analyzer/significance-scorer.d.ts.map +1 -0
  129. package/dist/core/analyzer/significance-scorer.js +407 -0
  130. package/dist/core/analyzer/significance-scorer.js.map +1 -0
  131. package/dist/core/analyzer/subgraph-extractor.d.ts +43 -0
  132. package/dist/core/analyzer/subgraph-extractor.d.ts.map +1 -0
  133. package/dist/core/analyzer/subgraph-extractor.js +129 -0
  134. package/dist/core/analyzer/subgraph-extractor.js.map +1 -0
  135. package/dist/core/analyzer/vector-index.d.ts +63 -0
  136. package/dist/core/analyzer/vector-index.d.ts.map +1 -0
  137. package/dist/core/analyzer/vector-index.js +169 -0
  138. package/dist/core/analyzer/vector-index.js.map +1 -0
  139. package/dist/core/drift/drift-detector.d.ts +102 -0
  140. package/dist/core/drift/drift-detector.d.ts.map +1 -0
  141. package/dist/core/drift/drift-detector.js +597 -0
  142. package/dist/core/drift/drift-detector.js.map +1 -0
  143. package/dist/core/drift/git-diff.d.ts +55 -0
  144. package/dist/core/drift/git-diff.d.ts.map +1 -0
  145. package/dist/core/drift/git-diff.js +356 -0
  146. package/dist/core/drift/git-diff.js.map +1 -0
  147. package/dist/core/drift/index.d.ts +12 -0
  148. package/dist/core/drift/index.d.ts.map +1 -0
  149. package/dist/core/drift/index.js +9 -0
  150. package/dist/core/drift/index.js.map +1 -0
  151. package/dist/core/drift/spec-mapper.d.ts +73 -0
  152. package/dist/core/drift/spec-mapper.d.ts.map +1 -0
  153. package/dist/core/drift/spec-mapper.js +353 -0
  154. package/dist/core/drift/spec-mapper.js.map +1 -0
  155. package/dist/core/generator/adr-generator.d.ts +32 -0
  156. package/dist/core/generator/adr-generator.d.ts.map +1 -0
  157. package/dist/core/generator/adr-generator.js +192 -0
  158. package/dist/core/generator/adr-generator.js.map +1 -0
  159. package/dist/core/generator/index.d.ts +9 -0
  160. package/dist/core/generator/index.d.ts.map +1 -0
  161. package/dist/core/generator/index.js +12 -0
  162. package/dist/core/generator/index.js.map +1 -0
  163. package/dist/core/generator/mapping-generator.d.ts +54 -0
  164. package/dist/core/generator/mapping-generator.d.ts.map +1 -0
  165. package/dist/core/generator/mapping-generator.js +239 -0
  166. package/dist/core/generator/mapping-generator.js.map +1 -0
  167. package/dist/core/generator/openspec-compat.d.ts +160 -0
  168. package/dist/core/generator/openspec-compat.d.ts.map +1 -0
  169. package/dist/core/generator/openspec-compat.js +523 -0
  170. package/dist/core/generator/openspec-compat.js.map +1 -0
  171. package/dist/core/generator/openspec-format-generator.d.ts +111 -0
  172. package/dist/core/generator/openspec-format-generator.d.ts.map +1 -0
  173. package/dist/core/generator/openspec-format-generator.js +817 -0
  174. package/dist/core/generator/openspec-format-generator.js.map +1 -0
  175. package/dist/core/generator/openspec-writer.d.ts +131 -0
  176. package/dist/core/generator/openspec-writer.d.ts.map +1 -0
  177. package/dist/core/generator/openspec-writer.js +379 -0
  178. package/dist/core/generator/openspec-writer.js.map +1 -0
  179. package/dist/core/generator/prompts.d.ts +35 -0
  180. package/dist/core/generator/prompts.d.ts.map +1 -0
  181. package/dist/core/generator/prompts.js +212 -0
  182. package/dist/core/generator/prompts.js.map +1 -0
  183. package/dist/core/generator/spec-pipeline.d.ts +94 -0
  184. package/dist/core/generator/spec-pipeline.d.ts.map +1 -0
  185. package/dist/core/generator/spec-pipeline.js +474 -0
  186. package/dist/core/generator/spec-pipeline.js.map +1 -0
  187. package/dist/core/generator/stages/stage1-survey.d.ts +19 -0
  188. package/dist/core/generator/stages/stage1-survey.d.ts.map +1 -0
  189. package/dist/core/generator/stages/stage1-survey.js +105 -0
  190. package/dist/core/generator/stages/stage1-survey.js.map +1 -0
  191. package/dist/core/generator/stages/stage2-entities.d.ts +11 -0
  192. package/dist/core/generator/stages/stage2-entities.d.ts.map +1 -0
  193. package/dist/core/generator/stages/stage2-entities.js +67 -0
  194. package/dist/core/generator/stages/stage2-entities.js.map +1 -0
  195. package/dist/core/generator/stages/stage3-services.d.ts +11 -0
  196. package/dist/core/generator/stages/stage3-services.d.ts.map +1 -0
  197. package/dist/core/generator/stages/stage3-services.js +75 -0
  198. package/dist/core/generator/stages/stage3-services.js.map +1 -0
  199. package/dist/core/generator/stages/stage4-api.d.ts +11 -0
  200. package/dist/core/generator/stages/stage4-api.d.ts.map +1 -0
  201. package/dist/core/generator/stages/stage4-api.js +65 -0
  202. package/dist/core/generator/stages/stage4-api.js.map +1 -0
  203. package/dist/core/generator/stages/stage5-architecture.d.ts +10 -0
  204. package/dist/core/generator/stages/stage5-architecture.d.ts.map +1 -0
  205. package/dist/core/generator/stages/stage5-architecture.js +62 -0
  206. package/dist/core/generator/stages/stage5-architecture.js.map +1 -0
  207. package/dist/core/generator/stages/stage6-adr.d.ts +8 -0
  208. package/dist/core/generator/stages/stage6-adr.d.ts.map +1 -0
  209. package/dist/core/generator/stages/stage6-adr.js +41 -0
  210. package/dist/core/generator/stages/stage6-adr.js.map +1 -0
  211. package/dist/core/services/chat-agent.d.ts +45 -0
  212. package/dist/core/services/chat-agent.d.ts.map +1 -0
  213. package/dist/core/services/chat-agent.js +310 -0
  214. package/dist/core/services/chat-agent.js.map +1 -0
  215. package/dist/core/services/chat-tools.d.ts +32 -0
  216. package/dist/core/services/chat-tools.d.ts.map +1 -0
  217. package/dist/core/services/chat-tools.js +270 -0
  218. package/dist/core/services/chat-tools.js.map +1 -0
  219. package/dist/core/services/config-manager.d.ts +61 -0
  220. package/dist/core/services/config-manager.d.ts.map +1 -0
  221. package/dist/core/services/config-manager.js +143 -0
  222. package/dist/core/services/config-manager.js.map +1 -0
  223. package/dist/core/services/gitignore-manager.d.ts +29 -0
  224. package/dist/core/services/gitignore-manager.d.ts.map +1 -0
  225. package/dist/core/services/gitignore-manager.js +106 -0
  226. package/dist/core/services/gitignore-manager.js.map +1 -0
  227. package/dist/core/services/index.d.ts +8 -0
  228. package/dist/core/services/index.d.ts.map +1 -0
  229. package/dist/core/services/index.js +8 -0
  230. package/dist/core/services/index.js.map +1 -0
  231. package/dist/core/services/llm-service.d.ts +336 -0
  232. package/dist/core/services/llm-service.d.ts.map +1 -0
  233. package/dist/core/services/llm-service.js +1155 -0
  234. package/dist/core/services/llm-service.js.map +1 -0
  235. package/dist/core/services/mcp-handlers/analysis.d.ts +42 -0
  236. package/dist/core/services/mcp-handlers/analysis.d.ts.map +1 -0
  237. package/dist/core/services/mcp-handlers/analysis.js +300 -0
  238. package/dist/core/services/mcp-handlers/analysis.js.map +1 -0
  239. package/dist/core/services/mcp-handlers/graph.d.ts +65 -0
  240. package/dist/core/services/mcp-handlers/graph.d.ts.map +1 -0
  241. package/dist/core/services/mcp-handlers/graph.js +509 -0
  242. package/dist/core/services/mcp-handlers/graph.js.map +1 -0
  243. package/dist/core/services/mcp-handlers/semantic.d.ts +38 -0
  244. package/dist/core/services/mcp-handlers/semantic.d.ts.map +1 -0
  245. package/dist/core/services/mcp-handlers/semantic.js +172 -0
  246. package/dist/core/services/mcp-handlers/semantic.js.map +1 -0
  247. package/dist/core/services/mcp-handlers/utils.d.ts +21 -0
  248. package/dist/core/services/mcp-handlers/utils.d.ts.map +1 -0
  249. package/dist/core/services/mcp-handlers/utils.js +62 -0
  250. package/dist/core/services/mcp-handlers/utils.js.map +1 -0
  251. package/dist/core/services/project-detector.d.ts +32 -0
  252. package/dist/core/services/project-detector.d.ts.map +1 -0
  253. package/dist/core/services/project-detector.js +111 -0
  254. package/dist/core/services/project-detector.js.map +1 -0
  255. package/dist/core/verifier/index.d.ts +5 -0
  256. package/dist/core/verifier/index.d.ts.map +1 -0
  257. package/dist/core/verifier/index.js +5 -0
  258. package/dist/core/verifier/index.js.map +1 -0
  259. package/dist/core/verifier/verification-engine.d.ts +226 -0
  260. package/dist/core/verifier/verification-engine.d.ts.map +1 -0
  261. package/dist/core/verifier/verification-engine.js +681 -0
  262. package/dist/core/verifier/verification-engine.js.map +1 -0
  263. package/dist/types/index.d.ts +252 -0
  264. package/dist/types/index.d.ts.map +1 -0
  265. package/dist/types/index.js +5 -0
  266. package/dist/types/index.js.map +1 -0
  267. package/dist/types/pipeline.d.ts +148 -0
  268. package/dist/types/pipeline.d.ts.map +1 -0
  269. package/dist/types/pipeline.js +5 -0
  270. package/dist/types/pipeline.js.map +1 -0
  271. package/dist/utils/errors.d.ts +51 -0
  272. package/dist/utils/errors.d.ts.map +1 -0
  273. package/dist/utils/errors.js +128 -0
  274. package/dist/utils/errors.js.map +1 -0
  275. package/dist/utils/logger.d.ts +149 -0
  276. package/dist/utils/logger.d.ts.map +1 -0
  277. package/dist/utils/logger.js +331 -0
  278. package/dist/utils/logger.js.map +1 -0
  279. package/dist/utils/progress.d.ts +142 -0
  280. package/dist/utils/progress.d.ts.map +1 -0
  281. package/dist/utils/progress.js +280 -0
  282. package/dist/utils/progress.js.map +1 -0
  283. package/dist/utils/prompts.d.ts +53 -0
  284. package/dist/utils/prompts.d.ts.map +1 -0
  285. package/dist/utils/prompts.js +199 -0
  286. package/dist/utils/prompts.js.map +1 -0
  287. package/dist/utils/shutdown.d.ts +89 -0
  288. package/dist/utils/shutdown.d.ts.map +1 -0
  289. package/dist/utils/shutdown.js +237 -0
  290. package/dist/utils/shutdown.js.map +1 -0
  291. package/package.json +114 -0
  292. package/src/viewer/InteractiveGraphViewer.jsx +1486 -0
  293. package/src/viewer/app/index.html +17 -0
  294. package/src/viewer/app/main.jsx +13 -0
  295. package/src/viewer/components/ArchitectureView.jsx +177 -0
  296. package/src/viewer/components/ChatPanel.jsx +448 -0
  297. package/src/viewer/components/ClusterGraph.jsx +441 -0
  298. package/src/viewer/components/FilterBar.jsx +179 -0
  299. package/src/viewer/components/FlatGraph.jsx +275 -0
  300. package/src/viewer/components/MicroComponents.jsx +83 -0
  301. package/src/viewer/hooks/usePanZoom.js +79 -0
  302. package/src/viewer/utils/constants.js +47 -0
  303. package/src/viewer/utils/graph-helpers.js +291 -0
@@ -0,0 +1,441 @@
1
+ import { useMemo, useRef, useEffect } from 'react';
2
+ import { usePanZoom } from '../hooks/usePanZoom.js';
3
+ import { extColor } from '../utils/constants.js';
4
+ import { computeClusterLayout } from '../utils/graph-helpers.js';
5
+
6
+ export function ClusterGraph({
7
+ clusters,
8
+ edges,
9
+ nodes,
10
+ allNodes,
11
+ expandedClusters,
12
+ onToggle,
13
+ onSelectNode,
14
+ onClear,
15
+ hasSelection,
16
+ selectedId,
17
+ affectedIds,
18
+ linkedIds,
19
+ focusedIds,
20
+ }) {
21
+ const clusterPos = useMemo(
22
+ () => computeClusterLayout(clusters),
23
+ [clusters.map((c) => c.id).join()]
24
+ );
25
+ const visibleIds = useMemo(() => new Set(nodes.map((n) => n.id)), [nodes]);
26
+ const {
27
+ transform,
28
+ onMouseDown,
29
+ onMouseMove,
30
+ onMouseUp,
31
+ onWheel,
32
+ onMouseLeave,
33
+ onDblClick,
34
+ reset,
35
+ panToCenter,
36
+ isDrag,
37
+ } = usePanZoom();
38
+
39
+ const clusterKey = clusters.map((c) => c.id).join('|');
40
+ useEffect(() => {
41
+ reset();
42
+ }, [clusterKey]);
43
+
44
+ const prevExpandedRef = useRef(new Set());
45
+ useEffect(() => {
46
+ for (const cid of expandedClusters) {
47
+ if (prevExpandedRef.current.has(cid)) continue;
48
+ const cp = clusterPos[cid];
49
+ if (!cp) continue;
50
+ const members = (allNodes || nodes).filter((n) => n.cluster.id === cid);
51
+ const r = 55 + members.length * 9 + 20;
52
+ const { k, x: tx, y: ty } = transform;
53
+ const inView =
54
+ (cp.x - r) * k + tx >= 0 &&
55
+ (cp.x + r) * k + tx <= 900 &&
56
+ (cp.y - r) * k + ty >= 0 &&
57
+ (cp.y + r) * k + ty <= 540;
58
+ if (!inView) {
59
+ panToCenter(cp.x, cp.y);
60
+ }
61
+ }
62
+ prevExpandedRef.current = new Set(expandedClusters);
63
+ }, [expandedClusters]);
64
+
65
+ const nodeLayouts = useMemo(() => {
66
+ const layouts = {};
67
+ clusters.forEach((cl) => {
68
+ if (!expandedClusters.has(cl.id)) return;
69
+ const members = (allNodes || nodes).filter((n) => n.cluster.id === cl.id);
70
+ const cp = clusterPos[cl.id];
71
+ if (!cp) return;
72
+ const r = 55 + members.length * 9;
73
+ const layout = {};
74
+ members.forEach((n, i) => {
75
+ const a = (i / Math.max(members.length, 1)) * Math.PI * 2 - Math.PI / 2;
76
+ layout[n.id] = { x: cp.x + Math.cos(a) * r, y: cp.y + Math.sin(a) * r };
77
+ });
78
+ layouts[cl.id] = layout;
79
+ });
80
+ return layouts;
81
+ }, [clusters, expandedClusters, allNodes, nodes, clusterPos]);
82
+
83
+ const clusterEdges = useMemo(() => {
84
+ const counts = {};
85
+ edges.forEach((e) => {
86
+ const sn = nodes.find((n) => n.id === e.source);
87
+ const tn = nodes.find((n) => n.id === e.target);
88
+ if (!sn || !tn || sn.cluster.id === tn.cluster.id) return;
89
+ const key = `${sn.cluster.id}->${tn.cluster.id}`;
90
+ counts[key] = (counts[key] || 0) + 1;
91
+ });
92
+ return Object.entries(counts).map(([key, count]) => {
93
+ const [sc, tc] = key.split('->');
94
+ return { id: key, source: sc, target: tc, count };
95
+ });
96
+ }, [edges, nodes]);
97
+
98
+ return (
99
+ <svg
100
+ viewBox="0 0 900 540"
101
+ style={{ width: '100%', height: '100%', cursor: 'grab' }}
102
+ onMouseDown={onMouseDown}
103
+ onMouseMove={onMouseMove}
104
+ onMouseUp={onMouseUp}
105
+ onMouseLeave={onMouseLeave}
106
+ onWheel={onWheel}
107
+ onDoubleClick={onDblClick}
108
+ onClick={(e) => {
109
+ if (!isDrag() && (e.target === e.currentTarget || e.target.tagName === 'svg'))
110
+ onSelectNode(null);
111
+ }}
112
+ >
113
+ <defs>
114
+ <marker id="carr" markerWidth="6" markerHeight="6" refX="5" refY="2.5" orient="auto">
115
+ <path d="M0,0 L0,5 L6,2.5z" fill="#2a3060" />
116
+ </marker>
117
+ <marker id="carr-sel" markerWidth="6" markerHeight="6" refX="5" refY="2.5" orient="auto">
118
+ <path d="M0,0 L0,5 L6,2.5z" fill="#7c6af7" />
119
+ </marker>
120
+ <marker id="carr-in" markerWidth="6" markerHeight="6" refX="5" refY="2.5" orient="auto">
121
+ <path d="M0,0 L0,5 L6,2.5z" fill="#3ecfcf" />
122
+ </marker>
123
+ <filter id="cglow">
124
+ <feGaussianBlur stdDeviation="6" result="b" />
125
+ <feMerge>
126
+ <feMergeNode in="b" />
127
+ <feMergeNode in="SourceGraphic" />
128
+ </feMerge>
129
+ </filter>
130
+ <filter id="nglow">
131
+ <feGaussianBlur stdDeviation="3" result="b" />
132
+ <feMerge>
133
+ <feMergeNode in="b" />
134
+ <feMergeNode in="SourceGraphic" />
135
+ </feMerge>
136
+ </filter>
137
+ </defs>
138
+
139
+ <g transform={`translate(${transform.x},${transform.y}) scale(${transform.k})`}>
140
+ {/* Inter-cluster edges */}
141
+ {!selectedId &&
142
+ clusterEdges.map((e) => {
143
+ const s = clusterPos[e.source],
144
+ t = clusterPos[e.target];
145
+ if (!s || !t) return null;
146
+ const dx = t.x - s.x,
147
+ dy = t.y - s.y,
148
+ len = Math.sqrt(dx * dx + dy * dy) || 1;
149
+ const nx = dx / len,
150
+ ny = dy / len;
151
+ const rs = 38,
152
+ rt = 38;
153
+ const w = Math.min(1 + e.count * 0.15, 4);
154
+ return (
155
+ <g key={e.id}>
156
+ <line
157
+ x1={s.x + nx * rs}
158
+ y1={s.y + ny * rs}
159
+ x2={t.x - nx * (rt + 5)}
160
+ y2={t.y - ny * (rt + 5)}
161
+ stroke="#1e2448"
162
+ strokeWidth={w}
163
+ strokeOpacity={0.5}
164
+ markerEnd="url(#carr)"
165
+ />
166
+ <text
167
+ x={(s.x + nx * rs + t.x - nx * (rt + 5)) / 2}
168
+ y={(s.y + ny * rs + t.y - ny * (rt + 5)) / 2 - 4}
169
+ textAnchor="middle"
170
+ fontSize={7}
171
+ fill="#2a3060"
172
+ fontFamily="'JetBrains Mono',monospace"
173
+ style={{ pointerEvents: 'none' }}
174
+ >
175
+ {e.count}
176
+ </text>
177
+ </g>
178
+ );
179
+ })}
180
+
181
+ {/* Node-level edges when a node is selected */}
182
+ {selectedId &&
183
+ (() => {
184
+ const getNodePos = (nid) => {
185
+ const n = (allNodes || nodes).find((x) => x.id === nid);
186
+ if (!n) return null;
187
+ const clId = n.cluster.id;
188
+ if (nodeLayouts[clId]?.[nid]) return nodeLayouts[clId][nid];
189
+ return clusterPos[clId] || null;
190
+ };
191
+
192
+ return edges
193
+ .filter((e) => e.source === selectedId || e.target === selectedId)
194
+ .map((e, i) => {
195
+ const sp = getNodePos(e.source);
196
+ const tp = getNodePos(e.target);
197
+ if (!sp || !tp) return null;
198
+ const dx = tp.x - sp.x,
199
+ dy = tp.y - sp.y,
200
+ len = Math.sqrt(dx * dx + dy * dy) || 1;
201
+ const nx = dx / len,
202
+ ny = dy / len;
203
+ const isOut = e.source === selectedId;
204
+ return (
205
+ <line
206
+ key={i}
207
+ x1={sp.x + nx * 14}
208
+ y1={sp.y + ny * 14}
209
+ x2={tp.x - nx * 19}
210
+ y2={tp.y - ny * 19}
211
+ stroke={isOut ? '#7c6af7' : '#3ecfcf'}
212
+ strokeWidth={1.5}
213
+ strokeOpacity={0.9}
214
+ strokeDasharray={e.isType ? '4 2' : undefined}
215
+ markerEnd={isOut ? 'url(#carr-sel)' : 'url(#carr-in)'}
216
+ />
217
+ );
218
+ });
219
+ })()}
220
+
221
+ {/* Intra-cluster edges (expanded) */}
222
+ {clusters
223
+ .filter((cl) => expandedClusters.has(cl.id))
224
+ .map((cl) => {
225
+ const layout = nodeLayouts[cl.id] || {};
226
+ return edges
227
+ .filter((e) => {
228
+ const sn = nodes.find((n) => n.id === e.source);
229
+ const tn = nodes.find((n) => n.id === e.target);
230
+ return sn?.cluster.id === cl.id && tn?.cluster.id === cl.id;
231
+ })
232
+ .map((e) => {
233
+ const s = layout[e.source],
234
+ t = layout[e.target];
235
+ if (!s || !t) return null;
236
+ const dx = t.x - s.x,
237
+ dy = t.y - s.y,
238
+ len = Math.sqrt(dx * dx + dy * dy) || 1;
239
+ const nx = dx / len,
240
+ ny = dy / len,
241
+ r = 13;
242
+ return (
243
+ <line
244
+ key={e.id}
245
+ x1={s.x + nx * r}
246
+ y1={s.y + ny * r}
247
+ x2={t.x - nx * (r + 4)}
248
+ y2={t.y - ny * (r + 4)}
249
+ stroke={cl.color}
250
+ strokeWidth={0.8}
251
+ strokeOpacity={0.45}
252
+ strokeDasharray={e.isType ? '3 2' : undefined}
253
+ markerEnd="url(#carr)"
254
+ />
255
+ );
256
+ });
257
+ })}
258
+
259
+ {/* Cluster bubbles */}
260
+ {clusters.map((cl) => {
261
+ const p = clusterPos[cl.id];
262
+ if (!p) return null;
263
+ const allMembers = (allNodes || nodes).filter((n) => n.cluster.id === cl.id);
264
+ const visibleMembers = allMembers.filter((n) => visibleIds.has(n.id));
265
+ const isExpanded = expandedClusters.has(cl.id);
266
+ const r = 32 + Math.min(allMembers.length * 1.4, 18);
267
+ const inDeg = clusterEdges
268
+ .filter((e) => e.target === cl.id)
269
+ .reduce((s, e) => s + e.count, 0);
270
+ const outDeg = clusterEdges
271
+ .filter((e) => e.source === cl.id)
272
+ .reduce((s, e) => s + e.count, 0);
273
+
274
+ return (
275
+ <g key={cl.id}>
276
+ {(() => {
277
+ const clusterLinked =
278
+ linkedIds.size > 0 && allMembers.some((n) => linkedIds.has(n.id));
279
+ const hasFocused = focusedIds?.length > 0;
280
+ const clusterFocused = hasFocused && allMembers.some((n) => focusedIds.includes(n.id));
281
+ const isClusterGreyed =
282
+ (hasFocused && !clusterFocused && !clusterLinked) ||
283
+ (!hasFocused && visibleMembers.length === 0 && !clusterLinked);
284
+ const isLinkedCollapsed = clusterLinked && !isExpanded;
285
+ return (
286
+ <g
287
+ transform={`translate(${p.x},${p.y})`}
288
+ onClick={() => {
289
+ if (!isDrag()) onToggle(cl.id);
290
+ }}
291
+ style={{ cursor: 'pointer' }}
292
+ filter={
293
+ isExpanded ? 'url(#cglow)' : isLinkedCollapsed ? 'url(#cglow)' : undefined
294
+ }
295
+ opacity={isClusterGreyed ? 0.18 : 1}
296
+ >
297
+ <circle
298
+ r={r}
299
+ fill={isLinkedCollapsed ? `${cl.color}18` : `${cl.color}10`}
300
+ stroke={cl.color}
301
+ strokeWidth={isExpanded ? 1.8 : isLinkedCollapsed ? 1.5 : 1}
302
+ strokeOpacity={isExpanded ? 0.85 : isLinkedCollapsed ? 0.7 : 0.35}
303
+ />
304
+ <text
305
+ textAnchor="middle"
306
+ y={-10}
307
+ fontSize={8.5}
308
+ fontWeight={700}
309
+ fill={cl.color}
310
+ fontFamily="'JetBrains Mono',monospace"
311
+ style={{ pointerEvents: 'none' }}
312
+ >
313
+ {cl.name.split('/').pop()}
314
+ </text>
315
+ <text
316
+ textAnchor="middle"
317
+ y={3}
318
+ fontSize={7}
319
+ fill={`${cl.color}90`}
320
+ fontFamily="'JetBrains Mono',monospace"
321
+ style={{ pointerEvents: 'none' }}
322
+ >
323
+ {visibleMembers.length}/{allMembers.length} files
324
+ </text>
325
+ <text
326
+ textAnchor="middle"
327
+ y={14}
328
+ fontSize={6.5}
329
+ fill={`${cl.color}60`}
330
+ fontFamily="'JetBrains Mono',monospace"
331
+ style={{ pointerEvents: 'none' }}
332
+ >
333
+ ↙{inDeg} ↗{outDeg}
334
+ </text>
335
+ <text
336
+ textAnchor="middle"
337
+ y={r - 8}
338
+ fontSize={9}
339
+ fill={`${cl.color}80`}
340
+ fontFamily="'JetBrains Mono',monospace"
341
+ style={{ pointerEvents: 'none' }}
342
+ >
343
+ {isExpanded ? '▲' : '▼'}
344
+ </text>
345
+ </g>
346
+ );
347
+ })()}
348
+
349
+ {/* Member nodes */}
350
+ {isExpanded &&
351
+ allMembers.map((n) => {
352
+ const np = nodeLayouts[cl.id]?.[n.id];
353
+ if (!np) return null;
354
+ const isSel = n.id === selectedId;
355
+ const isAff = affectedIds.includes(n.id);
356
+ const col = extColor(n.ext);
357
+ const isInFocused = focusedIds?.includes(n.id) || false;
358
+ const isLinked = linkedIds.has(n.id);
359
+ const hasFocused = focusedIds?.length > 0;
360
+ const isVisible = visibleIds.has(n.id);
361
+ const isGreyed = !isInFocused && (
362
+ (hasFocused && !isLinked) ||
363
+ (!hasFocused && !isVisible && !isLinked)
364
+ );
365
+ return (
366
+ <g
367
+ key={n.id}
368
+ transform={`translate(${np.x},${np.y})`}
369
+ onClick={(e) => {
370
+ e.stopPropagation();
371
+ if (!isDrag()) onSelectNode(n.id);
372
+ }}
373
+ style={{ cursor: 'pointer' }}
374
+ filter={isSel ? 'url(#nglow)' : undefined}
375
+ opacity={isGreyed ? 0.18 : 1}
376
+ >
377
+ <circle
378
+ r={13}
379
+ fill={isSel ? `${col}1a` : '#0b0d1e'}
380
+ stroke={isSel ? col : isAff ? col : cl.color}
381
+ strokeWidth={isSel ? 2 : 0.8}
382
+ strokeOpacity={isSel ? 1 : isAff ? 0.9 : 0.45}
383
+ />
384
+ <text
385
+ textAnchor="middle"
386
+ dominantBaseline="middle"
387
+ fontSize={6}
388
+ fill={isSel ? '#fff' : '#5a6090'}
389
+ fontFamily="'JetBrains Mono',monospace"
390
+ style={{ pointerEvents: 'none' }}
391
+ >
392
+ {n.label.length > 10 ? n.label.slice(0, 9) + '...' : n.label}
393
+ </text>
394
+ </g>
395
+ );
396
+ })}
397
+ </g>
398
+ );
399
+ })}
400
+ </g>
401
+ <foreignObject x="8" y="8" width="52" height="44" style={{ pointerEvents: 'all' }}>
402
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
403
+ <button
404
+ onClick={reset}
405
+ title="Reset pan/zoom (or double-click background)"
406
+ style={{
407
+ fontSize: 8,
408
+ padding: '2px 6px',
409
+ background: '#0d0f22',
410
+ border: `1px solid ${transform.x !== 0 || transform.y !== 0 || transform.k !== 1 ? '#7c6af7' : '#1a1f38'}`,
411
+ borderRadius: 4,
412
+ color: transform.x !== 0 || transform.y !== 0 || transform.k !== 1 ? '#7c6af7' : '#2a2f4a',
413
+ cursor: 'pointer',
414
+ fontFamily: "'JetBrains Mono',monospace",
415
+ letterSpacing: '0.05em',
416
+ }}
417
+ >
418
+ ⌖ view
419
+ </button>
420
+ <button
421
+ onClick={onClear}
422
+ title="Clear selection and collapse all clusters (Escape)"
423
+ style={{
424
+ fontSize: 8,
425
+ padding: '2px 6px',
426
+ background: '#0d0f22',
427
+ border: `1px solid ${hasSelection ? '#7c6af7' : '#1a1f38'}`,
428
+ borderRadius: 4,
429
+ color: hasSelection ? '#7c6af7' : '#2a2f4a',
430
+ cursor: hasSelection ? 'pointer' : 'default',
431
+ fontFamily: "'JetBrains Mono',monospace",
432
+ letterSpacing: '0.05em',
433
+ }}
434
+ >
435
+ x clear
436
+ </button>
437
+ </div>
438
+ </foreignObject>
439
+ </svg>
440
+ );
441
+ }
@@ -0,0 +1,179 @@
1
+ function FToggle({ active, onChange, label, badge, activeColor = '#7c6af7' }) {
2
+ return (
3
+ <button
4
+ onClick={() => onChange(!active)}
5
+ style={{
6
+ background: active ? `${activeColor}1a` : 'transparent',
7
+ border: `1px solid ${active ? activeColor : '#1a1f38'}`,
8
+ borderRadius: 4,
9
+ color: active ? activeColor : '#3a3f5c',
10
+ padding: '2px 8px',
11
+ cursor: 'pointer',
12
+ fontSize: 9,
13
+ fontFamily: 'inherit',
14
+ display: 'flex',
15
+ alignItems: 'center',
16
+ gap: 4,
17
+ }}
18
+ >
19
+ {label}
20
+ {badge !== undefined && (
21
+ <span
22
+ style={{
23
+ background: '#0d0f22',
24
+ borderRadius: 3,
25
+ padding: '0 4px',
26
+ color: '#3a3f5c',
27
+ fontSize: 8,
28
+ }}
29
+ >
30
+ {badge}
31
+ </span>
32
+ )}
33
+ </button>
34
+ );
35
+ }
36
+
37
+ export function FilterBar({ filters, setFilters, stats, clusterNames }) {
38
+ const hasActive =
39
+ filters.hideOrphans ||
40
+ filters.minScore > 0 ||
41
+ filters.topN < 999 ||
42
+ filters.cluster !== '' ||
43
+ filters.refactorOnly;
44
+ return (
45
+ <div
46
+ style={{
47
+ display: 'flex',
48
+ alignItems: 'center',
49
+ gap: 10,
50
+ flexWrap: 'wrap',
51
+ padding: '7px 18px',
52
+ borderBottom: '1px solid #0f1224',
53
+ background: '#07091a',
54
+ flexShrink: 0,
55
+ fontSize: 9,
56
+ }}
57
+ >
58
+ <span style={{ color: '#2a2f4a', letterSpacing: '0.08em', fontWeight: 700 }}>FILTERS</span>
59
+
60
+ <FToggle
61
+ active={filters.hideOrphans}
62
+ onChange={(v) => setFilters((f) => ({ ...f, hideOrphans: v }))}
63
+ label="Hide orphans"
64
+ badge={stats.orphanCount}
65
+ activeColor="#f77c6a"
66
+ />
67
+
68
+ <FToggle
69
+ active={filters.refactorOnly}
70
+ onChange={(v) => setFilters((f) => ({ ...f, refactorOnly: v }))}
71
+ label="Nodes to refactor"
72
+ badge={stats.refactorVisible}
73
+ activeColor="#f97373"
74
+ />
75
+
76
+ <div style={{ display: 'flex', alignItems: 'center', gap: 5 }}>
77
+ <span style={{ color: '#2a2f4a' }}>{'Score >='}</span>
78
+ <input
79
+ type="range"
80
+ min={0}
81
+ max={65}
82
+ step={5}
83
+ value={filters.minScore}
84
+ onChange={(e) => setFilters((f) => ({ ...f, minScore: +e.target.value }))}
85
+ style={{ width: 72, accentColor: '#7c6af7' }}
86
+ />
87
+ <span style={{ color: '#7c6af7', minWidth: 16 }}>{filters.minScore}</span>
88
+ </div>
89
+
90
+ <div style={{ display: 'flex', alignItems: 'center', gap: 5 }}>
91
+ <span style={{ color: '#2a2f4a' }}>Top</span>
92
+ <select
93
+ value={filters.topN}
94
+ onChange={(e) => setFilters((f) => ({ ...f, topN: +e.target.value }))}
95
+ style={{
96
+ background: '#0d0f22',
97
+ border: '1px solid #1a1f38',
98
+ color: '#c8cde8',
99
+ borderRadius: 4,
100
+ padding: '2px 5px',
101
+ fontSize: 9,
102
+ fontFamily: 'inherit',
103
+ }}
104
+ >
105
+ {[999, 50, 30, 20, 10].map((n) => (
106
+ <option key={n} value={n}>
107
+ {n === 999 ? 'All' : String(n)}
108
+ </option>
109
+ ))}
110
+ </select>
111
+ <span style={{ color: '#2a2f4a' }}>nodes</span>
112
+ </div>
113
+
114
+ <div style={{ display: 'flex', alignItems: 'center', gap: 5 }}>
115
+ <span style={{ color: '#2a2f4a' }}>Cluster</span>
116
+ <select
117
+ value={filters.cluster}
118
+ onChange={(e) => setFilters((f) => ({ ...f, cluster: e.target.value }))}
119
+ style={{
120
+ background: '#0d0f22',
121
+ border: '1px solid #1a1f38',
122
+ color: '#c8cde8',
123
+ borderRadius: 4,
124
+ padding: '2px 5px',
125
+ fontSize: 9,
126
+ fontFamily: 'inherit',
127
+ maxWidth: 130,
128
+ }}
129
+ >
130
+ <option value="">All</option>
131
+ {clusterNames.map((n) => (
132
+ <option key={n} value={n}>
133
+ {n}
134
+ </option>
135
+ ))}
136
+ </select>
137
+ </div>
138
+
139
+ {hasActive && (
140
+ <button
141
+ onClick={() =>
142
+ setFilters({
143
+ hideOrphans: false,
144
+ minScore: 0,
145
+ topN: 999,
146
+ cluster: '',
147
+ refactorOnly: false,
148
+ })
149
+ }
150
+ style={{
151
+ background: 'none',
152
+ border: '1px solid #2a2f4a',
153
+ borderRadius: 4,
154
+ color: '#3a3f5c',
155
+ padding: '2px 7px',
156
+ cursor: 'pointer',
157
+ fontSize: 9,
158
+ fontFamily: 'inherit',
159
+ }}
160
+ >
161
+ reset
162
+ </button>
163
+ )}
164
+
165
+ <div style={{ marginLeft: 'auto', color: '#2a2f4a', display: 'flex', gap: 8 }}>
166
+ <span>
167
+ <span style={{ color: '#7c6af7' }}>{stats.visible}</span>/
168
+ <span style={{ color: '#3a4060' }}>{stats.total}</span> nodes
169
+ </span>
170
+ <span>
171
+ <span style={{ color: '#3ecfcf' }}>{stats.visibleEdges}</span> edges
172
+ </span>
173
+ {stats.orphanCount > 0 && !filters.hideOrphans && (
174
+ <span style={{ color: '#f77c6a66' }}>{stats.orphanCount} orphans</span>
175
+ )}
176
+ </div>
177
+ </div>
178
+ );
179
+ }