@donartcha/openlag 0.1.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 (99) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +82 -0
  3. package/bin/openlag.js +2 -0
  4. package/dist/assets/arc-4YUHkXo3.js +1 -0
  5. package/dist/assets/architectureDiagram-3BPJPVTR-WeGmL7HM.js +36 -0
  6. package/dist/assets/blockDiagram-GPEHLZMM-CtV7ubAx.js +132 -0
  7. package/dist/assets/c4Diagram-AAUBKEIU-DqYDW5c3.js +10 -0
  8. package/dist/assets/channel-Tsel3-MK.js +1 -0
  9. package/dist/assets/chunk-2J33WTMH-BE8P9tjh.js +1 -0
  10. package/dist/assets/chunk-4BX2VUAB-Bi7oLGF5.js +1 -0
  11. package/dist/assets/chunk-55IACEB6-D9Xhxp_r.js +1 -0
  12. package/dist/assets/chunk-727SXJPM-Dz8jKE60.js +206 -0
  13. package/dist/assets/chunk-AQP2D5EJ-BzmM0IeH.js +231 -0
  14. package/dist/assets/chunk-FMBD7UC4-Cvl5dpcx.js +15 -0
  15. package/dist/assets/chunk-ND2GUHAM-Dz2efqnq.js +1 -0
  16. package/dist/assets/chunk-QZHKN3VN-CwblgSnQ.js +1 -0
  17. package/dist/assets/classDiagram-4FO5ZUOK-Bgm-_cW8.js +1 -0
  18. package/dist/assets/classDiagram-v2-Q7XG4LA2-Bgm-_cW8.js +1 -0
  19. package/dist/assets/cose-bilkent-S5V4N54A-h_A3nZUx.js +1 -0
  20. package/dist/assets/cytoscape.esm-D_LviqZs.js +331 -0
  21. package/dist/assets/dagre-BM42HDAG-CN_B2Doz.js +4 -0
  22. package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  23. package/dist/assets/diagram-2AECGRRQ-C9TAFwjG.js +43 -0
  24. package/dist/assets/diagram-5GNKFQAL-BThljQLo.js +10 -0
  25. package/dist/assets/diagram-KO2AKTUF-bRPq25Se.js +3 -0
  26. package/dist/assets/diagram-LMA3HP47-BubLCIus.js +24 -0
  27. package/dist/assets/diagram-OG6HWLK6-CJpfhIsS.js +24 -0
  28. package/dist/assets/erDiagram-TEJ5UH35-6Xkza9wL.js +85 -0
  29. package/dist/assets/flowDiagram-I6XJVG4X-Bq_to3hX.js +162 -0
  30. package/dist/assets/ganttDiagram-6RSMTGT7-C3CmvYl7.js +292 -0
  31. package/dist/assets/gitGraphDiagram-PVQCEYII-C93LTfrl.js +106 -0
  32. package/dist/assets/graph-CAnANduQ.js +1 -0
  33. package/dist/assets/index-0RMQQ34p.css +1 -0
  34. package/dist/assets/index-ByxguSZe.js +729 -0
  35. package/dist/assets/infoDiagram-5YYISTIA-CMfuwygl.js +2 -0
  36. package/dist/assets/init-Gi6I4Gst.js +1 -0
  37. package/dist/assets/ishikawaDiagram-YF4QCWOH-CbJ5ojDF.js +70 -0
  38. package/dist/assets/journeyDiagram-JHISSGLW-C_Xz8YyT.js +139 -0
  39. package/dist/assets/kanban-definition-UN3LZRKU-GVv_iRMq.js +89 -0
  40. package/dist/assets/katex-DkKDou_j.js +257 -0
  41. package/dist/assets/layout-DGIYPm2g.js +1 -0
  42. package/dist/assets/linear-BNEtUH2J.js +1 -0
  43. package/dist/assets/mindmap-definition-RKZ34NQL-DIsL0XSF.js +96 -0
  44. package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  45. package/dist/assets/pieDiagram-4H26LBE5-CSCTSOjk.js +30 -0
  46. package/dist/assets/quadrantDiagram-W4KKPZXB-CQQ9OaFY.js +7 -0
  47. package/dist/assets/requirementDiagram-4Y6WPE33-Cjn3la_S.js +84 -0
  48. package/dist/assets/sankeyDiagram-5OEKKPKP-DoVspvVc.js +40 -0
  49. package/dist/assets/sequenceDiagram-3UESZ5HK-UsoGmL4w.js +162 -0
  50. package/dist/assets/stateDiagram-AJRCARHV-DLmf7Dc8.js +1 -0
  51. package/dist/assets/stateDiagram-v2-BHNVJYJU-jkiDZ_3u.js +1 -0
  52. package/dist/assets/timeline-definition-PNZ67QCA-HfyRxZ8p.js +120 -0
  53. package/dist/assets/vennDiagram-CIIHVFJN-B6pM3L33.js +34 -0
  54. package/dist/assets/wardley-L42UT6IY-B-LdKtrI.js +173 -0
  55. package/dist/assets/wardleyDiagram-YWT4CUSO-BD45zhOu.js +78 -0
  56. package/dist/assets/xychartDiagram-2RQKCTM6-zsDMbUiS.js +7 -0
  57. package/dist/cli/openlag.js +1793 -0
  58. package/dist/index.html +14 -0
  59. package/index.html +13 -0
  60. package/package.json +84 -0
  61. package/scripts/cli/build.ts +34 -0
  62. package/scripts/cli/dev.ts +35 -0
  63. package/scripts/cli/generate.ts +92 -0
  64. package/scripts/cli/init.ts +427 -0
  65. package/scripts/cli/lint.ts +29 -0
  66. package/scripts/cli/openlag.ts +110 -0
  67. package/scripts/cli/vite-bin.ts +8 -0
  68. package/scripts/core/parser/diagnostic.ts +34 -0
  69. package/scripts/core/parser/normalizer.ts +27 -0
  70. package/scripts/core/parser/scanner.ts +30 -0
  71. package/scripts/core/parser/schemas.ts +23 -0
  72. package/scripts/core/parser/types.ts +30 -0
  73. package/scripts/core/parser.ts +127 -0
  74. package/scripts/generate-relations.ts +53 -0
  75. package/scripts/lint/lint-engine.ts +85 -0
  76. package/scripts/lint/lint-profiles.ts +49 -0
  77. package/scripts/lint/lint-rules.ts +174 -0
  78. package/scripts/lint/lint-types.ts +43 -0
  79. package/src/App.tsx +164 -0
  80. package/src/components/DocumentationView.tsx +905 -0
  81. package/src/components/GraphView.tsx +529 -0
  82. package/src/components/GuideView.tsx +535 -0
  83. package/src/components/ImpactView.tsx +365 -0
  84. package/src/components/MarkdownRenderer.tsx +120 -0
  85. package/src/components/OrphansView.tsx +360 -0
  86. package/src/components/SettingsView.tsx +146 -0
  87. package/src/core/generated/relation-definitions.ts +622 -0
  88. package/src/core/graph/GraphQueryLayer.ts +194 -0
  89. package/src/core/registry/ArtifactRegistry.ts +19 -0
  90. package/src/core/registry/RelationRegistry.ts +27 -0
  91. package/src/core/semantic/artifact-layers.ts +43 -0
  92. package/src/core/semantic/ownership-rules.ts +13 -0
  93. package/src/core/semantic/types.ts +11 -0
  94. package/src/index.css +121 -0
  95. package/src/lib/reportUtils.ts +59 -0
  96. package/src/main.tsx +10 -0
  97. package/src/store.ts +146 -0
  98. package/src/types.ts +77 -0
  99. package/vite.config.ts +31 -0
