@trench-craft/sds 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 (276) hide show
  1. package/README.en.md +522 -0
  2. package/README.md +566 -0
  3. package/dist/bin/sds.d.ts +3 -0
  4. package/dist/bin/sds.d.ts.map +1 -0
  5. package/dist/bin/sds.js +50 -0
  6. package/dist/bin/sds.js.map +1 -0
  7. package/dist/src/__tests__/cli.test.d.ts +2 -0
  8. package/dist/src/__tests__/cli.test.d.ts.map +1 -0
  9. package/dist/src/__tests__/cli.test.js +37 -0
  10. package/dist/src/__tests__/cli.test.js.map +1 -0
  11. package/dist/src/__tests__/e2e.test.d.ts +5 -0
  12. package/dist/src/__tests__/e2e.test.d.ts.map +1 -0
  13. package/dist/src/__tests__/e2e.test.js +143 -0
  14. package/dist/src/__tests__/e2e.test.js.map +1 -0
  15. package/dist/src/__tests__/graph.test.d.ts +5 -0
  16. package/dist/src/__tests__/graph.test.d.ts.map +1 -0
  17. package/dist/src/__tests__/graph.test.js +423 -0
  18. package/dist/src/__tests__/graph.test.js.map +1 -0
  19. package/dist/src/__tests__/openspec.test.d.ts +5 -0
  20. package/dist/src/__tests__/openspec.test.d.ts.map +1 -0
  21. package/dist/src/__tests__/openspec.test.js +172 -0
  22. package/dist/src/__tests__/openspec.test.js.map +1 -0
  23. package/dist/src/commands/apply.d.ts +4 -0
  24. package/dist/src/commands/apply.d.ts.map +1 -0
  25. package/dist/src/commands/apply.js +14 -0
  26. package/dist/src/commands/apply.js.map +1 -0
  27. package/dist/src/commands/archive.d.ts +4 -0
  28. package/dist/src/commands/archive.d.ts.map +1 -0
  29. package/dist/src/commands/archive.js +20 -0
  30. package/dist/src/commands/archive.js.map +1 -0
  31. package/dist/src/commands/cache.d.ts +4 -0
  32. package/dist/src/commands/cache.d.ts.map +1 -0
  33. package/dist/src/commands/cache.js +31 -0
  34. package/dist/src/commands/cache.js.map +1 -0
  35. package/dist/src/commands/config.d.ts +4 -0
  36. package/dist/src/commands/config.d.ts.map +1 -0
  37. package/dist/src/commands/config.js +29 -0
  38. package/dist/src/commands/config.js.map +1 -0
  39. package/dist/src/commands/e2e.d.ts +4 -0
  40. package/dist/src/commands/e2e.d.ts.map +1 -0
  41. package/dist/src/commands/e2e.js +65 -0
  42. package/dist/src/commands/e2e.js.map +1 -0
  43. package/dist/src/commands/graph.d.ts +4 -0
  44. package/dist/src/commands/graph.d.ts.map +1 -0
  45. package/dist/src/commands/graph.js +783 -0
  46. package/dist/src/commands/graph.js.map +1 -0
  47. package/dist/src/commands/init.d.ts +4 -0
  48. package/dist/src/commands/init.d.ts.map +1 -0
  49. package/dist/src/commands/init.js +15 -0
  50. package/dist/src/commands/init.js.map +1 -0
  51. package/dist/src/commands/project.d.ts +4 -0
  52. package/dist/src/commands/project.d.ts.map +1 -0
  53. package/dist/src/commands/project.js +6 -0
  54. package/dist/src/commands/project.js.map +1 -0
  55. package/dist/src/commands/propose.d.ts +4 -0
  56. package/dist/src/commands/propose.d.ts.map +1 -0
  57. package/dist/src/commands/propose.js +26 -0
  58. package/dist/src/commands/propose.js.map +1 -0
  59. package/dist/src/commands/registry.d.ts +4 -0
  60. package/dist/src/commands/registry.d.ts.map +1 -0
  61. package/dist/src/commands/registry.js +6 -0
  62. package/dist/src/commands/registry.js.map +1 -0
  63. package/dist/src/commands/skills-install.d.ts +4 -0
  64. package/dist/src/commands/skills-install.d.ts.map +1 -0
  65. package/dist/src/commands/skills-install.js +158 -0
  66. package/dist/src/commands/skills-install.js.map +1 -0
  67. package/dist/src/commands/skills.d.ts +4 -0
  68. package/dist/src/commands/skills.d.ts.map +1 -0
  69. package/dist/src/commands/skills.js +19 -0
  70. package/dist/src/commands/skills.js.map +1 -0
  71. package/dist/src/commands/verify.d.ts +4 -0
  72. package/dist/src/commands/verify.d.ts.map +1 -0
  73. package/dist/src/commands/verify.js +31 -0
  74. package/dist/src/commands/verify.js.map +1 -0
  75. package/dist/src/core/engine.d.ts +33 -0
  76. package/dist/src/core/engine.d.ts.map +1 -0
  77. package/dist/src/core/engine.js +87 -0
  78. package/dist/src/core/engine.js.map +1 -0
  79. package/dist/src/core/engine.test.d.ts +2 -0
  80. package/dist/src/core/engine.test.d.ts.map +1 -0
  81. package/dist/src/core/engine.test.js +13 -0
  82. package/dist/src/core/engine.test.js.map +1 -0
  83. package/dist/src/core/session-state.d.ts +18 -0
  84. package/dist/src/core/session-state.d.ts.map +1 -0
  85. package/dist/src/core/session-state.js +55 -0
  86. package/dist/src/core/session-state.js.map +1 -0
  87. package/dist/src/core/session-state.test.d.ts +2 -0
  88. package/dist/src/core/session-state.test.d.ts.map +1 -0
  89. package/dist/src/core/session-state.test.js +90 -0
  90. package/dist/src/core/session-state.test.js.map +1 -0
  91. package/dist/src/core/subagent-timeout.d.ts +18 -0
  92. package/dist/src/core/subagent-timeout.d.ts.map +1 -0
  93. package/dist/src/core/subagent-timeout.js +61 -0
  94. package/dist/src/core/subagent-timeout.js.map +1 -0
  95. package/dist/src/core/subagent-timeout.test.d.ts +2 -0
  96. package/dist/src/core/subagent-timeout.test.d.ts.map +1 -0
  97. package/dist/src/core/subagent-timeout.test.js +57 -0
  98. package/dist/src/core/subagent-timeout.test.js.map +1 -0
  99. package/dist/src/core/task-sync.d.ts +19 -0
  100. package/dist/src/core/task-sync.d.ts.map +1 -0
  101. package/dist/src/core/task-sync.js +62 -0
  102. package/dist/src/core/task-sync.js.map +1 -0
  103. package/dist/src/core/task-sync.test.d.ts +2 -0
  104. package/dist/src/core/task-sync.test.d.ts.map +1 -0
  105. package/dist/src/core/task-sync.test.js +84 -0
  106. package/dist/src/core/task-sync.test.js.map +1 -0
  107. package/dist/src/graph/advanced-performance.d.ts +137 -0
  108. package/dist/src/graph/advanced-performance.d.ts.map +1 -0
  109. package/dist/src/graph/advanced-performance.js +375 -0
  110. package/dist/src/graph/advanced-performance.js.map +1 -0
  111. package/dist/src/graph/database.d.ts +79 -0
  112. package/dist/src/graph/database.d.ts.map +1 -0
  113. package/dist/src/graph/database.js +305 -0
  114. package/dist/src/graph/database.js.map +1 -0
  115. package/dist/src/graph/engine.d.ts +43 -0
  116. package/dist/src/graph/engine.d.ts.map +1 -0
  117. package/dist/src/graph/engine.js +334 -0
  118. package/dist/src/graph/engine.js.map +1 -0
  119. package/dist/src/graph/exporter.d.ts +56 -0
  120. package/dist/src/graph/exporter.d.ts.map +1 -0
  121. package/dist/src/graph/exporter.js +273 -0
  122. package/dist/src/graph/exporter.js.map +1 -0
  123. package/dist/src/graph/index.d.ts +21 -0
  124. package/dist/src/graph/index.d.ts.map +1 -0
  125. package/dist/src/graph/index.js +14 -0
  126. package/dist/src/graph/index.js.map +1 -0
  127. package/dist/src/graph/layouts.d.ts +77 -0
  128. package/dist/src/graph/layouts.d.ts.map +1 -0
  129. package/dist/src/graph/layouts.js +368 -0
  130. package/dist/src/graph/layouts.js.map +1 -0
  131. package/dist/src/graph/parser.d.ts +47 -0
  132. package/dist/src/graph/parser.d.ts.map +1 -0
  133. package/dist/src/graph/parser.js +228 -0
  134. package/dist/src/graph/parser.js.map +1 -0
  135. package/dist/src/graph/performance.d.ts +90 -0
  136. package/dist/src/graph/performance.d.ts.map +1 -0
  137. package/dist/src/graph/performance.js +275 -0
  138. package/dist/src/graph/performance.js.map +1 -0
  139. package/dist/src/graph/semantic.d.ts +151 -0
  140. package/dist/src/graph/semantic.d.ts.map +1 -0
  141. package/dist/src/graph/semantic.js +402 -0
  142. package/dist/src/graph/semantic.js.map +1 -0
  143. package/dist/src/graph/types.d.ts +114 -0
  144. package/dist/src/graph/types.d.ts.map +1 -0
  145. package/dist/src/graph/types.js +5 -0
  146. package/dist/src/graph/types.js.map +1 -0
  147. package/dist/src/graph/visualizer.d.ts +50 -0
  148. package/dist/src/graph/visualizer.d.ts.map +1 -0
  149. package/dist/src/graph/visualizer.js +869 -0
  150. package/dist/src/graph/visualizer.js.map +1 -0
  151. package/dist/src/openspec/apply.d.ts +16 -0
  152. package/dist/src/openspec/apply.d.ts.map +1 -0
  153. package/dist/src/openspec/apply.js +140 -0
  154. package/dist/src/openspec/apply.js.map +1 -0
  155. package/dist/src/openspec/archive.d.ts +3 -0
  156. package/dist/src/openspec/archive.d.ts.map +1 -0
  157. package/dist/src/openspec/archive.js +17 -0
  158. package/dist/src/openspec/archive.js.map +1 -0
  159. package/dist/src/openspec/e2e-generate.d.ts +39 -0
  160. package/dist/src/openspec/e2e-generate.d.ts.map +1 -0
  161. package/dist/src/openspec/e2e-generate.js +315 -0
  162. package/dist/src/openspec/e2e-generate.js.map +1 -0
  163. package/dist/src/openspec/e2e-runner.d.ts +32 -0
  164. package/dist/src/openspec/e2e-runner.d.ts.map +1 -0
  165. package/dist/src/openspec/e2e-runner.js +208 -0
  166. package/dist/src/openspec/e2e-runner.js.map +1 -0
  167. package/dist/src/openspec/explore.d.ts +3 -0
  168. package/dist/src/openspec/explore.d.ts.map +1 -0
  169. package/dist/src/openspec/explore.js +20 -0
  170. package/dist/src/openspec/explore.js.map +1 -0
  171. package/dist/src/openspec/propose.d.ts +8 -0
  172. package/dist/src/openspec/propose.d.ts.map +1 -0
  173. package/dist/src/openspec/propose.js +124 -0
  174. package/dist/src/openspec/propose.js.map +1 -0
  175. package/dist/src/openspec/verify.d.ts +13 -0
  176. package/dist/src/openspec/verify.d.ts.map +1 -0
  177. package/dist/src/openspec/verify.js +156 -0
  178. package/dist/src/openspec/verify.js.map +1 -0
  179. package/dist/src/platform/claudecode.d.ts +2 -0
  180. package/dist/src/platform/claudecode.d.ts.map +1 -0
  181. package/dist/src/platform/claudecode.js +7 -0
  182. package/dist/src/platform/claudecode.js.map +1 -0
  183. package/dist/src/platform/codex.d.ts +2 -0
  184. package/dist/src/platform/codex.d.ts.map +1 -0
  185. package/dist/src/platform/codex.js +7 -0
  186. package/dist/src/platform/codex.js.map +1 -0
  187. package/dist/src/platform/opencode.d.ts +2 -0
  188. package/dist/src/platform/opencode.d.ts.map +1 -0
  189. package/dist/src/platform/opencode.js +7 -0
  190. package/dist/src/platform/opencode.js.map +1 -0
  191. package/dist/src/platform/router.d.ts +13 -0
  192. package/dist/src/platform/router.d.ts.map +1 -0
  193. package/dist/src/platform/router.js +57 -0
  194. package/dist/src/platform/router.js.map +1 -0
  195. package/dist/src/skills/cache.d.ts +16 -0
  196. package/dist/src/skills/cache.d.ts.map +1 -0
  197. package/dist/src/skills/cache.js +53 -0
  198. package/dist/src/skills/cache.js.map +1 -0
  199. package/dist/src/skills/discovery.d.ts +12 -0
  200. package/dist/src/skills/discovery.d.ts.map +1 -0
  201. package/dist/src/skills/discovery.js +61 -0
  202. package/dist/src/skills/discovery.js.map +1 -0
  203. package/dist/src/skills/loader.d.ts +12 -0
  204. package/dist/src/skills/loader.d.ts.map +1 -0
  205. package/dist/src/skills/loader.js +69 -0
  206. package/dist/src/skills/loader.js.map +1 -0
  207. package/dist/src/skills/registry.d.ts +23 -0
  208. package/dist/src/skills/registry.d.ts.map +1 -0
  209. package/dist/src/skills/registry.js +68 -0
  210. package/dist/src/skills/registry.js.map +1 -0
  211. package/dist/src/utils/fs.d.ts +5 -0
  212. package/dist/src/utils/fs.d.ts.map +1 -0
  213. package/dist/src/utils/fs.js +23 -0
  214. package/dist/src/utils/fs.js.map +1 -0
  215. package/dist/src/utils/logger.d.ts +7 -0
  216. package/dist/src/utils/logger.d.ts.map +1 -0
  217. package/dist/src/utils/logger.js +8 -0
  218. package/dist/src/utils/logger.js.map +1 -0
  219. package/dist/src/utils/yaml.d.ts +3 -0
  220. package/dist/src/utils/yaml.d.ts.map +1 -0
  221. package/dist/src/utils/yaml.js +8 -0
  222. package/dist/src/utils/yaml.js.map +1 -0
  223. package/package.json +62 -0
  224. package/registry/skills-registry.yaml +218 -0
  225. package/skills/core/brainstorming/SKILL.md +259 -0
  226. package/skills/core/brainstorming/scripts/frame-template.html +214 -0
  227. package/skills/core/brainstorming/scripts/helper.js +88 -0
  228. package/skills/core/brainstorming/scripts/server.cjs +338 -0
  229. package/skills/core/brainstorming/scripts/start-server.sh +153 -0
  230. package/skills/core/brainstorming/scripts/stop-server.sh +55 -0
  231. package/skills/core/brainstorming/skill.yaml +12 -0
  232. package/skills/core/brainstorming/spec-document-reviewer-prompt.md +48 -0
  233. package/skills/core/brainstorming/visual-companion.md +286 -0
  234. package/skills/core/claude-code-core/SKILL.md +164 -0
  235. package/skills/core/claude-code-core/skill.yaml +14 -0
  236. package/skills/core/claude-code-prompt/SKILL.md +283 -0
  237. package/skills/core/claude-code-prompt/skill.yaml +14 -0
  238. package/skills/core/claude-code-subagent/SKILL.md +168 -0
  239. package/skills/core/claude-code-subagent/skill.yaml +14 -0
  240. package/skills/core/e2e-generate/SKILL.md +147 -0
  241. package/skills/core/e2e-generate/skill.yaml +12 -0
  242. package/skills/core/ecc-agents-md-router/SKILL.md +90 -0
  243. package/skills/core/ecc-agents-md-router/skill.yaml +12 -0
  244. package/skills/core/ecc-context-injector/SKILL.md +69 -0
  245. package/skills/core/ecc-context-injector/skill.yaml +12 -0
  246. package/skills/core/existing-code-caveman/SKILL.md +340 -0
  247. package/skills/core/existing-code-caveman/skill.yaml +12 -0
  248. package/skills/core/opsx-apply/SKILL.md +121 -0
  249. package/skills/core/opsx-apply/skill.yaml +12 -0
  250. package/skills/core/opsx-archive/SKILL.md +83 -0
  251. package/skills/core/opsx-archive/skill.yaml +12 -0
  252. package/skills/core/opsx-explore/SKILL.md +101 -0
  253. package/skills/core/opsx-explore/skill.yaml +12 -0
  254. package/skills/core/opsx-propose/SKILL.md +131 -0
  255. package/skills/core/opsx-propose/skill.yaml +16 -0
  256. package/skills/core/opsx-verify/SKILL.md +109 -0
  257. package/skills/core/opsx-verify/skill.yaml +12 -0
  258. package/skills/core/subagent-driven-development/SKILL.md +157 -0
  259. package/skills/core/subagent-driven-development/code-quality-reviewer-prompt.md +64 -0
  260. package/skills/core/subagent-driven-development/implementer-prompt.md +122 -0
  261. package/skills/core/subagent-driven-development/skill.yaml +12 -0
  262. package/skills/core/subagent-driven-development/spec-reviewer-prompt.md +61 -0
  263. package/skills/core/writing-plans/SKILL.md +268 -0
  264. package/skills/core/writing-plans/plan-document-reviewer-prompt.md +63 -0
  265. package/skills/core/writing-plans/skill.yaml +12 -0
  266. package/skills/locale/chinese-code-review/SKILL.md +17 -0
  267. package/skills/locale/chinese-code-review/skill.yaml +16 -0
  268. package/skills/locale/chinese-commit-conventions/SKILL.md +17 -0
  269. package/skills/locale/chinese-commit-conventions/skill.yaml +16 -0
  270. package/skills/locale/chinese-documentation/SKILL.md +17 -0
  271. package/skills/locale/chinese-documentation/skill.yaml +16 -0
  272. package/skills/locale/chinese-git-workflow/SKILL.md +17 -0
  273. package/skills/locale/chinese-git-workflow/skill.yaml +16 -0
  274. package/templates/agents-md.md +42 -0
  275. package/templates/claude-md.md +44 -0
  276. package/templates/codex-md.md +49 -0
