spec-gen-cli 1.1.0 → 1.2.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 (205) hide show
  1. package/README.md +163 -77
  2. package/dist/api/analyze.d.ts.map +1 -1
  3. package/dist/api/analyze.js +56 -27
  4. package/dist/api/analyze.js.map +1 -1
  5. package/dist/api/drift.d.ts.map +1 -1
  6. package/dist/api/drift.js +26 -20
  7. package/dist/api/drift.js.map +1 -1
  8. package/dist/api/generate.d.ts.map +1 -1
  9. package/dist/api/generate.js +19 -43
  10. package/dist/api/generate.js.map +1 -1
  11. package/dist/api/init.d.ts.map +1 -1
  12. package/dist/api/init.js +6 -5
  13. package/dist/api/init.js.map +1 -1
  14. package/dist/api/run.d.ts.map +1 -1
  15. package/dist/api/run.js +67 -51
  16. package/dist/api/run.js.map +1 -1
  17. package/dist/api/specs.d.ts.map +1 -1
  18. package/dist/api/specs.js +5 -4
  19. package/dist/api/specs.js.map +1 -1
  20. package/dist/api/types.d.ts +7 -1
  21. package/dist/api/types.d.ts.map +1 -1
  22. package/dist/api/verify.d.ts.map +1 -1
  23. package/dist/api/verify.js +31 -32
  24. package/dist/api/verify.js.map +1 -1
  25. package/dist/cli/commands/analyze.d.ts.map +1 -1
  26. package/dist/cli/commands/analyze.js +41 -62
  27. package/dist/cli/commands/analyze.js.map +1 -1
  28. package/dist/cli/commands/doctor.d.ts +9 -0
  29. package/dist/cli/commands/doctor.d.ts.map +1 -0
  30. package/dist/cli/commands/doctor.js +273 -0
  31. package/dist/cli/commands/doctor.js.map +1 -0
  32. package/dist/cli/commands/drift.d.ts.map +1 -1
  33. package/dist/cli/commands/drift.js +18 -32
  34. package/dist/cli/commands/drift.js.map +1 -1
  35. package/dist/cli/commands/generate.d.ts.map +1 -1
  36. package/dist/cli/commands/generate.js +40 -101
  37. package/dist/cli/commands/generate.js.map +1 -1
  38. package/dist/cli/commands/init.d.ts.map +1 -1
  39. package/dist/cli/commands/init.js +17 -15
  40. package/dist/cli/commands/init.js.map +1 -1
  41. package/dist/cli/commands/mcp.d.ts.map +1 -1
  42. package/dist/cli/commands/mcp.js +2 -1
  43. package/dist/cli/commands/mcp.js.map +1 -1
  44. package/dist/cli/commands/run.d.ts +0 -15
  45. package/dist/cli/commands/run.d.ts.map +1 -1
  46. package/dist/cli/commands/run.js +61 -111
  47. package/dist/cli/commands/run.js.map +1 -1
  48. package/dist/cli/commands/verify.d.ts.map +1 -1
  49. package/dist/cli/commands/verify.js +18 -52
  50. package/dist/cli/commands/verify.js.map +1 -1
  51. package/dist/cli/commands/view.d.ts +4 -0
  52. package/dist/cli/commands/view.d.ts.map +1 -1
  53. package/dist/cli/commands/view.js +29 -24
  54. package/dist/cli/commands/view.js.map +1 -1
  55. package/dist/cli/index.js +22 -4
  56. package/dist/cli/index.js.map +1 -1
  57. package/dist/constants.d.ts +254 -0
  58. package/dist/constants.d.ts.map +1 -0
  59. package/dist/constants.js +320 -0
  60. package/dist/constants.js.map +1 -0
  61. package/dist/core/analyzer/artifact-generator.d.ts +8 -0
  62. package/dist/core/analyzer/artifact-generator.d.ts.map +1 -1
  63. package/dist/core/analyzer/artifact-generator.js +59 -11
  64. package/dist/core/analyzer/artifact-generator.js.map +1 -1
  65. package/dist/core/analyzer/call-graph.d.ts.map +1 -1
  66. package/dist/core/analyzer/call-graph.js +135 -1
  67. package/dist/core/analyzer/call-graph.js.map +1 -1
  68. package/dist/core/analyzer/dependency-graph.d.ts +32 -0
  69. package/dist/core/analyzer/dependency-graph.d.ts.map +1 -1
  70. package/dist/core/analyzer/dependency-graph.js +104 -10
  71. package/dist/core/analyzer/dependency-graph.js.map +1 -1
  72. package/dist/core/analyzer/duplicate-detector.d.ts.map +1 -1
  73. package/dist/core/analyzer/duplicate-detector.js +4 -0
  74. package/dist/core/analyzer/duplicate-detector.js.map +1 -1
  75. package/dist/core/analyzer/embedding-service.d.ts +6 -0
  76. package/dist/core/analyzer/embedding-service.d.ts.map +1 -1
  77. package/dist/core/analyzer/embedding-service.js +15 -1
  78. package/dist/core/analyzer/embedding-service.js.map +1 -1
  79. package/dist/core/analyzer/file-walker.d.ts.map +1 -1
  80. package/dist/core/analyzer/file-walker.js +4 -3
  81. package/dist/core/analyzer/file-walker.js.map +1 -1
  82. package/dist/core/analyzer/http-route-parser.d.ts +111 -0
  83. package/dist/core/analyzer/http-route-parser.d.ts.map +1 -0
  84. package/dist/core/analyzer/http-route-parser.js +466 -0
  85. package/dist/core/analyzer/http-route-parser.js.map +1 -0
  86. package/dist/core/analyzer/import-parser.d.ts.map +1 -1
  87. package/dist/core/analyzer/import-parser.js +19 -5
  88. package/dist/core/analyzer/import-parser.js.map +1 -1
  89. package/dist/core/analyzer/refactor-analyzer.d.ts.map +1 -1
  90. package/dist/core/analyzer/refactor-analyzer.js +8 -7
  91. package/dist/core/analyzer/refactor-analyzer.js.map +1 -1
  92. package/dist/core/analyzer/repository-mapper.d.ts.map +1 -1
  93. package/dist/core/analyzer/repository-mapper.js +12 -13
  94. package/dist/core/analyzer/repository-mapper.js.map +1 -1
  95. package/dist/core/analyzer/signature-extractor.d.ts +1 -1
  96. package/dist/core/analyzer/signature-extractor.d.ts.map +1 -1
  97. package/dist/core/analyzer/signature-extractor.js +69 -1
  98. package/dist/core/analyzer/signature-extractor.js.map +1 -1
  99. package/dist/core/analyzer/spec-vector-index.d.ts.map +1 -1
  100. package/dist/core/analyzer/spec-vector-index.js +4 -3
  101. package/dist/core/analyzer/spec-vector-index.js.map +1 -1
  102. package/dist/core/analyzer/vector-index.d.ts.map +1 -1
  103. package/dist/core/analyzer/vector-index.js +29 -1
  104. package/dist/core/analyzer/vector-index.js.map +1 -1
  105. package/dist/core/drift/drift-detector.d.ts.map +1 -1
  106. package/dist/core/drift/drift-detector.js +7 -6
  107. package/dist/core/drift/drift-detector.js.map +1 -1
  108. package/dist/core/drift/git-diff.d.ts.map +1 -1
  109. package/dist/core/drift/git-diff.js +28 -16
  110. package/dist/core/drift/git-diff.js.map +1 -1
  111. package/dist/core/generator/mapping-generator.d.ts.map +1 -1
  112. package/dist/core/generator/mapping-generator.js +11 -10
  113. package/dist/core/generator/mapping-generator.js.map +1 -1
  114. package/dist/core/generator/openspec-compat.d.ts.map +1 -1
  115. package/dist/core/generator/openspec-compat.js +3 -2
  116. package/dist/core/generator/openspec-compat.js.map +1 -1
  117. package/dist/core/generator/openspec-format-generator.js.map +1 -1
  118. package/dist/core/generator/openspec-writer.d.ts +0 -4
  119. package/dist/core/generator/openspec-writer.d.ts.map +1 -1
  120. package/dist/core/generator/openspec-writer.js +30 -41
  121. package/dist/core/generator/openspec-writer.js.map +1 -1
  122. package/dist/core/generator/spec-pipeline.d.ts.map +1 -1
  123. package/dist/core/generator/spec-pipeline.js +4 -4
  124. package/dist/core/generator/spec-pipeline.js.map +1 -1
  125. package/dist/core/generator/stages/stage1-survey.d.ts.map +1 -1
  126. package/dist/core/generator/stages/stage1-survey.js +5 -3
  127. package/dist/core/generator/stages/stage1-survey.js.map +1 -1
  128. package/dist/core/generator/stages/stage2-entities.d.ts.map +1 -1
  129. package/dist/core/generator/stages/stage2-entities.js +10 -9
  130. package/dist/core/generator/stages/stage2-entities.js.map +1 -1
  131. package/dist/core/generator/stages/stage3-services.d.ts.map +1 -1
  132. package/dist/core/generator/stages/stage3-services.js +9 -8
  133. package/dist/core/generator/stages/stage3-services.js.map +1 -1
  134. package/dist/core/generator/stages/stage4-api.d.ts.map +1 -1
  135. package/dist/core/generator/stages/stage4-api.js +10 -9
  136. package/dist/core/generator/stages/stage4-api.js.map +1 -1
  137. package/dist/core/generator/stages/stage5-architecture.d.ts.map +1 -1
  138. package/dist/core/generator/stages/stage5-architecture.js +5 -4
  139. package/dist/core/generator/stages/stage5-architecture.js.map +1 -1
  140. package/dist/core/generator/stages/stage6-adr.d.ts.map +1 -1
  141. package/dist/core/generator/stages/stage6-adr.js +7 -2
  142. package/dist/core/generator/stages/stage6-adr.js.map +1 -1
  143. package/dist/core/services/chat-agent.d.ts.map +1 -1
  144. package/dist/core/services/chat-agent.js +46 -15
  145. package/dist/core/services/chat-agent.js.map +1 -1
  146. package/dist/core/services/config-manager.d.ts.map +1 -1
  147. package/dist/core/services/config-manager.js +32 -26
  148. package/dist/core/services/config-manager.js.map +1 -1
  149. package/dist/core/services/gitignore-manager.d.ts.map +1 -1
  150. package/dist/core/services/gitignore-manager.js +2 -13
  151. package/dist/core/services/gitignore-manager.js.map +1 -1
  152. package/dist/core/services/llm-service.d.ts.map +1 -1
  153. package/dist/core/services/llm-service.js +33 -35
  154. package/dist/core/services/llm-service.js.map +1 -1
  155. package/dist/core/services/mcp-handlers/analysis.d.ts.map +1 -1
  156. package/dist/core/services/mcp-handlers/analysis.js +23 -14
  157. package/dist/core/services/mcp-handlers/analysis.js.map +1 -1
  158. package/dist/core/services/mcp-handlers/graph.d.ts.map +1 -1
  159. package/dist/core/services/mcp-handlers/graph.js +24 -23
  160. package/dist/core/services/mcp-handlers/graph.js.map +1 -1
  161. package/dist/core/services/mcp-handlers/semantic.d.ts +1 -1
  162. package/dist/core/services/mcp-handlers/semantic.d.ts.map +1 -1
  163. package/dist/core/services/mcp-handlers/semantic.js +17 -16
  164. package/dist/core/services/mcp-handlers/semantic.js.map +1 -1
  165. package/dist/core/services/mcp-handlers/utils.d.ts.map +1 -1
  166. package/dist/core/services/mcp-handlers/utils.js +4 -3
  167. package/dist/core/services/mcp-handlers/utils.js.map +1 -1
  168. package/dist/core/services/project-detector.d.ts.map +1 -1
  169. package/dist/core/services/project-detector.js +2 -13
  170. package/dist/core/services/project-detector.js.map +1 -1
  171. package/dist/core/verifier/verification-engine.d.ts +9 -3
  172. package/dist/core/verifier/verification-engine.d.ts.map +1 -1
  173. package/dist/core/verifier/verification-engine.js +25 -13
  174. package/dist/core/verifier/verification-engine.js.map +1 -1
  175. package/dist/types/index.d.ts +2 -0
  176. package/dist/types/index.d.ts.map +1 -1
  177. package/dist/utils/command-helpers.d.ts +38 -0
  178. package/dist/utils/command-helpers.d.ts.map +1 -0
  179. package/dist/utils/command-helpers.js +82 -0
  180. package/dist/utils/command-helpers.js.map +1 -0
  181. package/dist/utils/errors.d.ts.map +1 -1
  182. package/dist/utils/errors.js +4 -3
  183. package/dist/utils/errors.js.map +1 -1
  184. package/dist/utils/logger.d.ts.map +1 -1
  185. package/dist/utils/logger.js +14 -3
  186. package/dist/utils/logger.js.map +1 -1
  187. package/dist/utils/progress.d.ts +1 -1
  188. package/dist/utils/progress.d.ts.map +1 -1
  189. package/dist/utils/progress.js +15 -12
  190. package/dist/utils/progress.js.map +1 -1
  191. package/dist/utils/shutdown.d.ts.map +1 -1
  192. package/dist/utils/shutdown.js +4 -3
  193. package/dist/utils/shutdown.js.map +1 -1
  194. package/package.json +9 -5
  195. package/src/viewer/InteractiveGraphViewer.jsx +182 -139
  196. package/src/viewer/components/ArchitectureView.jsx +19 -19
  197. package/src/viewer/components/ChatPanel.jsx +40 -40
  198. package/src/viewer/components/ClusterGraph.jsx +34 -22
  199. package/src/viewer/components/FilterBar.jsx +26 -26
  200. package/src/viewer/components/FlatGraph.jsx +22 -15
  201. package/src/viewer/components/MicroComponents.jsx +14 -12
  202. package/src/viewer/utils/constants.js +17 -0
  203. package/src/viewer/utils/graph-helpers.js +7 -3
  204. package/src/viewer/utils/graph-helpers.test.ts +39 -0
  205. package/src/viewer/utils/themes.js +170 -0