@@ -0,0 +1,529 @@
1
+ import React, { useMemo, useCallback, useEffect, useState } from 'react';
2
+ import { ReactFlow, MiniMap, Controls, Background, useNodesState, useEdgesState, Node, Edge, MarkerType, Handle, Position, Panel, ReactFlowProvider, useReactFlow } from '@xyflow/react';
3
+ import dagre from 'dagre';
4
+ import { Search, X, AlertCircle, ChevronUp, ChevronDown } from 'lucide-react';
5
+ import '@xyflow/react/dist/style.css';
6
+ import { useStore } from '../store';
7
+ import { ArtifactType } from '../types';
8
+
9
+ const typeColors: Record<ArtifactType, string> = {
10
+ REQUIREMENT: 'text-blue-400 border-blue-400',
11
+ USE_CASE: 'text-indigo-400 border-indigo-400',
12
+ DESIGN: 'text-purple-400 border-purple-400',
13
+ COMPONENT: 'text-amber-400 border-amber-400',
14
+ CODE_ENTITY: 'text-emerald-400 border-emerald-400',
15
+ TEST: 'text-rose-400 border-rose-400',
16
+ DOCUMENTATION: 'text-slate-400 border-slate-400',
17
+ INCIDENT: 'text-red-400 border-red-400',
18
+ INFRASTRUCTURE: 'text-cyan-400 border-cyan-400',
19
+ DEPLOYMENT: 'text-sky-400 border-sky-400',
20
+ MONITORING: 'text-orange-400 border-orange-400',
21
+ MAINTENANCE: 'text-violet-400 border-violet-400',
22
+ PROJECT: 'text-stone-400 border-stone-400',
23
+ EPIC: 'text-indigo-500 border-indigo-500',
24
+ FEATURE: 'text-blue-500 border-blue-500',
25
+ BUSINESS_RULE: 'text-teal-400 border-teal-400',
26
+ DECISION: 'text-pink-400 border-pink-400',
27
+ TEST_CASE: 'text-rose-500 border-rose-500',
28
+ CHANGE: 'text-lime-400 border-lime-400',
29
+ BUG: 'text-red-500 border-red-500',
30
+ RISK: 'text-fuchsia-400 border-fuchsia-400',
31
+ GLOSSARY_TERM: 'text-gray-400 border-gray-400',
32
+ API: 'text-emerald-500 border-emerald-500',
33
+ DATABASE_ENTITY: 'text-orange-500 border-orange-500',
34
+ SYSTEM_VERSION: 'text-yellow-500 border-yellow-500',
35
+ VERSION: 'text-teal-500 border-teal-500',
36
+ LIBRARY: 'text-blue-400 border-blue-400',
37
+ ENVIRONMENT: 'text-indigo-400 border-indigo-400',
38
+ CHECK: 'text-red-400 border-red-400',
39
+ PROCESS: 'text-fuchsia-400 border-fuchsia-400',
40
+ PIPELINE: 'text-sky-400 border-sky-400',
41
+ };
42
+
43
+ const CustomNode = ({ data, selected }: any) => {
44
+ const colorClass = typeColors[data.type as ArtifactType] || 'text-white/40 border-white/40';
45
+ const isDimmed = data.dimmed;
46
+ const isOrphan = data.isOrphan;
47
+
48
+ return (
49
+ <div className={`p-4 shadow-xl border bg-[#151515] w-[260px] cursor-pointer transition-all duration-300
50
+ ${selected ? 'ring-1 ring-white/50 border-white/50 z-10' : 'border-white/10 hover:border-white/30'}
51
+ ${isDimmed ? 'opacity-30 grayscale saturate-0' : 'opacity-100'}
52
+ ${isOrphan ? 'border-red-500/50' : 'border-white/10'}
53
+ border-l-[3px]`}
54
+ style={{ borderLeftColor: isOrphan ? '#ef4444' : 'currentColor' }}
55
+ >
56
+ <Handle type="target" position={Position.Top} id="t-top" className={`w-2 h-2 rounded-none ${isDimmed ? 'opacity-0' : 'bg-emerald-500/50 border-0'}`} style={{ left: '30%' }} />
57
+ <Handle type="source" position={Position.Top} id="s-top" className={`w-2 h-2 rounded-none ${isDimmed ? 'opacity-0' : 'bg-blue-500/50 border-0'}`} style={{ left: '70%' }} />
58
+ <div className={`flex justify-between items-start mb-3 ${colorClass}`}>
59
+ <div className="flex flex-col gap-0.5">
60
+ <div className="flex items-center gap-1.5">
61
+ <span className="text-[9px] font-bold uppercase tracking-widest">{data.type}</span>
62
+ {isOrphan && (
63
+ <div className="text-red-500" title="Orphan Artifact (No relationships)">
64
+ <AlertCircle size={10} />
65
+ </div>
66
+ )}
67
+ </div>
68
+ {data.subType && <span className="text-[8px] font-sans uppercase tracking-wider opacity-60">{data.subType}</span>}
69
+ {data.layer && <span className="text-[7px] font-mono tracking-wider opacity-40">{data.layer}</span>}
70
+ </div>
71
+ <span className="text-[10px] font-mono opacity-60 shrink-0 ml-2 border border-white/10 px-1">{data.id}</span>
72
+ </div>
73
+ <div className="font-serif text-[#e0e0e0] text-sm mb-2 leading-tight">{data.title}</div>
74
+ <div className="flex flex-col gap-1">
75
+ <div className="text-[11px] leading-relaxed opacity-60 h-8 overflow-hidden text-ellipsis line-clamp-2">{data.description}</div>
76
+ {data.ownership?.owner && <div className="text-[8px] text-emerald-500 font-mono">OWNER: {data.ownership.owner}</div>}
77
+ {data.ownership?.team && <div className="text-[8px] text-blue-400 font-mono">TEAM: {data.ownership.team}</div>}
78
+ </div>
79
+ <Handle type="target" position={Position.Bottom} id="t-bot" className={`w-2 h-2 rounded-none ${isDimmed ? 'opacity-0' : 'bg-emerald-500/50 border-0'}`} style={{ left: '70%' }} />
80
+ <Handle type="source" position={Position.Bottom} id="s-bot" className={`w-2 h-2 rounded-none ${isDimmed ? 'opacity-0' : 'bg-blue-500/50 border-0'}`} style={{ left: '30%' }} />
81
+ </div>
82
+ );
83
+ };
84
+
85
+ const nodeTypes = {
86
+ artifact: CustomNode,
87
+ };
88
+
89
+ const NODE_WIDTH = 260;
90
+ const NODE_HEIGHT = 140;
91
+
92
+ const getLayoutedElements = (nodes: Node[], edges: Edge[], direction = 'TB') => {
93
+ const dagreGraph = new dagre.graphlib.Graph();
94
+ dagreGraph.setDefaultEdgeLabel(() => ({}));
95
+
96
+ // Aumentar la separación para reducir cruces y dar espacio a las etiquetas
97
+ dagreGraph.setGraph({ rankdir: direction, align: 'UL', edgesep: 120, ranksep: 160, nodesep: 100 });
98
+
99
+ nodes.forEach((node) => {
100
+ if (!node.hidden) {
101
+ dagreGraph.setNode(node.id, { width: NODE_WIDTH, height: NODE_HEIGHT });
102
+ }
103
+ });
104
+
105
+ edges.forEach((edge) => {
106
+ if (!edge.hidden) {
107
+ dagreGraph.setEdge(edge.source, edge.target);
108
+ }
109
+ });
110
+
111
+ dagre.layout(dagreGraph);
112
+
113
+ const newNodes = nodes.map((node) => {
114
+ if (node.hidden) {
115
+ return { ...node, position: { x: 0, y: 0 } };
116
+ }
117
+ const nodeWithPosition = dagreGraph.node(node.id);
118
+ return {
119
+ ...node,
120
+ position: {
121
+ x: nodeWithPosition.x - NODE_WIDTH / 2,
122
+ y: nodeWithPosition.y - NODE_HEIGHT / 2,
123
+ },
124
+ };
125
+ });
126
+
127
+ return { nodes: newNodes, edges };
128
+ };
129
+
130
+ const Legend = () => {
131
+ const [isOpen, setIsOpen] = useState(false);
132
+ return (
133
+ <Panel position="bottom-center" className="m-4 z-50">
134
+ <div className="bg-[#0c0c0c] border border-white/10 p-3 shadow-2xl rounded-md flex flex-col gap-2 w-48">
135
+ <div className="flex items-center justify-between cursor-pointer" onClick={() => setIsOpen(!isOpen)}>
136
+ <span className="text-[10px] uppercase font-bold tracking-widest text-white/50 hover:text-white transition-colors">Legend</span>
137
+ {isOpen ? <ChevronDown size={12} className="text-white/50" /> : <ChevronUp size={12} className="text-white/50" />}
138
+ </div>
139
+
140
+ {isOpen && (
141
+ <div className="flex flex-col gap-3 pt-2 border-t border-white/10 max-h-[60vh] overflow-y-auto" style={{ scrollbarWidth: 'thin' }}>
142
+ <div className="flex flex-col gap-1.5">
143
+ <div className="text-[9px] uppercase font-bold text-white/30">Relation Strength</div>
144
+ <div className="flex items-center gap-2">
145
+ <div className="w-4 h-[2px] bg-[#f87171]"></div>
146
+ <span className="text-[9px] text-white/70 font-mono">STRONG (Solid)</span>
147
+ </div>
148
+ <div className="flex items-center gap-2">
149
+ <div className="w-4 h-[2px]" style={{ borderTop: '2px dashed #38bdf8' }}></div>
150
+ <span className="text-[9px] text-white/70 font-mono">MEDIUM (Dashed)</span>
151
+ </div>
152
+ <div className="flex items-center gap-2">
153
+ <div className="w-4 h-[1px]" style={{ borderTop: '2px dotted #94a3b8' }}></div>
154
+ <span className="text-[9px] text-white/70 font-mono">WEAK (Dotted)</span>
155
+ </div>
156
+ </div>
157
+
158
+ <div className="flex flex-col gap-1.5">
159
+ <div className="text-[9px] uppercase font-bold text-white/30">Node Connectors</div>
160
+ <div className="flex items-center gap-2">
161
+ <div className="w-2 h-2 rounded bg-blue-500/50"></div>
162
+ <span className="text-[9px] text-white/70 font-mono">Outputs (Source)</span>
163
+ </div>
164
+ <div className="flex items-center gap-2">
165
+ <div className="w-2 h-2 rounded bg-emerald-500/50"></div>
166
+ <span className="text-[9px] text-white/70 font-mono">Inputs (Target)</span>
167
+ </div>
168
+ </div>
169
+
170
+ <div className="flex flex-col gap-1.5">
171
+ <div className="text-[9px] uppercase font-bold text-white/30">Artifact Types</div>
172
+ {Object.entries(typeColors).map(([type, colorClass]) => (
173
+ <div key={type} className="flex items-center gap-2">
174
+ <div className={`w-2 h-2 rounded-full border border-current bg-current ${colorClass.split(' ')[0]}`}></div>
175
+ <span className={`text-[9px] font-mono ${colorClass.split(' ')[0]}`}>{type}</span>
176
+ </div>
177
+ ))}
178
+ </div>
179
+ </div>
180
+ )}
181
+ </div>
182
+ </Panel>
183
+ );
184
+ };
185
+
186
+ const GraphFlow: React.FC = () => {
187
+ const { fullGraph, currentSubgraph: graph, selectedArtifactId, setSelectedArtifact, setView, settings, globalFilters, setGlobalFilter } = useStore();
188
+ const { setCenter } = useReactFlow();
189
+ const [nodes, setNodes, onNodesChange] = useNodesState([]);
190
+
191
+ const orphanIds = useMemo(() => {
192
+ if (!fullGraph) return new Set<string>();
193
+ const linked = new Set<string>();
194
+ fullGraph.relations.forEach(rel => {
195
+ linked.add(rel.from);
196
+ linked.add(rel.to);
197
+ });
198
+ const orphans = new Set<string>();
199
+ fullGraph.artifacts.forEach(art => {
200
+ if (!linked.has(art.id)) orphans.add(art.id);
201
+ });
202
+ return orphans;
203
+ }, [fullGraph]);
204
+
205
+ const [searchTerm, setSearchTerm] = useState('');
206
+ const [isDropdownOpen, setIsDropdownOpen] = useState(false);
207
+ const filterLayer = globalFilters.layer;
208
+ const filterOwner = globalFilters.owner;
209
+ const filterTeam = globalFilters.team;
210
+
211
+ const setFilterLayer = (val: string) => setGlobalFilter('layer', val);
212
+ const setFilterOwner = (val: string) => setGlobalFilter('owner', val);
213
+ const setFilterTeam = (val: string) => setGlobalFilter('team', val);
214
+
215
+ const filterOptions = useMemo(() => {
216
+ if (!fullGraph || !fullGraph.artifacts) return { layers: [], owners: [], teams: [] };
217
+ const layers = Array.from(new Set(fullGraph.artifacts.map(a => a.layer).filter(Boolean))) as string[];
218
+ const owners = Array.from(new Set(fullGraph.artifacts.map(a => a.ownership?.owner).filter(Boolean))) as string[];
219
+ const teams = Array.from(new Set(fullGraph.artifacts.map(a => a.ownership?.team).filter(Boolean))) as string[];
220
+ return { layers, owners, teams };
221
+ }, [fullGraph]);
222
+
223
+ const filteredArtifacts = useMemo(() => {
224
+ if (!graph) return [];
225
+ return graph.artifacts.filter(a => {
226
+ // layer/owner are already filtered by projectSubgraph in the query layer,
227
+ // but we apply searchTerm here specifically.
228
+ if (!searchTerm) return true;
229
+
230
+ const term = searchTerm.toLowerCase();
231
+ return a.title.toLowerCase().includes(term) ||
232
+ a.type.toLowerCase().includes(term) ||
233
+ (a.subType && a.subType.toLowerCase().includes(term)) ||
234
+ a.id.toLowerCase().includes(term);
235
+ });
236
+ }, [graph, searchTerm]);
237
+
238
+ useEffect(() => {
239
+ if (selectedArtifactId && nodes.length > 0) {
240
+ const selectedNode = nodes.find(n => n.id === selectedArtifactId);
241
+ if (selectedNode) {
242
+ setCenter(selectedNode.position.x + NODE_WIDTH / 2, selectedNode.position.y + NODE_HEIGHT / 2, { zoom: 1, duration: 800 });
243
+ }
244
+ }
245
+ }, [selectedArtifactId, nodes, setCenter]);
246
+ const [edges, setEdges, onEdgesChange] = useEdgesState([]);
247
+
248
+ useEffect(() => {
249
+ if (!graph) return;
250
+
251
+ let connectedNodes = new Set<string>();
252
+ let connectedEdges = new Set<string>();
253
+
254
+ if (selectedArtifactId) {
255
+ connectedNodes.add(selectedArtifactId);
256
+
257
+ let currentLevelNodes = new Set<string>([selectedArtifactId]);
258
+
259
+ const maxDepth = settings.graphFocusDepth === 0 ? Infinity : settings.graphFocusDepth;
260
+
261
+ for (let depth = 0; depth < maxDepth; depth++) {
262
+ let nextLevelNodes = new Set<string>();
263
+ let edgesToAdd = new Set<string>();
264
+
265
+ graph.relations.forEach(r => {
266
+ if (currentLevelNodes.has(r.from) || currentLevelNodes.has(r.to)) {
267
+ edgesToAdd.add(r.id);
268
+ if (!connectedNodes.has(r.from)) nextLevelNodes.add(r.from);
269
+ if (!connectedNodes.has(r.to)) nextLevelNodes.add(r.to);
270
+ }
271
+ });
272
+
273
+ if (nextLevelNodes.size === 0) break; // Optimization: stop if no new nodes
274
+
275
+ nextLevelNodes.forEach(n => connectedNodes.add(n));
276
+ edgesToAdd.forEach(e => connectedEdges.add(e));
277
+ currentLevelNodes = nextLevelNodes;
278
+ }
279
+ }
280
+
281
+ let initialNodes: Node[] = graph.artifacts.map((a) => {
282
+ // Find connections for dimming logic
283
+ const isSelected = selectedArtifactId === a.id;
284
+ let isConnectedToSelected = false;
285
+
286
+ if (selectedArtifactId) {
287
+ isConnectedToSelected = connectedNodes.has(a.id);
288
+ }
289
+
290
+ const isFilteredOut = (filterLayer !== 'ALL' && a.layer !== filterLayer) ||
291
+ (filterOwner !== 'ALL' && a.ownership?.owner !== filterOwner) ||
292
+ (filterTeam !== 'ALL' && a.ownership?.team !== filterTeam);
293
+
294
+ const isDimmed = isFilteredOut || (selectedArtifactId !== null && !isSelected && !isConnectedToSelected);
295
+ const isOrphan = orphanIds.has(a.id);
296
+
297
+ return {
298
+ id: a.id,
299
+ type: 'artifact',
300
+ position: { x: 0, y: 0 },
301
+ data: { ...a, dimmed: isDimmed, isOrphan }
302
+ };
303
+ });
304
+
305
+ let initialEdges: Edge[] = graph.relations.map((r) => {
306
+ let isConnectedToSelected = true;
307
+ if (selectedArtifactId) {
308
+ isConnectedToSelected = connectedEdges.has(r.id);
309
+ }
310
+
311
+ const sourceNode = graph.artifacts.find(a => a.id === r.from);
312
+ const targetNode = graph.artifacts.find(a => a.id === r.to);
313
+ const isFilteredOut = (filterLayer !== 'ALL' && (sourceNode?.layer !== filterLayer || targetNode?.layer !== filterLayer)) ||
314
+ (filterOwner !== 'ALL' && (sourceNode?.ownership?.owner !== filterOwner || targetNode?.ownership?.owner !== filterOwner)) ||
315
+ (filterTeam !== 'ALL' && (sourceNode?.ownership?.team !== filterTeam || targetNode?.ownership?.team !== filterTeam));
316
+
317
+ const isDimmed = isFilteredOut || (selectedArtifactId !== null && !isConnectedToSelected);
318
+
319
+ const isWeak = r.strength === 'WEAK';
320
+ const isStrong = r.strength === 'STRONG';
321
+
322
+ let edgeColor = '#38bdf8'; // MEDIUM (sky-400)
323
+ if (isStrong) edgeColor = '#f87171'; // STRONG (red-400)
324
+ if (isWeak) edgeColor = '#94a3b8'; // WEAK (slate-400)
325
+
326
+ if (isDimmed) {
327
+ edgeColor = 'rgba(255,255,255,0.05)';
328
+ }
329
+
330
+ return {
331
+ id: r.id,
332
+ source: r.from,
333
+ target: r.to,
334
+ sourceHandle: 's-bot',
335
+ targetHandle: 't-top',
336
+ label: r.category ? `${r.type} (${r.category})` : r.type,
337
+ type: 'default',
338
+ animated: selectedArtifactId !== null && isConnectedToSelected,
339
+ markerEnd: {
340
+ type: MarkerType.ArrowClosed,
341
+ color: edgeColor
342
+ },
343
+ style: {
344
+ stroke: edgeColor,
345
+ strokeWidth: isDimmed ? 1 : (selectedArtifactId && isConnectedToSelected ? (isStrong ? 3 : (isWeak ? 1.5 : 2.5)) : (isStrong ? 2 : (isWeak ? 1 : 1.5))),
346
+ strokeDasharray: isWeak ? '2 4' : (isStrong ? '0' : '5'),
347
+ transition: 'all 0.3s ease'
348
+ },
349
+ labelStyle: {
350
+ fill: isDimmed ? 'transparent' : 'rgba(255,255,255,0.8)',
351
+ fontSize: 8,
352
+ fontWeight: isStrong ? 700 : 400,
353
+ letterSpacing: '1px'
354
+ },
355
+ labelBgStyle: {
356
+ fill: isDimmed ? 'transparent' : '#111111',
357
+ opacity: isDimmed ? 0 : 1,
358
+ stroke: isDimmed ? 'transparent' : 'rgba(255,255,255,0.1)',
359
+ strokeWidth: 1
360
+ },
361
+ labelBgBorderRadius: 4,
362
+ labelBgPadding: [8, 4],
363
+ };
364
+ });
365
+
366
+ const layouted = getLayoutedElements(initialNodes, initialEdges, 'TB');
367
+
368
+ setNodes(layouted.nodes);
369
+ setEdges(layouted.edges);
370
+ }, [graph, selectedArtifactId, settings.graphFocusDepth, setNodes, setEdges, orphanIds, filterLayer, filterOwner, filterTeam]);
371
+
372
+ const onNodeClick = useCallback((_: any, node: Node) => {
373
+ setSelectedArtifact(node.id);
374
+ setIsDropdownOpen(false);
375
+ }, [setSelectedArtifact]);
376
+
377
+ const onPaneClick = useCallback(() => {
378
+ setSelectedArtifact(null);
379
+ setIsDropdownOpen(false);
380
+ }, [setSelectedArtifact]);
381
+
382
+ if (!graph) return <div className="p-8 text-white/50">Loading graph...</div>;
383
+
384
+ return (
385
+ <div className="h-full w-full bg-[#0a0a0a] relative">
386
+ <ReactFlow
387
+ nodes={nodes}
388
+ edges={edges}
389
+ onNodesChange={onNodesChange}
390
+ onEdgesChange={onEdgesChange}
391
+ onNodeClick={onNodeClick}
392
+ onPaneClick={onPaneClick}
393
+ nodeTypes={nodeTypes}
394
+ fitView
395
+ colorMode="dark"
396
+ minZoom={0.2}
397
+ elevateNodesOnSelect={true}
398
+ >
399
+ <Legend />
400
+ <Panel position="top-left" className="m-4 z-50">
401
+ <div className="bg-[#0c0c0c] border border-white/10 p-3 shadow-2xl w-80 flex flex-col gap-2 rounded-md transition-all">
402
+ <div className="flex items-center gap-3 border-b border-white/5 pb-2">
403
+ <Search size={16} className="text-white/40 shrink-0" />
404
+ <input
405
+ type="text"
406
+ placeholder="Search by Type, Title or ID..."
407
+ className="bg-transparent border-none text-xs text-white outline-none w-full placeholder:text-white/20 font-sans"
408
+ value={searchTerm}
409
+ onChange={e => { setSearchTerm(e.target.value); setIsDropdownOpen(true); }}
410
+ onFocus={() => setIsDropdownOpen(true)}
411
+ />
412
+ {selectedArtifactId && (
413
+ <button
414
+ onClick={() => { setSelectedArtifact(null); setSearchTerm(''); setIsDropdownOpen(false); }}
415
+ className="text-white/40 hover:text-emerald-400 transition-colors shrink-0 p-1"
416
+ title="Clear Selection"
417
+ >
418
+ <X size={14} />
419
+ </button>
420
+ )}
421
+ </div>
422
+
423
+ {isDropdownOpen && (
424
+ <div className="max-h-[50vh] overflow-y-auto flex flex-col mt-1 space-y-1 pr-1" style={{ scrollbarWidth: 'thin' }}>
425
+ {orphanIds.size > 0 && !searchTerm && (
426
+ <button
427
+ onClick={() => setView('orphans')}
428
+ className="flex items-center gap-2 p-2 bg-red-500/10 border border-red-500/20 text-red-400 text-[10px] rounded mb-2 hover:bg-red-500/20 transition-all uppercase tracking-widest font-bold"
429
+ >
430
+ <AlertCircle size={14} />
431
+ {orphanIds.size} Orphan Artifacts Found
432
+ </button>
433
+ )}
434
+ {filteredArtifacts.length === 0 ? (
435
+ <div className="text-xs text-white/40 p-2 italic">No artifacts found.</div>
436
+ ) : (
437
+ filteredArtifacts.map(a => (
438
+ <button
439
+ key={a.id}
440
+ className={`text-left text-xs p-2 hover:bg-white/10 border-l-[3px] transition-colors flex flex-col gap-1 rounded-r-md
441
+ ${selectedArtifactId === a.id ? `${typeColors[a.type]?.split(' ')[1] || 'border-emerald-500'} bg-white/5` : 'border-transparent hover:border-white/30'}`}
442
+ onClick={() => {
443
+ setSelectedArtifact(a.id);
444
+ setIsDropdownOpen(false);
445
+ }}
446
+ >
447
+ <div className="text-[9px] uppercase tracking-widest text-[#e0e0e0] opacity-50 font-bold flex justify-between">
448
+ <div className="flex flex-col gap-0.5">
449
+ <span>{a.type}</span>
450
+ {a.subType && <span className="text-[8px] opacity-70">{a.subType}</span>}
451
+ </div>
452
+ <span className="font-mono">{a.id}</span>
453
+ </div>
454
+ <div className={`truncate font-serif text-sm ${typeColors[a.type]?.split(' ')[0] || 'text-emerald-400'}`}>{a.title}</div>
455
+ </button>
456
+ ))
457
+ )}
458
+ </div>
459
+ )}
460
+
461
+ <div className="flex gap-1 pt-2">
462
+ <select
463
+ value={filterLayer}
464
+ onChange={(e) => setFilterLayer(e.target.value)}
465
+ className={`bg-transparent py-1 px-1 text-[8px] focus:outline-none cursor-pointer uppercase tracking-wider flex-1 text-center transition-all border rounded-sm ${
466
+ filterLayer !== 'ALL'
467
+ ? 'bg-blue-500/20 border-blue-500/40 text-blue-400 font-bold'
468
+ : 'border-white/10 text-white/50 hover:border-white/20'
469
+ }`}
470
+ >
471
+ <option value="ALL" className="bg-[#0c0c0c] text-white font-normal">LAYER</option>
472
+ {filterOptions.layers.map(layer => (
473
+ <option key={layer} value={layer} className="bg-[#0c0c0c] text-white font-normal">{layer}</option>
474
+ ))}
475
+ </select>
476
+ <select
477
+ value={filterOwner}
478
+ onChange={(e) => setFilterOwner(e.target.value)}
479
+ className={`bg-transparent py-1 px-1 text-[8px] focus:outline-none cursor-pointer uppercase tracking-wider flex-1 text-center transition-all border rounded-sm ${
480
+ filterOwner !== 'ALL'
481
+ ? 'bg-blue-500/20 border-blue-500/40 text-blue-400 font-bold'
482
+ : 'border-white/10 text-white/50 hover:border-white/20'
483
+ }`}
484
+ >
485
+ <option value="ALL" className="bg-[#0c0c0c] text-white font-normal">OWNER</option>
486
+ {filterOptions.owners.map(owner => (
487
+ <option key={owner} value={owner} className="bg-[#0c0c0c] text-white font-normal">{owner}</option>
488
+ ))}
489
+ </select>
490
+ <select
491
+ value={filterTeam}
492
+ onChange={(e) => setFilterTeam(e.target.value)}
493
+ className={`bg-transparent py-1 px-1 text-[8px] focus:outline-none cursor-pointer uppercase tracking-wider flex-1 text-center transition-all border rounded-sm ${
494
+ filterTeam !== 'ALL'
495
+ ? 'bg-blue-500/20 border-blue-500/40 text-blue-400 font-bold'
496
+ : 'border-white/10 text-white/50 hover:border-white/20'
497
+ }`}
498
+ >
499
+ <option value="ALL" className="bg-[#0c0c0c] text-white font-normal">TEAM</option>
500
+ {filterOptions.teams.map(team => (
501
+ <option key={team} value={team} className="bg-[#0c0c0c] text-white font-normal">{team}</option>
502
+ ))}
503
+ </select>
504
+ </div>
505
+ </div>
506
+ </Panel>
507
+
508
+ <MiniMap
509
+ style={{ backgroundColor: '#0c0c0c', border: '1px solid rgba(255,255,255,0.1)' }}
510
+ maskColor="rgba(0,0,0,0.5)"
511
+ nodeColor="#333"
512
+ />
513
+ <Controls
514
+ style={{ backgroundColor: '#151515', border: '1px solid rgba(255,255,255,0.1)', fill: '#fff' }}
515
+ />
516
+ <Background color="#ffffff" gap={20} size={1} style={{ opacity: 0.03 }} />
517
+ </ReactFlow>
518
+ </div>
519
+ );
520
+ };
521
+
522
+ export const GraphView: React.FC = () => {
523
+ return (
524
+ <ReactFlowProvider>
525
+ <GraphFlow />
526
+ </ReactFlowProvider>
527
+ );
528
+ };
529
+