@@ -0,0 +1,869 @@
1
+ /**
2
+ * GraphVisualizer - 图谱可视化器
3
+ * 生成 HTML 交互图谱
4
+ */
5
+ import { resolve } from 'path';
6
+ import { writeFile, mkdir } from 'fs/promises';
7
+ import { LayoutEngine } from './layouts.js';
8
+ export class GraphVisualizer {
9
+ db;
10
+ projectDir;
11
+ layoutEngine;
12
+ constructor(projectDir, db) {
13
+ this.projectDir = projectDir;
14
+ this.db = db;
15
+ this.layoutEngine = new LayoutEngine();
16
+ }
17
+ /**
18
+ * 生成可视化
19
+ */
20
+ async visualize(options) {
21
+ const startTime = Date.now();
22
+ const errors = [];
23
+ try {
24
+ // 获取图数据
25
+ const { nodes, edges } = this.buildGraph(options);
26
+ // 根据格式生成输出
27
+ let content;
28
+ let outputPath;
29
+ switch (options.format) {
30
+ case 'html':
31
+ content = this.generateHtml(nodes, edges, options);
32
+ outputPath = options.outputPath || resolve(this.projectDir, '.sds', 'graph.html');
33
+ break;
34
+ case 'svg':
35
+ content = this.generateSvg(nodes, edges, options);
36
+ outputPath = options.outputPath || resolve(this.projectDir, '.sds', 'graph.svg');
37
+ break;
38
+ default:
39
+ throw new Error(`Unsupported format: ${options.format}`);
40
+ }
41
+ // 确保目录存在
42
+ // sync removed
43
+ // sync removed
44
+ const { dirname } = await import('path');
45
+ await mkdir(dirname(outputPath), { recursive: true });
46
+ // 写入文件
47
+ await writeFile(outputPath, content, 'utf-8');
48
+ return {
49
+ status: 'completed',
50
+ outputPath,
51
+ format: options.format,
52
+ nodesCount: nodes.length,
53
+ edgesCount: edges.length,
54
+ durationMs: Date.now() - startTime,
55
+ errors,
56
+ };
57
+ }
58
+ catch (error) {
59
+ errors.push(error.message);
60
+ return {
61
+ status: 'failed',
62
+ outputPath: '',
63
+ format: options.format,
64
+ nodesCount: 0,
65
+ edgesCount: 0,
66
+ durationMs: Date.now() - startTime,
67
+ errors,
68
+ };
69
+ }
70
+ }
71
+ /**
72
+ * 构建图数据
73
+ */
74
+ buildGraph(options) {
75
+ const nodes = [];
76
+ const edges = [];
77
+ const nodeMap = new Map();
78
+ // 获取文件
79
+ const files = this.db.getAllFiles();
80
+ const symbols = this.db.getAllSymbols();
81
+ const callEdges = this.db.query(`
82
+ SELECT
83
+ cg.id,
84
+ cg.caller_file_id,
85
+ cg.caller_symbol_id,
86
+ cg.callee_file_id,
87
+ cg.callee_symbol_id,
88
+ cg.relation_type,
89
+ f1.path as caller_file,
90
+ s1.name as caller_name,
91
+ f2.path as callee_file,
92
+ s2.name as callee_name
93
+ FROM call_graph cg
94
+ JOIN files f1 ON cg.caller_file_id = f1.id
95
+ LEFT JOIN symbols s1 ON cg.caller_symbol_id = s1.id
96
+ JOIN files f2 ON cg.callee_file_id = f2.id
97
+ LEFT JOIN symbols s2 ON cg.callee_symbol_id = s2.id
98
+ `);
99
+ // 创建模块节点
100
+ const modules = new Set();
101
+ for (const file of files) {
102
+ const parts = file.path.split('/');
103
+ if (parts.length > 1) {
104
+ modules.add(parts[0]);
105
+ }
106
+ }
107
+ for (const module of modules) {
108
+ const node = {
109
+ id: `module:${module}`,
110
+ label: module,
111
+ type: 'module',
112
+ size: 30,
113
+ color: '#4CAF50',
114
+ group: 'module',
115
+ };
116
+ nodes.push(node);
117
+ nodeMap.set(node.id, node);
118
+ }
119
+ // 创建文件节点
120
+ for (const file of files.slice(0, options.maxNodes)) {
121
+ const parts = file.path.split('/');
122
+ const module = parts.length > 1 ? parts[0] : 'root';
123
+ const node = {
124
+ id: `file:${file.path}`,
125
+ label: parts[parts.length - 1],
126
+ type: 'file',
127
+ path: file.path,
128
+ size: 15,
129
+ color: '#2196F3',
130
+ group: module,
131
+ };
132
+ nodes.push(node);
133
+ nodeMap.set(node.id, node);
134
+ // 添加模块到文件的边
135
+ edges.push({
136
+ id: `edge:module:${module}->file:${file.path}`,
137
+ source: `module:${module}`,
138
+ target: `file:${file.path}`,
139
+ type: 'import',
140
+ weight: 1,
141
+ });
142
+ }
143
+ // 创建符号节点
144
+ for (const symbol of symbols.slice(0, options.maxNodes * 2)) {
145
+ const file = files.find(f => f.id === symbol.fileId);
146
+ if (!file)
147
+ continue;
148
+ const node = {
149
+ id: `symbol:${file.path}:${symbol.name}`,
150
+ label: symbol.name,
151
+ type: symbol.type,
152
+ path: file.path,
153
+ line: symbol.lineStart,
154
+ size: 10,
155
+ color: this.getSymbolColor(symbol.type),
156
+ group: file.path.split('/')[0] || 'root',
157
+ };
158
+ nodes.push(node);
159
+ nodeMap.set(node.id, node);
160
+ // 添加文件到符号的边
161
+ edges.push({
162
+ id: `edge:file:${file.path}->symbol:${file.path}:${symbol.name}`,
163
+ source: `file:${file.path}`,
164
+ target: `symbol:${file.path}:${symbol.name}`,
165
+ type: 'import',
166
+ weight: 1,
167
+ });
168
+ }
169
+ // 创建调用边
170
+ for (const edge of callEdges.slice(0, options.maxEdges)) {
171
+ const sourceId = edge.caller_symbol_id
172
+ ? `symbol:${edge.caller_file}:${edge.caller_name}`
173
+ : `file:${edge.caller_file}`;
174
+ const targetId = edge.callee_symbol_id
175
+ ? `symbol:${edge.callee_file}:${edge.callee_name}`
176
+ : `file:${edge.callee_file}`;
177
+ if (nodeMap.has(sourceId) && nodeMap.has(targetId)) {
178
+ edges.push({
179
+ id: `edge:${edge.id}`,
180
+ source: sourceId,
181
+ target: targetId,
182
+ type: edge.relation_type,
183
+ weight: 2,
184
+ });
185
+ }
186
+ }
187
+ return { nodes, edges };
188
+ }
189
+ /**
190
+ * 获取符号颜色
191
+ */
192
+ getSymbolColor(type) {
193
+ const colors = {
194
+ function: '#FF9800',
195
+ class: '#E91E63',
196
+ interface: '#9C27B0',
197
+ type: '#673AB7',
198
+ variable: '#607D8B',
199
+ };
200
+ return colors[type] || '#757575';
201
+ }
202
+ /**
203
+ * 生成 HTML
204
+ */
205
+ generateHtml(nodes, edges, options) {
206
+ const theme = options.theme === 'dark' ? 'dark' : 'light';
207
+ return `<!DOCTYPE html>
208
+ <html lang="en">
209
+ <head>
210
+ <meta charset="UTF-8">
211
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
212
+ <title>TrenchCraft Graph Visualization</title>
213
+ <style>
214
+ * {
215
+ margin: 0;
216
+ padding: 0;
217
+ box-sizing: border-box;
218
+ }
219
+
220
+ body {
221
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
222
+ background: ${theme === 'dark' ? '#1a1a1a' : '#ffffff'};
223
+ color: ${theme === 'dark' ? '#ffffff' : '#333333'};
224
+ }
225
+
226
+ #container {
227
+ width: 100vw;
228
+ height: 100vh;
229
+ position: relative;
230
+ }
231
+
232
+ #graph {
233
+ width: 100%;
234
+ height: 100%;
235
+ }
236
+
237
+ #sidebar {
238
+ position: absolute;
239
+ top: 0;
240
+ right: 0;
241
+ width: 300px;
242
+ height: 100%;
243
+ background: ${theme === 'dark' ? '#2d2d2d' : '#f5f5f5'};
244
+ border-left: 1px solid ${theme === 'dark' ? '#444' : '#ddd'};
245
+ padding: 20px;
246
+ overflow-y: auto;
247
+ }
248
+
249
+ #sidebar h2 {
250
+ margin-bottom: 20px;
251
+ font-size: 18px;
252
+ }
253
+
254
+ #sidebar .stat {
255
+ margin-bottom: 10px;
256
+ padding: 10px;
257
+ background: ${theme === 'dark' ? '#3d3d3d' : '#ffffff'};
258
+ border-radius: 5px;
259
+ }
260
+
261
+ #sidebar .stat label {
262
+ font-weight: bold;
263
+ display: block;
264
+ margin-bottom: 5px;
265
+ }
266
+
267
+ #sidebar .stat value {
268
+ font-size: 24px;
269
+ color: #2196F3;
270
+ }
271
+
272
+ #legend {
273
+ margin-top: 20px;
274
+ }
275
+
276
+ #legend h3 {
277
+ margin-bottom: 10px;
278
+ }
279
+
280
+ #legend .item {
281
+ display: flex;
282
+ align-items: center;
283
+ margin-bottom: 5px;
284
+ }
285
+
286
+ #legend .item .color {
287
+ width: 20px;
288
+ height: 20px;
289
+ border-radius: 50%;
290
+ margin-right: 10px;
291
+ }
292
+
293
+ #legend .item .label {
294
+ font-size: 14px;
295
+ }
296
+
297
+ #controls {
298
+ position: absolute;
299
+ top: 20px;
300
+ left: 20px;
301
+ display: flex;
302
+ gap: 10px;
303
+ flex-wrap: wrap;
304
+ }
305
+
306
+ #controls button {
307
+ padding: 10px 15px;
308
+ background: ${theme === 'dark' ? '#3d3d3d' : '#ffffff'};
309
+ border: 1px solid ${theme === 'dark' ? '#555' : '#ddd'};
310
+ border-radius: 5px;
311
+ cursor: pointer;
312
+ color: ${theme === 'dark' ? '#ffffff' : '#333333'};
313
+ }
314
+
315
+ #controls button:hover {
316
+ background: ${theme === 'dark' ? '#4d4d4d' : '#f0f0f0'};
317
+ }
318
+
319
+ #controls button.active {
320
+ background: #2196F3;
321
+ color: white;
322
+ border-color: #2196F3;
323
+ }
324
+
325
+ #search-container {
326
+ position: absolute;
327
+ top: 20px;
328
+ left: 50%;
329
+ transform: translateX(-50%);
330
+ display: flex;
331
+ gap: 10px;
332
+ align-items: center;
333
+ }
334
+
335
+ #search-input {
336
+ padding: 10px 15px;
337
+ width: 300px;
338
+ background: ${theme === 'dark' ? '#3d3d3d' : '#ffffff'};
339
+ border: 1px solid ${theme === 'dark' ? '#555' : '#ddd'};
340
+ border-radius: 5px;
341
+ color: ${theme === 'dark' ? '#ffffff' : '#333333'};
342
+ font-size: 14px;
343
+ }
344
+
345
+ #search-results {
346
+ position: absolute;
347
+ top: 100%;
348
+ left: 0;
349
+ width: 100%;
350
+ max-height: 300px;
351
+ overflow-y: auto;
352
+ background: ${theme === 'dark' ? '#3d3d3d' : '#ffffff'};
353
+ border: 1px solid ${theme === 'dark' ? '#555' : '#ddd'};
354
+ border-radius: 5px;
355
+ display: none;
356
+ z-index: 1000;
357
+ }
358
+
359
+ #search-results .result-item {
360
+ padding: 10px 15px;
361
+ cursor: pointer;
362
+ border-bottom: 1px solid ${theme === 'dark' ? '#444' : '#eee'};
363
+ }
364
+
365
+ #search-results .result-item:hover {
366
+ background: ${theme === 'dark' ? '#4d4d4d' : '#f0f0f0'};
367
+ }
368
+
369
+ #search-results .result-item .name {
370
+ font-weight: bold;
371
+ margin-bottom: 5px;
372
+ }
373
+
374
+ #search-results .result-item .path {
375
+ font-size: 12px;
376
+ color: ${theme === 'dark' ? '#ccc' : '#666'};
377
+ }
378
+
379
+ #filter-container {
380
+ position: absolute;
381
+ top: 70px;
382
+ left: 20px;
383
+ display: flex;
384
+ gap: 10px;
385
+ flex-wrap: wrap;
386
+ }
387
+
388
+ #filter-container .filter-group {
389
+ display: flex;
390
+ gap: 5px;
391
+ align-items: center;
392
+ }
393
+
394
+ #filter-container .filter-group label {
395
+ font-size: 12px;
396
+ color: ${theme === 'dark' ? '#ccc' : '#666'};
397
+ }
398
+
399
+ #filter-container .filter-group select {
400
+ padding: 5px 10px;
401
+ background: ${theme === 'dark' ? '#3d3d3d' : '#ffffff'};
402
+ border: 1px solid ${theme === 'dark' ? '#555' : '#ddd'};
403
+ border-radius: 5px;
404
+ color: ${theme === 'dark' ? '#ffffff' : '#333333'};
405
+ font-size: 12px;
406
+ }
407
+
408
+ #path-highlight {
409
+ position: absolute;
410
+ bottom: 20px;
411
+ left: 20px;
412
+ padding: 10px 15px;
413
+ background: ${theme === 'dark' ? '#3d3d3d' : '#ffffff'};
414
+ border: 1px solid ${theme === 'dark' ? '#555' : '#ddd'};
415
+ border-radius: 5px;
416
+ display: none;
417
+ max-width: 400px;
418
+ }
419
+
420
+ #path-highlight h4 {
421
+ margin-bottom: 10px;
422
+ font-size: 14px;
423
+ }
424
+
425
+ #path-highlight .path-list {
426
+ font-size: 12px;
427
+ color: ${theme === 'dark' ? '#ccc' : '#666'};
428
+ }
429
+
430
+ #path-highlight .path-list .path-item {
431
+ margin-bottom: 5px;
432
+ padding: 5px;
433
+ background: ${theme === 'dark' ? '#4d4d4d' : '#f0f0f0'};
434
+ border-radius: 3px;
435
+ }
436
+
437
+ #tooltip {
438
+ position: absolute;
439
+ display: none;
440
+ background: ${theme === 'dark' ? '#3d3d3d' : '#ffffff'};
441
+ border: 1px solid ${theme === 'dark' ? '#555' : '#ddd'};
442
+ border-radius: 5px;
443
+ padding: 10px;
444
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
445
+ max-width: 300px;
446
+ }
447
+
448
+ #tooltip h4 {
449
+ margin-bottom: 5px;
450
+ }
451
+
452
+ #tooltip p {
453
+ font-size: 14px;
454
+ color: ${theme === 'dark' ? '#ccc' : '#666'};
455
+ }
456
+ </style>
457
+ </head>
458
+ <body>
459
+ <div id="container">
460
+ <div id="graph"></div>
461
+ <div id="sidebar">
462
+ <h2>Graph Statistics</h2>
463
+ <div class="stat">
464
+ <label>Nodes</label>
465
+ <value>${nodes.length}</value>
466
+ </div>
467
+ <div class="stat">
468
+ <label>Edges</label>
469
+ <value>${edges.length}</value>
470
+ </div>
471
+ <div id="legend">
472
+ <h3>Legend</h3>
473
+ <div class="item">
474
+ <div class="color" style="background: #4CAF50;"></div>
475
+ <div class="label">Module</div>
476
+ </div>
477
+ <div class="item">
478
+ <div class="color" style="background: #2196F3;"></div>
479
+ <div class="label">File</div>
480
+ </div>
481
+ <div class="item">
482
+ <div class="color" style="background: #FF9800;"></div>
483
+ <div class="label">Function</div>
484
+ </div>
485
+ <div class="item">
486
+ <div class="color" style="background: #E91E63;"></div>
487
+ <div class="label">Class</div>
488
+ </div>
489
+ <div class="item">
490
+ <div class="color" style="background: #9C27B0;"></div>
491
+ <div class="label">Interface</div>
492
+ </div>
493
+ </div>
494
+ <div id="selected-info" style="margin-top: 20px; display: none;">
495
+ <h3>Selected Node</h3>
496
+ <div id="selected-details"></div>
497
+ </div>
498
+ </div>
499
+ <div id="controls">
500
+ <button onclick="zoomIn()">Zoom In</button>
501
+ <button onclick="zoomOut()">Zoom Out</button>
502
+ <button onclick="resetView()">Reset</button>
503
+ <button onclick="togglePhysics()">Physics</button>
504
+ <button onclick="highlightAll()">Highlight All</button>
505
+ <button onclick="clearHighlight()">Clear Highlight</button>
506
+ </div>
507
+ <div id="search-container">
508
+ <input type="text" id="search-input" placeholder="Search nodes..." oninput="searchNodes(this.value)">
509
+ <div id="search-results"></div>
510
+ </div>
511
+ <div id="filter-container">
512
+ <div class="filter-group">
513
+ <label>Type:</label>
514
+ <select id="type-filter" onchange="filterByType(this.value)">
515
+ <option value="all">All</option>
516
+ <option value="module">Module</option>
517
+ <option value="file">File</option>
518
+ <option value="function">Function</option>
519
+ <option value="class">Class</option>
520
+ <option value="interface">Interface</option>
521
+ </select>
522
+ </div>
523
+ <div class="filter-group">
524
+ <label>Group:</label>
525
+ <select id="group-filter" onchange="filterByGroup(this.value)">
526
+ <option value="all">All</option>
527
+ ${Array.from(new Set(nodes.map(n => n.group))).map(g => `<option value="${g}">${g}</option>`).join('\n ')}
528
+ </select>
529
+ </div>
530
+ </div>
531
+ <div id="path-highlight">
532
+ <h4>Highlighted Path</h4>
533
+ <div id="path-list" class="path-list"></div>
534
+ </div>
535
+ <div id="tooltip">
536
+ <h4 id="tooltip-title"></h4>
537
+ <p id="tooltip-content"></p>
538
+ </div>
539
+ </div>
540
+
541
+ <script src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
542
+ <script>
543
+ const nodes = ${JSON.stringify(nodes)};
544
+ const edges = ${JSON.stringify(edges)};
545
+
546
+ const container = document.getElementById('graph');
547
+ const data = {
548
+ nodes: new vis.DataSet(nodes.map(n => ({
549
+ ...n,
550
+ shape: n.type === 'module' ? 'diamond' : n.type === 'file' ? 'dot' : 'triangle',
551
+ font: { color: '${theme === 'dark' ? '#fff' : '#333'}' }
552
+ }))),
553
+ edges: new vis.DataSet(edges.map(e => ({
554
+ ...e,
555
+ arrows: 'to',
556
+ color: { color: '${theme === 'dark' ? '#666' : '#999'}', highlight: '#2196F3' }
557
+ })))
558
+ };
559
+
560
+ const options = {
561
+ nodes: {
562
+ borderWidth: 2,
563
+ shadow: true
564
+ },
565
+ edges: {
566
+ smooth: {
567
+ type: 'continuous'
568
+ }
569
+ },
570
+ physics: {
571
+ enabled: true,
572
+ solver: 'forceAtlas2Based',
573
+ forceAtlas2Based: {
574
+ gravitationalConstant: -100,
575
+ centralGravity: 0.01,
576
+ springLength: 100,
577
+ springConstant: 0.08
578
+ },
579
+ stabilization: {
580
+ iterations: 100
581
+ }
582
+ },
583
+ interaction: {
584
+ hover: true,
585
+ tooltipDelay: 200
586
+ }
587
+ };
588
+
589
+ const network = new vis.Network(container, data, options);
590
+
591
+ // 工具提示
592
+ const tooltip = document.getElementById('tooltip');
593
+ const tooltipTitle = document.getElementById('tooltip-title');
594
+ const tooltipContent = document.getElementById('tooltip-content');
595
+
596
+ network.on('hoverNode', function(params) {
597
+ const node = nodes.find(n => n.id === params.node);
598
+ if (node) {
599
+ tooltipTitle.textContent = node.label;
600
+ tooltipContent.textContent = node.path ? \`\${node.path}\${node.line ? ':' + node.line : ''}\` : node.type;
601
+ tooltip.style.display = 'block';
602
+ tooltip.style.left = params.event.center.x + 10 + 'px';
603
+ tooltip.style.top = params.event.center.y + 10 + 'px';
604
+ }
605
+ });
606
+
607
+ network.on('blurNode', function() {
608
+ tooltip.style.display = 'none';
609
+ });
610
+
611
+ // 选择节点
612
+ network.on('selectNode', function(params) {
613
+ const nodeId = params.nodes[0];
614
+ const node = nodes.find(n => n.id === nodeId);
615
+
616
+ if (node) {
617
+ const selectedInfo = document.getElementById('selected-info');
618
+ const selectedDetails = document.getElementById('selected-details');
619
+
620
+ selectedDetails.innerHTML = \`
621
+ <div style="margin-bottom: 5px;"><strong>Name:</strong> \${node.label}</div>
622
+ <div style="margin-bottom: 5px;"><strong>Type:</strong> \${node.type}</div>
623
+ \${node.path ? \`<div style="margin-bottom: 5px;"><strong>Path:</strong> \${node.path}</div>\` : ''}
624
+ \${node.line ? \`<div style="margin-bottom: 5px;"><strong>Line:</strong> \${node.line}</div>\` : ''}
625
+ <div style="margin-bottom: 5px;"><strong>Group:</strong> \${node.group}</div>
626
+ \`;
627
+
628
+ selectedInfo.style.display = 'block';
629
+
630
+ // 高亮相关节点
631
+ highlightRelatedNodes(nodeId);
632
+ }
633
+ });
634
+
635
+ // 高亮相关节点
636
+ function highlightRelatedNodes(nodeId) {
637
+ const connectedNodes = network.getConnectedNodes(nodeId);
638
+ const allNodes = data.nodes.get();
639
+ const allEdges = data.edges.get();
640
+
641
+ // 更新节点样式
642
+ data.nodes.update(allNodes.map(n => ({
643
+ ...n,
644
+ opacity: connectedNodes.includes(n.id) || n.id === nodeId ? 1 : 0.3,
645
+ borderWidth: n.id === nodeId ? 4 : 2
646
+ })));
647
+
648
+ // 更新边样式
649
+ data.edges.update(allEdges.map(e => ({
650
+ ...e,
651
+ opacity: e.from === nodeId || e.to === nodeId ? 1 : 0.3,
652
+ width: e.from === nodeId || e.to === nodeId ? 3 : 1
653
+ })));
654
+ }
655
+
656
+ // 搜索功能
657
+ function searchNodes(query) {
658
+ const resultsContainer = document.getElementById('search-results');
659
+
660
+ if (!query) {
661
+ resultsContainer.style.display = 'none';
662
+ return;
663
+ }
664
+
665
+ const results = nodes.filter(n =>
666
+ n.label.toLowerCase().includes(query.toLowerCase()) ||
667
+ (n.path && n.path.toLowerCase().includes(query.toLowerCase()))
668
+ ).slice(0, 10);
669
+
670
+ if (results.length === 0) {
671
+ resultsContainer.style.display = 'none';
672
+ return;
673
+ }
674
+
675
+ resultsContainer.innerHTML = results.map(n => \`
676
+ <div class="result-item" onclick="selectNode('\${n.id}')">
677
+ <div class="name">\${n.label}</div>
678
+ <div class="path">\${n.path || n.type}</div>
679
+ </div>
680
+ \`).join('');
681
+
682
+ resultsContainer.style.display = 'block';
683
+ }
684
+
685
+ // 选择节点
686
+ function selectNode(nodeId) {
687
+ network.selectNodes([nodeId]);
688
+ network.focus(nodeId, { scale: 1.5, animation: true });
689
+
690
+ const resultsContainer = document.getElementById('search-results');
691
+ resultsContainer.style.display = 'none';
692
+
693
+ const searchInput = document.getElementById('search-input');
694
+ searchInput.value = '';
695
+
696
+ highlightRelatedNodes(nodeId);
697
+ }
698
+
699
+ // 按类型过滤
700
+ function filterByType(type) {
701
+ const allNodes = data.nodes.get();
702
+ const allEdges = data.edges.get();
703
+
704
+ if (type === 'all') {
705
+ data.nodes.update(allNodes.map(n => ({ ...n, hidden: false })));
706
+ data.edges.update(allEdges.map(e => ({ ...e, hidden: false })));
707
+ } else {
708
+ const filteredNodes = allNodes.filter(n => n.type === type);
709
+ const filteredNodeIds = filteredNodes.map(n => n.id);
710
+
711
+ data.nodes.update(allNodes.map(n => ({
712
+ ...n,
713
+ hidden: !filteredNodeIds.includes(n.id)
714
+ })));
715
+
716
+ data.edges.update(allEdges.map(e => ({
717
+ ...e,
718
+ hidden: !filteredNodeIds.includes(e.from) || !filteredNodeIds.includes(e.to)
719
+ })));
720
+ }
721
+ }
722
+
723
+ // 按组过滤
724
+ function filterByGroup(group) {
725
+ const allNodes = data.nodes.get();
726
+ const allEdges = data.edges.get();
727
+
728
+ if (group === 'all') {
729
+ data.nodes.update(allNodes.map(n => ({ ...n, hidden: false })));
730
+ data.edges.update(allEdges.map(e => ({ ...e, hidden: false })));
731
+ } else {
732
+ const filteredNodes = allNodes.filter(n => n.group === group);
733
+ const filteredNodeIds = filteredNodes.map(n => n.id);
734
+
735
+ data.nodes.update(allNodes.map(n => ({
736
+ ...n,
737
+ hidden: !filteredNodeIds.includes(n.id)
738
+ })));
739
+
740
+ data.edges.update(allEdges.map(e => ({
741
+ ...e,
742
+ hidden: !filteredNodeIds.includes(e.from) || !filteredNodeIds.includes(e.to)
743
+ })));
744
+ }
745
+ }
746
+
747
+ // 高亮所有节点
748
+ function highlightAll() {
749
+ const allNodes = data.nodes.get();
750
+ const allEdges = data.edges.get();
751
+
752
+ data.nodes.update(allNodes.map(n => ({
753
+ ...n,
754
+ opacity: 1,
755
+ borderWidth: 2
756
+ })));
757
+
758
+ data.edges.update(allEdges.map(e => ({
759
+ ...e,
760
+ opacity: 1,
761
+ width: 1
762
+ })));
763
+ }
764
+
765
+ // 清除高亮
766
+ function clearHighlight() {
767
+ const allNodes = data.nodes.get();
768
+ const allEdges = data.edges.get();
769
+
770
+ data.nodes.update(allNodes.map(n => ({
771
+ ...n,
772
+ opacity: 1,
773
+ borderWidth: 2
774
+ })));
775
+
776
+ data.edges.update(allEdges.map(e => ({
777
+ ...e,
778
+ opacity: 1,
779
+ width: 1
780
+ })));
781
+
782
+ document.getElementById('selected-info').style.display = 'none';
783
+ document.getElementById('path-highlight').style.display = 'none';
784
+ }
785
+
786
+ // 控制函数
787
+ function zoomIn() {
788
+ network.moveTo({ scale: network.getScale() * 1.2 });
789
+ }
790
+
791
+ function zoomOut() {
792
+ network.moveTo({ scale: network.getScale() / 1.2 });
793
+ }
794
+
795
+ function resetView() {
796
+ network.fit();
797
+ }
798
+
799
+ let physicsEnabled = true;
800
+ function togglePhysics() {
801
+ physicsEnabled = !physicsEnabled;
802
+ network.setOptions({ physics: { enabled: physicsEnabled } });
803
+
804
+ const button = document.querySelector('button[onclick="togglePhysics()"]');
805
+ if (physicsEnabled) {
806
+ button.classList.add('active');
807
+ } else {
808
+ button.classList.remove('active');
809
+ }
810
+ }
811
+
812
+ // 初始化物理引擎按钮状态
813
+ document.querySelector('button[onclick="togglePhysics()"]').classList.add('active');
814
+ </script>
815
+ </body>
816
+ </html>`;
817
+ }
818
+ /**
819
+ * 生成 SVG
820
+ */
821
+ generateSvg(nodes, edges, options) {
822
+ const width = 1200;
823
+ const height = 800;
824
+ const padding = 50;
825
+ // 简单的圆形布局
826
+ const centerX = width / 2;
827
+ const centerY = height / 2;
828
+ const radius = Math.min(width, height) / 2 - padding;
829
+ const nodePositions = new Map();
830
+ nodes.forEach((node, i) => {
831
+ const angle = (2 * Math.PI * i) / nodes.length;
832
+ const x = centerX + radius * Math.cos(angle);
833
+ const y = centerY + radius * Math.sin(angle);
834
+ nodePositions.set(node.id, { x, y });
835
+ });
836
+ let svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
837
+ <style>
838
+ text { font-family: Arial, sans-serif; font-size: 12px; }
839
+ .node { cursor: pointer; }
840
+ .edge { stroke-opacity: 0.6; }
841
+ </style>
842
+ <rect width="100%" height="100%" fill="${options.theme === 'dark' ? '#1a1a1a' : '#ffffff'}"/>
843
+ `;
844
+ // 绘制边
845
+ for (const edge of edges) {
846
+ const sourcePos = nodePositions.get(edge.source);
847
+ const targetPos = nodePositions.get(edge.target);
848
+ if (sourcePos && targetPos) {
849
+ svg += ` <line class="edge" x1="${sourcePos.x}" y1="${sourcePos.y}" x2="${targetPos.x}" y2="${targetPos.y}" stroke="${options.theme === 'dark' ? '#666' : '#999'}" stroke-width="${edge.weight}"/>
850
+ `;
851
+ }
852
+ }
853
+ // 绘制节点
854
+ for (const node of nodes) {
855
+ const pos = nodePositions.get(node.id);
856
+ if (!pos)
857
+ continue;
858
+ const nodeSize = node.size / 2;
859
+ svg += ` <g class="node" transform="translate(${pos.x}, ${pos.y})">
860
+ <circle r="${nodeSize}" fill="${node.color}" stroke="${options.theme === 'dark' ? '#fff' : '#333'}" stroke-width="1"/>
861
+ <text text-anchor="middle" dy="4" fill="${options.theme === 'dark' ? '#fff' : '#333'}">${node.label}</text>
862
+ </g>
863
+ `;
864
+ }
865
+ svg += `</svg>`;
866
+ return svg;
867
+ }
868
+ }
869
+ //# sourceMappingURL=visualizer.js.map