@@ -1,12 +1,12 @@
1
- function FToggle({ active, onChange, label, badge, activeColor = '#7c6af7' }) {
1
+ function FToggle({ active, onChange, label, badge, activeColor = 'var(--ac-primary)' }) {
2
2
  return (
3
3
  <button
4
4
  onClick={() => onChange(!active)}
5
5
  style={{
6
6
  background: active ? `${activeColor}1a` : 'transparent',
7
- border: `1px solid ${active ? activeColor : '#1a1f38'}`,
7
+ border: `1px solid ${active ? activeColor : 'var(--bd-muted)'}`,
8
8
  borderRadius: 4,
9
- color: active ? activeColor : '#3a3f5c',
9
+ color: active ? activeColor : 'var(--tx-ghost)',
10
10
  padding: '2px 8px',
11
11
  cursor: 'pointer',
12
12
  fontSize: 9,
@@ -20,10 +20,10 @@ function FToggle({ active, onChange, label, badge, activeColor = '#7c6af7' }) {
20
20
  {badge !== undefined && (
21
21
  <span
22
22
  style={{
23
- background: '#0d0f22',
23
+ background: 'var(--bg-input)',
24
24
  borderRadius: 3,
25
25
  padding: '0 4px',
26
- color: '#3a3f5c',
26
+ color: 'var(--tx-ghost)',
27
27
  fontSize: 8,
28
28
  }}
29
29
  >
@@ -49,13 +49,13 @@ export function FilterBar({ filters, setFilters, stats, clusterNames }) {
49
49
  gap: 10,
50
50
  flexWrap: 'wrap',
51
51
  padding: '7px 18px',
52
- borderBottom: '1px solid #0f1224',
53
- background: '#07091a',
52
+ borderBottom: '1px solid var(--bd-faint)',
53
+ background: 'var(--bg-base)',
54
54
  flexShrink: 0,
55
55
  fontSize: 9,
56
56
  }}
57
57
  >
58
- <span style={{ color: '#2a2f4a', letterSpacing: '0.08em', fontWeight: 700 }}>FILTERS</span>
58
+ <span style={{ color: 'var(--tx-faint)', letterSpacing: '0.08em', fontWeight: 700 }}>FILTERS</span>
59
59
 
60
60
  <FToggle
61
61
  active={filters.hideOrphans}
@@ -74,7 +74,7 @@ export function FilterBar({ filters, setFilters, stats, clusterNames }) {
74
74
  />
75
75
 
76
76
  <div style={{ display: 'flex', alignItems: 'center', gap: 5 }}>
77
- <span style={{ color: '#2a2f4a' }}>{'Score >='}</span>
77
+ <span style={{ color: 'var(--tx-faint)' }}>Score ≥</span>
78
78
  <input
79
79
  type="range"
80
80
  min={0}
@@ -82,20 +82,20 @@ export function FilterBar({ filters, setFilters, stats, clusterNames }) {
82
82
  step={5}
83
83
  value={filters.minScore}
84
84
  onChange={(e) => setFilters((f) => ({ ...f, minScore: +e.target.value }))}
85
- style={{ width: 72, accentColor: '#7c6af7' }}
85
+ style={{ width: 72, accentColor: 'var(--ac-primary)' }}
86
86
  />
87
- <span style={{ color: '#7c6af7', minWidth: 16 }}>{filters.minScore}</span>
87
+ <span style={{ color: 'var(--ac-primary)', minWidth: 16 }}>{filters.minScore}</span>
88
88
  </div>
89
89
 
90
90
  <div style={{ display: 'flex', alignItems: 'center', gap: 5 }}>
91
- <span style={{ color: '#2a2f4a' }}>Top</span>
91
+ <span style={{ color: 'var(--tx-faint)' }}>Top</span>
92
92
  <select
93
93
  value={filters.topN}
94
94
  onChange={(e) => setFilters((f) => ({ ...f, topN: +e.target.value }))}
95
95
  style={{
96
- background: '#0d0f22',
97
- border: '1px solid #1a1f38',
98
- color: '#c8cde8',
96
+ background: 'var(--bg-input)',
97
+ border: '1px solid var(--bd-muted)',
98
+ color: 'var(--tx-primary)',
99
99
  borderRadius: 4,
100
100
  padding: '2px 5px',
101
101
  fontSize: 9,
@@ -108,18 +108,18 @@ export function FilterBar({ filters, setFilters, stats, clusterNames }) {
108
108
  </option>
109
109
  ))}
110
110
  </select>
111
- <span style={{ color: '#2a2f4a' }}>nodes</span>
111
+ <span style={{ color: 'var(--tx-faint)' }}>nodes</span>
112
112
  </div>
113
113
 
114
114
  <div style={{ display: 'flex', alignItems: 'center', gap: 5 }}>
115
- <span style={{ color: '#2a2f4a' }}>Cluster</span>
115
+ <span style={{ color: 'var(--tx-faint)' }}>Cluster</span>
116
116
  <select
117
117
  value={filters.cluster}
118
118
  onChange={(e) => setFilters((f) => ({ ...f, cluster: e.target.value }))}
119
119
  style={{
120
- background: '#0d0f22',
121
- border: '1px solid #1a1f38',
122
- color: '#c8cde8',
120
+ background: 'var(--bg-input)',
121
+ border: '1px solid var(--bd-muted)',
122
+ color: 'var(--tx-primary)',
123
123
  borderRadius: 4,
124
124
  padding: '2px 5px',
125
125
  fontSize: 9,
@@ -149,9 +149,9 @@ export function FilterBar({ filters, setFilters, stats, clusterNames }) {
149
149
  }
150
150
  style={{
151
151
  background: 'none',
152
- border: '1px solid #2a2f4a',
152
+ border: '1px solid var(--tx-faint)',
153
153
  borderRadius: 4,
154
- color: '#3a3f5c',
154
+ color: 'var(--tx-ghost)',
155
155
  padding: '2px 7px',
156
156
  cursor: 'pointer',
157
157
  fontSize: 9,
@@ -162,13 +162,13 @@ export function FilterBar({ filters, setFilters, stats, clusterNames }) {
162
162
  </button>
163
163
  )}
164
164
 
165
- <div style={{ marginLeft: 'auto', color: '#2a2f4a', display: 'flex', gap: 8 }}>
165
+ <div style={{ marginLeft: 'auto', color: 'var(--tx-faint)', display: 'flex', gap: 8 }}>
166
166
  <span>
167
- <span style={{ color: '#7c6af7' }}>{stats.visible}</span>/
168
- <span style={{ color: '#3a4060' }}>{stats.total}</span> nodes
167
+ <span style={{ color: 'var(--ac-primary)' }}>{stats.visible}</span>/
168
+ <span style={{ color: 'var(--tx-dim)' }}>{stats.total}</span> nodes
169
169
  </span>
170
170
  <span>
171
- <span style={{ color: '#3ecfcf' }}>{stats.visibleEdges}</span> edges
171
+ <span style={{ color: 'var(--ac-teal)' }}>{stats.visibleEdges}</span> edges
172
172
  </span>
173
173
  {stats.orphanCount > 0 && !filters.hideOrphans && (
174
174
  <span style={{ color: '#f77c6a66' }}>{stats.orphanCount} orphans</span>
@@ -12,6 +12,7 @@ export function FlatGraph({
12
12
  onSelect,
13
13
  refactorOnly,
14
14
  linkedIds,
15
+ noGlow,
15
16
  }) {
16
17
  const posRef = useRef(null);
17
18
  const prevKey = useRef(null);
@@ -68,16 +69,16 @@ export function FlatGraph({
68
69
  >
69
70
  <defs>
70
71
  <marker id="arr" markerWidth="6" markerHeight="6" refX="5" refY="2.5" orient="auto">
71
- <path d="M0,0 L0,5 L6,2.5z" fill="#1e2340" />
72
+ <path d="M0,0 L0,5 L6,2.5z" style={{ fill: 'var(--ac-arrow)' }} />
72
73
  </marker>
73
74
  <marker id="arr-sel" markerWidth="6" markerHeight="6" refX="5" refY="2.5" orient="auto">
74
- <path d="M0,0 L0,5 L6,2.5z" fill="#7c6af7" />
75
+ <path d="M0,0 L0,5 L6,2.5z" style={{ fill: 'var(--ac-primary)' }} />
75
76
  </marker>
76
77
  <marker id="arr-aff" markerWidth="6" markerHeight="6" refX="5" refY="2.5" orient="auto">
77
78
  <path d="M0,0 L0,5 L6,2.5z" fill="#f77c6a" />
78
79
  </marker>
79
80
  <marker id="arr-type" markerWidth="6" markerHeight="6" refX="5" refY="2.5" orient="auto">
80
- <path d="M0,0 L0,5 L6,2.5z" fill="#2a2f5a" />
81
+ <path d="M0,0 L0,5 L6,2.5z" style={{ fill: 'var(--ac-edge-type)' }} />
81
82
  </marker>
82
83
  <filter id="glow">
83
84
  <feGaussianBlur stdDeviation="4" result="b" />
@@ -110,6 +111,7 @@ export function FlatGraph({
110
111
  const cy = pts.reduce((s, p) => s + p.y, 0) / pts.length;
111
112
  const r =
112
113
  Math.max(...pts.map((p) => Math.sqrt((p.x - cx) ** 2 + (p.y - cy) ** 2)), 20) + 28;
114
+ const clusterName = cn[0]?.cluster?.name ?? cid;
113
115
  return (
114
116
  <ellipse
115
117
  key={cid}
@@ -117,11 +119,13 @@ export function FlatGraph({
117
119
  cy={cy}
118
120
  rx={r}
119
121
  ry={r * 0.85}
120
- fill={`${color}07`}
121
- stroke={`${color}18`}
122
+ fill={noGlow ? 'none' : `${color}07`}
123
+ stroke={`${color}${noGlow ? '40' : '18'}`}
122
124
  strokeWidth={1}
123
125
  strokeDasharray="5 3"
124
- />
126
+ >
127
+ <title>{clusterName} — {cn.length} file{cn.length !== 1 ? 's' : ''}</title>
128
+ </ellipse>
125
129
  );
126
130
  })}
127
131
 
@@ -138,6 +142,8 @@ export function FlatGraph({
138
142
  nr = 18;
139
143
  const isSel = e.source === selectedId || e.target === selectedId;
140
144
  const isAff = affectedIds.includes(e.target) && e.source === selectedId;
145
+ const isDimEdge = focusedIds.length > 0 && !isSel &&
146
+ !focusedIds.includes(e.source) && !focusedIds.includes(e.target);
141
147
  return (
142
148
  <line
143
149
  key={e.id}
@@ -145,9 +151,9 @@ export function FlatGraph({
145
151
  y1={s.y + ny * nr}
146
152
  x2={t.x - nx * (nr + 5)}
147
153
  y2={t.y - ny * (nr + 5)}
148
- stroke={isSel ? '#7c6af7' : isAff ? '#f77c6a' : e.isType ? '#252a4a' : '#181c36'}
154
+ stroke={isSel ? 'var(--ac-primary)' : isAff ? '#f77c6a' : e.isType ? 'var(--ac-edge-type)' : 'var(--bd-edge)'}
149
155
  strokeWidth={isSel ? 1.5 : isAff ? 1.2 : 0.8}
150
- strokeOpacity={isSel ? 0.9 : isAff ? 0.7 : e.isType ? 0.35 : 0.55}
156
+ strokeOpacity={isDimEdge ? 0.08 : isSel ? 0.9 : isAff ? 0.7 : e.isType ? 0.35 : 0.55}
151
157
  strokeDasharray={e.isType ? '4 2' : undefined}
152
158
  markerEnd={
153
159
  isSel
@@ -189,8 +195,9 @@ export function FlatGraph({
189
195
  }}
190
196
  style={{ cursor: 'pointer' }}
191
197
  opacity={isDim ? 0.08 : isZeroScore ? 0.18 : 1}
192
- filter={isSel ? 'url(#glow)' : isAff ? 'url(#glow-aff)' : undefined}
198
+ filter={noGlow ? undefined : isSel ? 'url(#glow)' : isAff ? 'url(#glow-aff)' : undefined}
193
199
  >
200
+ <title>{n.label}{n.path ? `\n${n.path}` : ''}</title>
194
201
  <circle
195
202
  r={r + 4}
196
203
  fill="none"
@@ -209,8 +216,8 @@ export function FlatGraph({
209
216
  )}
210
217
  <circle
211
218
  r={r}
212
- fill={isSel ? `${col}1a` : isAff ? `${col}0d` : '#0b0d1e'}
213
- stroke={isSel ? col : isAff ? col : '#1c2038'}
219
+ fill={isSel ? `${col}1a` : isAff ? `${col}0d` : 'var(--bg-node)'}
220
+ stroke={isSel ? col : isAff ? col : 'var(--bd-edge)'}
214
221
  strokeWidth={isSel ? 2.5 : isAff ? 2 : 0.8}
215
222
  />
216
223
  {n.isEntry && (
@@ -228,7 +235,7 @@ export function FlatGraph({
228
235
  dominantBaseline="middle"
229
236
  fontSize={7}
230
237
  fontWeight={isSel ? 700 : 400}
231
- fill={isSel ? '#fff' : isAff ? col : '#5a6090'}
238
+ fill={isSel ? 'var(--tx-node-sel)' : isAff ? col : 'var(--tx-node)'}
232
239
  fontFamily="'JetBrains Mono',monospace"
233
240
  style={{ pointerEvents: 'none' }}
234
241
  >
@@ -257,11 +264,11 @@ export function FlatGraph({
257
264
  style={{
258
265
  fontSize: 8,
259
266
  padding: '2px 6px',
260
- background: '#0d0f22',
261
- border: `1px solid ${transform.x !== 0 || transform.y !== 0 || transform.k !== 1 ? '#7c6af7' : '#1a1f38'}`,
267
+ background: 'var(--bg-input)',
268
+ border: `1px solid ${transform.x !== 0 || transform.y !== 0 || transform.k !== 1 ? 'var(--ac-primary)' : 'var(--bd-muted)'}`,
262
269
  borderRadius: 4,
263
270
  color:
264
- transform.x !== 0 || transform.y !== 0 || transform.k !== 1 ? '#7c6af7' : '#2a2f4a',
271
+ transform.x !== 0 || transform.y !== 0 || transform.k !== 1 ? 'var(--ac-primary)' : 'var(--tx-faint)',
265
272
  cursor: 'pointer',
266
273
  fontFamily: "'JetBrains Mono',monospace",
267
274
  letterSpacing: '0.05em',
@@ -1,6 +1,6 @@
1
1
  export function Hint({ children }) {
2
2
  return (
3
- <div style={{ fontSize: 10, color: '#2a2f4a', lineHeight: 1.8, marginTop: 4 }}>{children}</div>
3
+ <div style={{ fontSize: 10, color: 'var(--tx-faint)', lineHeight: 1.8, marginTop: 4 }}>{children}</div>
4
4
  );
5
5
  }
6
6
 
@@ -9,7 +9,7 @@ export function SL({ children }) {
9
9
  <div
10
10
  style={{
11
11
  fontSize: 8,
12
- color: '#3a3f5c',
12
+ color: 'var(--tx-ghost)',
13
13
  letterSpacing: '0.08em',
14
14
  marginTop: 12,
15
15
  marginBottom: 5,
@@ -29,11 +29,11 @@ export function Row({ label, value }) {
29
29
  justifyContent: 'space-between',
30
30
  alignItems: 'center',
31
31
  padding: '3px 0',
32
- borderBottom: '1px solid #0e1025',
32
+ borderBottom: '1px solid var(--bd-dim)',
33
33
  }}
34
34
  >
35
- <span style={{ fontSize: 9, color: '#3a4070' }}>{label}</span>
36
- <span style={{ fontSize: 9, color: '#8890b0' }}>{value}</span>
35
+ <span style={{ fontSize: 9, color: 'var(--tx-dim)' }}>{label}</span>
36
+ <span style={{ fontSize: 9, color: 'var(--tx-secondary)' }}>{value}</span>
37
37
  </div>
38
38
  );
39
39
  }
@@ -58,20 +58,22 @@ export function Chip({ color, children }) {
58
58
 
59
59
  export function KindBadge({ kind }) {
60
60
  const map = {
61
- class: ['#a78bfa', '#1a1060'],
62
- function: ['#4ecdc4', '#00301a'],
63
- interface: ['#60a5fa', '#001a30'],
64
- type: ['#f472b6', '#2a0a20'],
65
- enum: ['#f5c518', '#2a1a00'],
61
+ class: '#a78bfa',
62
+ function: '#4ecdc4',
63
+ interface: '#60a5fa',
64
+ type: '#f472b6',
65
+ enum: '#f5c518',
66
+ const: '#fb923c',
66
67
  };
67
- const [c, bg] = map[kind] || ['#64748b', '#1a1a2a'];
68
+ const c = map[kind] || '#64748b';
68
69
  return (
69
70
  <span
70
71
  style={{
71
72
  fontSize: 7,
72
73
  padding: '1px 5px',
73
74
  borderRadius: 3,
74
- background: bg,
75
+ background: `${c}18`,
76
+ border: `1px solid ${c}45`,
75
77
  color: c,
76
78
  minWidth: 44,
77
79
  textAlign: 'center',
@@ -14,6 +14,23 @@ export const CLUSTER_PALETTE = [
14
14
  '#ffb347',
15
15
  ];
16
16
 
17
+ /** Darker variant of CLUSTER_PALETTE for light backgrounds — same hues, lower luminance. */
18
+ export const CLUSTER_PALETTE_LIGHT = [
19
+ '#4a34d4',
20
+ '#0e8f8f',
21
+ '#c0412a',
22
+ '#1a9e50',
23
+ '#c08010',
24
+ '#b82090',
25
+ '#1a6ec0',
26
+ '#6e9800',
27
+ '#b05010',
28
+ '#4040c0',
29
+ '#c01060',
30
+ '#007a60',
31
+ '#b07000',
32
+ ];
33
+
17
34
  export const EXT_COLOR = {
18
35
  '.ts': '#4ecdc4',
19
36
  '.tsx': '#3ecfcf',
@@ -1,4 +1,6 @@
1
1
  import { CLUSTER_PALETTE } from './constants.js';
2
+ export { CLUSTER_PALETTE } from './constants.js';
3
+ export { CLUSTER_PALETTE_LIGHT } from './constants.js';
2
4
 
3
5
  export function parseSpecRequirements(mdText) {
4
6
  const reqs = {};
@@ -39,7 +41,7 @@ export function normalizePath(p) {
39
41
  return (p || '').replace(/\\/g, '/').replace(/^\/+/, '');
40
42
  }
41
43
 
42
- export function parseGraph(raw) {
44
+ export function parseGraph(raw, palette = CLUSTER_PALETTE) {
43
45
  const clusterByNode = {};
44
46
  (raw.clusters || []).forEach((cl, ci) => {
45
47
  cl.files.forEach((fid) => {
@@ -47,7 +49,7 @@ export function parseGraph(raw) {
47
49
  name: cl.name,
48
50
  index: ci,
49
51
  id: cl.id,
50
- color: CLUSTER_PALETTE[ci % CLUSTER_PALETTE.length],
52
+ color: palette[ci % palette.length],
51
53
  };
52
54
  });
53
55
  });
@@ -83,13 +85,15 @@ export function parseGraph(raw) {
83
85
  id: cl.id,
84
86
  name: cl.name,
85
87
  files: cl.files,
86
- color: CLUSTER_PALETTE[ci % CLUSTER_PALETTE.length],
88
+ color: palette[ci % palette.length],
87
89
  }));
88
90
 
89
91
  return {
90
92
  nodes,
91
93
  edges,
92
94
  clusters,
95
+ structuralClusters: raw.structuralClusters || [],
96
+ directoryClusters: raw.directoryClusters || [],
93
97
  statistics: raw.statistics || {},
94
98
  rankings: raw.rankings || {},
95
99
  };
@@ -0,0 +1,39 @@
1
+ import { parseGraph } from './graph-helpers.js';
2
+
3
+ describe('parseGraph', () => {
4
+ it('should parse structuralClusters and directoryClusters from raw data', () => {
5
+ const raw = {
6
+ nodes: [],
7
+ edges: [],
8
+ clusters: [],
9
+ structuralClusters: [
10
+ { id: 'cluster1', name: 'Structural Cluster 1', files: ['file1.js'], color: '#ff0000' },
11
+ ],
12
+ directoryClusters: [
13
+ { id: 'dir1', name: 'Directory Cluster 1', files: ['dir/file.js'], color: '#00ff00' },
14
+ ],
15
+ statistics: {},
16
+ rankings: {},
17
+ };
18
+
19
+ const result = parseGraph(raw);
20
+
21
+ expect(result.structuralClusters).toEqual(raw.structuralClusters);
22
+ expect(result.directoryClusters).toEqual(raw.directoryClusters);
23
+ });
24
+
25
+ it('should provide empty arrays for missing structuralClusters and directoryClusters', () => {
26
+ const raw = {
27
+ nodes: [],
28
+ edges: [],
29
+ clusters: [],
30
+ statistics: {},
31
+ rankings: {},
32
+ };
33
+
34
+ const result = parseGraph(raw);
35
+
36
+ expect(result.structuralClusters).toEqual([]);
37
+ expect(result.directoryClusters).toEqual([]);
38
+ });
39
+ });
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Viewer themes — each theme is a map of CSS custom properties.
3
+ *
4
+ * The root <div> of InteractiveGraphViewer applies `theme.vars` as inline style,
5
+ * making all CSS variables available to every descendant via `var(--xxx)`.
6
+ */
7
+
8
+ export const THEMES = {
9
+ space: {
10
+ label: '⬡ space',
11
+ vars: {
12
+ '--bg-base': '#07091a',
13
+ '--bg-panel': '#080a1c',
14
+ '--bg-raised': '#0e1028',
15
+ '--bg-input': '#0d0f22',
16
+ '--bg-hover': '#131630',
17
+ '--bg-select': '#181b38',
18
+ '--bg-deep': '#080b1e',
19
+ '--bg-node': '#0b0d1e',
20
+ '--bd-faint': '#0f1224',
21
+ '--bd-muted': '#1a1f38',
22
+ '--bd-dim': '#0e1025',
23
+ '--bd-edge': '#181c36',
24
+ '--tx-primary': '#c8cde8',
25
+ '--tx-secondary': '#8890b0',
26
+ '--tx-bright': '#e0e4f0',
27
+ '--tx-muted': '#6a70a0',
28
+ '--tx-dim': '#3a4060',
29
+ '--tx-faint': '#2a2f4a',
30
+ '--tx-ghost': '#3a3f5c',
31
+ '--tx-node': '#5a6090',
32
+ '--tx-node-sel': '#ffffff',
33
+ '--ac-primary': '#7c6af7',
34
+ '--ac-teal': '#3ecfcf',
35
+ '--ac-edge-type': '#252a4a',
36
+ '--ac-arrow': '#1e2340',
37
+ '--ac-cluster-arr': '#2a3060',
38
+ },
39
+ },
40
+
41
+ terminal: {
42
+ label: '▶ terminal',
43
+ vars: {
44
+ '--bg-base': '#010d01',
45
+ '--bg-panel': '#020e02',
46
+ '--bg-raised': '#061206',
47
+ '--bg-input': '#041004',
48
+ '--bg-hover': '#0a1a0a',
49
+ '--bg-select': '#0d200d',
50
+ '--bg-deep': '#030e03',
51
+ '--bg-node': '#040f04',
52
+ '--bd-faint': '#071807',
53
+ '--bd-muted': '#0f2f0f',
54
+ '--bd-dim': '#061806',
55
+ '--bd-edge': '#0d200d',
56
+ '--tx-primary': '#80ee80',
57
+ '--tx-secondary': '#50a050',
58
+ '--tx-bright': '#c0ffc0',
59
+ '--tx-muted': '#408040',
60
+ '--tx-dim': '#285028',
61
+ '--tx-faint': '#1a3a1a',
62
+ '--tx-ghost': '#204020',
63
+ '--tx-node': '#40704a',
64
+ '--tx-node-sel': '#c0ffc0',
65
+ '--ac-primary': '#4ade80',
66
+ '--ac-teal': '#00d4aa',
67
+ '--ac-edge-type': '#103020',
68
+ '--ac-arrow': '#0d2010',
69
+ '--ac-cluster-arr': '#143820',
70
+ },
71
+ },
72
+
73
+ midnight: {
74
+ label: '◈ midnight',
75
+ vars: {
76
+ '--bg-base': '#000000',
77
+ '--bg-panel': '#050505',
78
+ '--bg-raised': '#0a0a0a',
79
+ '--bg-input': '#080808',
80
+ '--bg-hover': '#111111',
81
+ '--bg-select': '#161616',
82
+ '--bg-deep': '#060606',
83
+ '--bg-node': '#090909',
84
+ '--bd-faint': '#0e0e0e',
85
+ '--bd-muted': '#1c1c1c',
86
+ '--bd-dim': '#0c0c0c',
87
+ '--bd-edge': '#141414',
88
+ '--tx-primary': '#00d4e8',
89
+ '--tx-secondary': '#007a88',
90
+ '--tx-bright': '#80eeff',
91
+ '--tx-muted': '#005060',
92
+ '--tx-dim': '#003040',
93
+ '--tx-faint': '#001a20',
94
+ '--tx-ghost': '#002530',
95
+ '--tx-node': '#004558',
96
+ '--tx-node-sel': '#80eeff',
97
+ '--ac-primary': '#00e5ff',
98
+ '--ac-teal': '#00ff88',
99
+ '--ac-edge-type': '#001828',
100
+ '--ac-arrow': '#001020',
101
+ '--ac-cluster-arr': '#001e30',
102
+ },
103
+ },
104
+
105
+ warm: {
106
+ label: '☀ warm',
107
+ vars: {
108
+ '--bg-base': '#1c1711',
109
+ '--bg-panel': '#221e17',
110
+ '--bg-raised': '#2a2318',
111
+ '--bg-input': '#261f14',
112
+ '--bg-hover': '#302818',
113
+ '--bg-select': '#38301e',
114
+ '--bg-deep': '#201b11',
115
+ '--bg-node': '#201a0e',
116
+ '--bd-faint': '#2a2218',
117
+ '--bd-muted': '#3a3020',
118
+ '--bd-dim': '#281f14',
119
+ '--bd-edge': '#302810',
120
+ '--tx-primary': '#d4c59a',
121
+ '--tx-secondary': '#9a8060',
122
+ '--tx-bright': '#ede0c0',
123
+ '--tx-muted': '#806840',
124
+ '--tx-dim': '#605038',
125
+ '--tx-faint': '#483820',
126
+ '--tx-ghost': '#504030',
127
+ '--tx-node': '#706040',
128
+ '--tx-node-sel': '#ede0c0',
129
+ '--ac-primary': '#e8a020',
130
+ '--ac-teal': '#50b060',
131
+ '--ac-edge-type': '#382808',
132
+ '--ac-arrow': '#302408',
133
+ '--ac-cluster-arr': '#402808',
134
+ },
135
+ },
136
+ light: {
137
+ label: '○ light',
138
+ vars: {
139
+ '--bg-base': '#f4f5f9',
140
+ '--bg-panel': '#eef0f6',
141
+ '--bg-raised': '#ffffff',
142
+ '--bg-input': '#ffffff',
143
+ '--bg-hover': '#e8eaf2',
144
+ '--bg-select': '#dde0ef',
145
+ '--bg-deep': '#f0f1f8',
146
+ '--bg-node': '#f8f9fd',
147
+ '--bd-faint': '#dde0ec',
148
+ '--bd-muted': '#8890c8',
149
+ '--bd-dim': '#d0d4e8',
150
+ '--bd-edge': '#6870b0',
151
+ '--tx-primary': '#1a1c2e',
152
+ '--tx-secondary': '#3a3f60',
153
+ '--tx-bright': '#0a0c1a',
154
+ '--tx-muted': '#505580',
155
+ '--tx-dim': '#7880a8',
156
+ '--tx-faint': '#9098b8',
157
+ '--tx-ghost': '#a8aec8',
158
+ '--tx-node': '#1a1f40',
159
+ '--tx-node-sel': '#0a0c1a',
160
+ '--ac-primary': '#5b4de0',
161
+ '--ac-teal': '#0e9e9e',
162
+ '--ac-edge-type': '#8890c8',
163
+ '--ac-arrow': '#6870b0',
164
+ '--ac-cluster-arr': '#5b4de0',
165
+ },
166
+ },
167
+ };
168
+
169
+ export const THEME_KEYS = Object.keys(THEMES);
170
+ export const DEFAULT_THEME = 'space';