@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.
- package/LICENSE +373 -0
- package/README.md +82 -0
- package/bin/openlag.js +2 -0
- package/dist/assets/arc-4YUHkXo3.js +1 -0
- package/dist/assets/architectureDiagram-3BPJPVTR-WeGmL7HM.js +36 -0
- package/dist/assets/blockDiagram-GPEHLZMM-CtV7ubAx.js +132 -0
- package/dist/assets/c4Diagram-AAUBKEIU-DqYDW5c3.js +10 -0
- package/dist/assets/channel-Tsel3-MK.js +1 -0
- package/dist/assets/chunk-2J33WTMH-BE8P9tjh.js +1 -0
- package/dist/assets/chunk-4BX2VUAB-Bi7oLGF5.js +1 -0
- package/dist/assets/chunk-55IACEB6-D9Xhxp_r.js +1 -0
- package/dist/assets/chunk-727SXJPM-Dz8jKE60.js +206 -0
- package/dist/assets/chunk-AQP2D5EJ-BzmM0IeH.js +231 -0
- package/dist/assets/chunk-FMBD7UC4-Cvl5dpcx.js +15 -0
- package/dist/assets/chunk-ND2GUHAM-Dz2efqnq.js +1 -0
- package/dist/assets/chunk-QZHKN3VN-CwblgSnQ.js +1 -0
- package/dist/assets/classDiagram-4FO5ZUOK-Bgm-_cW8.js +1 -0
- package/dist/assets/classDiagram-v2-Q7XG4LA2-Bgm-_cW8.js +1 -0
- package/dist/assets/cose-bilkent-S5V4N54A-h_A3nZUx.js +1 -0
- package/dist/assets/cytoscape.esm-D_LviqZs.js +331 -0
- package/dist/assets/dagre-BM42HDAG-CN_B2Doz.js +4 -0
- package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/dist/assets/diagram-2AECGRRQ-C9TAFwjG.js +43 -0
- package/dist/assets/diagram-5GNKFQAL-BThljQLo.js +10 -0
- package/dist/assets/diagram-KO2AKTUF-bRPq25Se.js +3 -0
- package/dist/assets/diagram-LMA3HP47-BubLCIus.js +24 -0
- package/dist/assets/diagram-OG6HWLK6-CJpfhIsS.js +24 -0
- package/dist/assets/erDiagram-TEJ5UH35-6Xkza9wL.js +85 -0
- package/dist/assets/flowDiagram-I6XJVG4X-Bq_to3hX.js +162 -0
- package/dist/assets/ganttDiagram-6RSMTGT7-C3CmvYl7.js +292 -0
- package/dist/assets/gitGraphDiagram-PVQCEYII-C93LTfrl.js +106 -0
- package/dist/assets/graph-CAnANduQ.js +1 -0
- package/dist/assets/index-0RMQQ34p.css +1 -0
- package/dist/assets/index-ByxguSZe.js +729 -0
- package/dist/assets/infoDiagram-5YYISTIA-CMfuwygl.js +2 -0
- package/dist/assets/init-Gi6I4Gst.js +1 -0
- package/dist/assets/ishikawaDiagram-YF4QCWOH-CbJ5ojDF.js +70 -0
- package/dist/assets/journeyDiagram-JHISSGLW-C_Xz8YyT.js +139 -0
- package/dist/assets/kanban-definition-UN3LZRKU-GVv_iRMq.js +89 -0
- package/dist/assets/katex-DkKDou_j.js +257 -0
- package/dist/assets/layout-DGIYPm2g.js +1 -0
- package/dist/assets/linear-BNEtUH2J.js +1 -0
- package/dist/assets/mindmap-definition-RKZ34NQL-DIsL0XSF.js +96 -0
- package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/dist/assets/pieDiagram-4H26LBE5-CSCTSOjk.js +30 -0
- package/dist/assets/quadrantDiagram-W4KKPZXB-CQQ9OaFY.js +7 -0
- package/dist/assets/requirementDiagram-4Y6WPE33-Cjn3la_S.js +84 -0
- package/dist/assets/sankeyDiagram-5OEKKPKP-DoVspvVc.js +40 -0
- package/dist/assets/sequenceDiagram-3UESZ5HK-UsoGmL4w.js +162 -0
- package/dist/assets/stateDiagram-AJRCARHV-DLmf7Dc8.js +1 -0
- package/dist/assets/stateDiagram-v2-BHNVJYJU-jkiDZ_3u.js +1 -0
- package/dist/assets/timeline-definition-PNZ67QCA-HfyRxZ8p.js +120 -0
- package/dist/assets/vennDiagram-CIIHVFJN-B6pM3L33.js +34 -0
- package/dist/assets/wardley-L42UT6IY-B-LdKtrI.js +173 -0
- package/dist/assets/wardleyDiagram-YWT4CUSO-BD45zhOu.js +78 -0
- package/dist/assets/xychartDiagram-2RQKCTM6-zsDMbUiS.js +7 -0
- package/dist/cli/openlag.js +1793 -0
- package/dist/index.html +14 -0
- package/index.html +13 -0
- package/package.json +84 -0
- package/scripts/cli/build.ts +34 -0
- package/scripts/cli/dev.ts +35 -0
- package/scripts/cli/generate.ts +92 -0
- package/scripts/cli/init.ts +427 -0
- package/scripts/cli/lint.ts +29 -0
- package/scripts/cli/openlag.ts +110 -0
- package/scripts/cli/vite-bin.ts +8 -0
- package/scripts/core/parser/diagnostic.ts +34 -0
- package/scripts/core/parser/normalizer.ts +27 -0
- package/scripts/core/parser/scanner.ts +30 -0
- package/scripts/core/parser/schemas.ts +23 -0
- package/scripts/core/parser/types.ts +30 -0
- package/scripts/core/parser.ts +127 -0
- package/scripts/generate-relations.ts +53 -0
- package/scripts/lint/lint-engine.ts +85 -0
- package/scripts/lint/lint-profiles.ts +49 -0
- package/scripts/lint/lint-rules.ts +174 -0
- package/scripts/lint/lint-types.ts +43 -0
- package/src/App.tsx +164 -0
- package/src/components/DocumentationView.tsx +905 -0
- package/src/components/GraphView.tsx +529 -0
- package/src/components/GuideView.tsx +535 -0
- package/src/components/ImpactView.tsx +365 -0
- package/src/components/MarkdownRenderer.tsx +120 -0
- package/src/components/OrphansView.tsx +360 -0
- package/src/components/SettingsView.tsx +146 -0
- package/src/core/generated/relation-definitions.ts +622 -0
- package/src/core/graph/GraphQueryLayer.ts +194 -0
- package/src/core/registry/ArtifactRegistry.ts +19 -0
- package/src/core/registry/RelationRegistry.ts +27 -0
- package/src/core/semantic/artifact-layers.ts +43 -0
- package/src/core/semantic/ownership-rules.ts +13 -0
- package/src/core/semantic/types.ts +11 -0
- package/src/index.css +121 -0
- package/src/lib/reportUtils.ts +59 -0
- package/src/main.tsx +10 -0
- package/src/store.ts +146 -0
- package/src/types.ts +77 -0
- package/vite.config.ts +31 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import React, { useState, useMemo, useEffect } from 'react';
|
|
2
|
+
import { useStore } from '../store';
|
|
3
|
+
import { Activity, ArrowRight, GitCommit, Search, ShieldAlert, Zap } from 'lucide-react';
|
|
4
|
+
import { Artifact } from '../types';
|
|
5
|
+
|
|
6
|
+
export const ImpactView: React.FC = () => {
|
|
7
|
+
const { changes, versions, fullGraph: graph, systemVersions, globalFilters, setGlobalFilter } = useStore();
|
|
8
|
+
const [selectedChangeId, setSelectedChangeId] = useState<string | null>(null);
|
|
9
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
10
|
+
const [filterType, setFilterType] = useState<string | 'ALL'>('ALL');
|
|
11
|
+
const [filterStrength, setFilterStrength] = useState<'ALL' | 'STRONG' | 'MEDIUM' | 'WEAK' | 'IGNORE_WEAK'>('ALL');
|
|
12
|
+
|
|
13
|
+
const filterLayer = globalFilters.layer;
|
|
14
|
+
const filterOwner = globalFilters.owner;
|
|
15
|
+
const filterTeam = globalFilters.team;
|
|
16
|
+
|
|
17
|
+
const setFilterLayer = (val: string) => setGlobalFilter('layer', val);
|
|
18
|
+
const setFilterOwner = (val: string) => setGlobalFilter('owner', val);
|
|
19
|
+
const setFilterTeam = (val: string) => setGlobalFilter('team', val);
|
|
20
|
+
|
|
21
|
+
const filterOptions = useMemo(() => {
|
|
22
|
+
if (!graph || !graph.artifacts) return { layers: [], owners: [], teams: [] };
|
|
23
|
+
const layers = Array.from(new Set(graph.artifacts.map(a => a.layer).filter(Boolean))) as string[];
|
|
24
|
+
const owners = Array.from(new Set(graph.artifacts.map(a => a.ownership?.owner).filter(Boolean))) as string[];
|
|
25
|
+
const teams = Array.from(new Set(graph.artifacts.map(a => a.ownership?.team).filter(Boolean))) as string[];
|
|
26
|
+
return { layers, owners, teams };
|
|
27
|
+
}, [graph]);
|
|
28
|
+
|
|
29
|
+
// Initialize selectedId if not set
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (!selectedChangeId && (changes || []).length > 0) {
|
|
32
|
+
setSelectedChangeId(changes[0].id);
|
|
33
|
+
}
|
|
34
|
+
}, [changes, selectedChangeId]);
|
|
35
|
+
|
|
36
|
+
const filteredChanges = useMemo(() => {
|
|
37
|
+
return (changes || []).filter(c => {
|
|
38
|
+
const matchesSearch = (c.title || '').toLowerCase().includes((searchQuery || '').toLowerCase()) ||
|
|
39
|
+
(c.description || '').toLowerCase().includes((searchQuery || '').toLowerCase());
|
|
40
|
+
const matchesType = filterType === 'ALL' || c.type === filterType;
|
|
41
|
+
|
|
42
|
+
const affectedLayerTeamOwner = (c.affects || []).some(id => {
|
|
43
|
+
const artifact = graph?.artifacts.find(a => a.id === id);
|
|
44
|
+
if (!artifact) return false;
|
|
45
|
+
const matchesLayer = filterLayer === 'ALL' || artifact.layer === filterLayer;
|
|
46
|
+
const matchesOwner = filterOwner === 'ALL' || artifact.ownership?.owner === filterOwner;
|
|
47
|
+
const matchesTeam = filterTeam === 'ALL' || artifact.ownership?.team === filterTeam;
|
|
48
|
+
return matchesLayer && matchesOwner && matchesTeam;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const matchesFilters = (filterLayer === 'ALL' && filterOwner === 'ALL' && filterTeam === 'ALL') || affectedLayerTeamOwner;
|
|
52
|
+
|
|
53
|
+
const matchesStrength = filterStrength === 'ALL' || (c.affects || []).some(id => {
|
|
54
|
+
return graph?.relations.some(r => {
|
|
55
|
+
if (r.from !== id && r.to !== id) return false;
|
|
56
|
+
if (filterStrength === 'IGNORE_WEAK') return r.strength !== 'WEAK';
|
|
57
|
+
return r.strength === filterStrength;
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return matchesSearch && matchesType && matchesFilters && matchesStrength;
|
|
62
|
+
});
|
|
63
|
+
}, [changes, searchQuery, filterType, filterLayer, filterOwner, filterTeam, filterStrength, graph]);
|
|
64
|
+
|
|
65
|
+
const selectedChange = (changes || []).find(c => c.id === selectedChangeId);
|
|
66
|
+
|
|
67
|
+
const affectedSystems = useMemo(() => {
|
|
68
|
+
if (!selectedChange) return [];
|
|
69
|
+
|
|
70
|
+
// Find systems affected directly or through artifacts
|
|
71
|
+
const directlyAffected = (systemVersions || []).filter(sv => (selectedChange.affects || []).includes(sv.id));
|
|
72
|
+
const indirectIds = (selectedChange.affects || []).map(id => graph?.artifacts.find(a => a.id === id)?.systemVersionId).filter(Boolean);
|
|
73
|
+
const indirectlyAffected = (systemVersions || []).filter(sv => indirectIds.includes(sv.id));
|
|
74
|
+
|
|
75
|
+
// Merge unique
|
|
76
|
+
const all = [...directlyAffected, ...indirectlyAffected];
|
|
77
|
+
return (all || []).filter(item => item !== undefined).reduce((acc, item) => {
|
|
78
|
+
if (!acc.find(i => i.id === item.id)) {
|
|
79
|
+
acc.push(item);
|
|
80
|
+
}
|
|
81
|
+
return acc;
|
|
82
|
+
}, [] as typeof all);
|
|
83
|
+
}, [selectedChange, systemVersions, graph]);
|
|
84
|
+
|
|
85
|
+
const phaseImpact = useMemo(() => {
|
|
86
|
+
if (!selectedChange || !graph || !graph.artifacts) return {};
|
|
87
|
+
const affected = (selectedChange.affects || []).map(id => graph.artifacts.find(a => a.id === id)).filter(Boolean) as Artifact[];
|
|
88
|
+
|
|
89
|
+
return affected.reduce((acc, curr) => {
|
|
90
|
+
acc[curr.type] = (acc[curr.type] || 0) + 1;
|
|
91
|
+
return acc;
|
|
92
|
+
}, {} as Record<string, number>);
|
|
93
|
+
}, [selectedChange, graph]);
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div className="h-full w-full bg-[#0a0a0a] text-[#e0e0e0] flex overflow-hidden">
|
|
97
|
+
{/* Changes list sidebar */}
|
|
98
|
+
<div className="w-80 bg-[#0c0c0c] border-r border-white/10 h-full overflow-y-auto shrink-0 z-10 flex flex-col">
|
|
99
|
+
<div className="p-6 border-b border-white/10 bg-[#0a0a0a]">
|
|
100
|
+
<div className="text-[10px] uppercase tracking-widest text-white/60 font-bold flex items-center gap-3 mb-4">
|
|
101
|
+
<Activity size={16} className="text-emerald-400" /> CHANGE TIMELINE
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<div className="space-y-3">
|
|
105
|
+
<div className="relative">
|
|
106
|
+
<Search size={12} className="absolute left-3 top-1/2 -translate-y-1/2 text-white/30" />
|
|
107
|
+
<input
|
|
108
|
+
type="text"
|
|
109
|
+
placeholder="Search changes..."
|
|
110
|
+
value={searchQuery}
|
|
111
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
112
|
+
className="w-full bg-black/40 border border-white/10 rounded-sm py-1.5 pl-8 pr-3 text-[10px] font-mono focus:border-emerald-500/50 outline-none transition-colors"
|
|
113
|
+
/>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<div className="flex gap-1">
|
|
117
|
+
{['ALL', 'ERROR', 'FEATURE', 'EVOLUTION', 'REFACTOR', 'ADAPTATION'].map(type => (
|
|
118
|
+
<button
|
|
119
|
+
key={type}
|
|
120
|
+
onClick={() => setFilterType(type)}
|
|
121
|
+
className={`flex-1 text-[8px] font-bold py-1 border transition-all ${filterType === type ? 'bg-emerald-500/20 border-emerald-500/40 text-emerald-400' : 'bg-transparent border-white/5 text-white/30 hover:border-white/20'}`}
|
|
122
|
+
>
|
|
123
|
+
{type}
|
|
124
|
+
</button>
|
|
125
|
+
))}
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
<div className="flex gap-1 pt-2 border-t border-white/5">
|
|
129
|
+
{['ALL', 'STRONG', 'MEDIUM', 'WEAK', 'IGNORE_WEAK'].map(strength => (
|
|
130
|
+
<button
|
|
131
|
+
key={strength}
|
|
132
|
+
onClick={() => setFilterStrength(strength as any)}
|
|
133
|
+
className={`flex-1 text-[8px] font-bold py-1 border transition-all ${filterStrength === strength ? 'bg-amber-500/20 border-amber-500/40 text-amber-400' : 'bg-transparent border-white/5 text-white/30 hover:border-white/20'}`}
|
|
134
|
+
title={`Filter by Relation Strength: ${strength}`}
|
|
135
|
+
>
|
|
136
|
+
{strength === 'IGNORE_WEAK' ? 'NO WEAK' : strength}
|
|
137
|
+
</button>
|
|
138
|
+
))}
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<div className="flex gap-1 pt-2 border-t border-white/5">
|
|
142
|
+
<select
|
|
143
|
+
value={filterLayer}
|
|
144
|
+
onChange={(e) => setFilterLayer(e.target.value)}
|
|
145
|
+
className={`bg-transparent py-1 px-1 text-[8px] focus:outline-none cursor-pointer uppercase tracking-wider flex-1 text-center transition-all border ${
|
|
146
|
+
filterLayer !== 'ALL'
|
|
147
|
+
? 'bg-blue-500/20 border-blue-500/40 text-blue-400 font-bold'
|
|
148
|
+
: 'border-white/10 text-white/50 hover:border-white/20'
|
|
149
|
+
}`}
|
|
150
|
+
>
|
|
151
|
+
<option value="ALL" className="bg-[#0c0c0c] text-white font-normal">LAYER</option>
|
|
152
|
+
{filterOptions.layers.map(layer => (
|
|
153
|
+
<option key={layer} value={layer} className="bg-[#0c0c0c] text-white font-normal">{layer}</option>
|
|
154
|
+
))}
|
|
155
|
+
</select>
|
|
156
|
+
<select
|
|
157
|
+
value={filterOwner}
|
|
158
|
+
onChange={(e) => setFilterOwner(e.target.value)}
|
|
159
|
+
className={`bg-transparent py-1 px-1 text-[8px] focus:outline-none cursor-pointer uppercase tracking-wider flex-1 text-center transition-all border ${
|
|
160
|
+
filterOwner !== 'ALL'
|
|
161
|
+
? 'bg-blue-500/20 border-blue-500/40 text-blue-400 font-bold'
|
|
162
|
+
: 'border-white/10 text-white/50 hover:border-white/20'
|
|
163
|
+
}`}
|
|
164
|
+
>
|
|
165
|
+
<option value="ALL" className="bg-[#0c0c0c] text-white font-normal">OWNER</option>
|
|
166
|
+
{filterOptions.owners.map(owner => (
|
|
167
|
+
<option key={owner} value={owner} className="bg-[#0c0c0c] text-white font-normal">{owner}</option>
|
|
168
|
+
))}
|
|
169
|
+
</select>
|
|
170
|
+
<select
|
|
171
|
+
value={filterTeam}
|
|
172
|
+
onChange={(e) => setFilterTeam(e.target.value)}
|
|
173
|
+
className={`bg-transparent py-1 px-1 text-[8px] focus:outline-none cursor-pointer uppercase tracking-wider flex-1 text-center transition-all border ${
|
|
174
|
+
filterTeam !== 'ALL'
|
|
175
|
+
? 'bg-blue-500/20 border-blue-500/40 text-blue-400 font-bold'
|
|
176
|
+
: 'border-white/10 text-white/50 hover:border-white/20'
|
|
177
|
+
}`}
|
|
178
|
+
>
|
|
179
|
+
<option value="ALL" className="bg-[#0c0c0c] text-white font-normal">TEAM</option>
|
|
180
|
+
{filterOptions.teams.map(team => (
|
|
181
|
+
<option key={team} value={team} className="bg-[#0c0c0c] text-white font-normal">{team}</option>
|
|
182
|
+
))}
|
|
183
|
+
</select>
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
<div className="flex flex-col flex-1">
|
|
189
|
+
{(filteredChanges || []).length > 0 ? (filteredChanges || []).map(change => {
|
|
190
|
+
const vFrom = versions.find(v => v.id === change.versionFrom)?.name;
|
|
191
|
+
const vTo = versions.find(v => v.id === change.versionTo)?.name;
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<button
|
|
195
|
+
key={change.id}
|
|
196
|
+
onClick={() => setSelectedChangeId(change.id)}
|
|
197
|
+
className={`p-5 text-left border-b border-white/5 hover:bg-white/5 transition-all outline-none ${selectedChangeId === change.id ? 'bg-white/5 border-l-[3px] border-l-emerald-500 shadow-inner' : 'border-l-[3px] border-l-transparent'}`}
|
|
198
|
+
>
|
|
199
|
+
<div className="flex justify-between items-center mb-2">
|
|
200
|
+
<span className={`text-[8px] font-bold px-2 py-0.5 border uppercase tracking-widest ${
|
|
201
|
+
change.type === 'ERROR' ? 'text-red-400 bg-red-400/10 border-red-500/20' :
|
|
202
|
+
change.type === 'FEATURE' ? 'text-emerald-400 bg-emerald-400/10 border-emerald-500/20' :
|
|
203
|
+
change.type === 'EVOLUTION' ? 'text-blue-400 bg-blue-400/10 border-blue-500/20' :
|
|
204
|
+
change.type === 'REFACTOR' ? 'text-amber-400 bg-amber-400/10 border-amber-500/20' :
|
|
205
|
+
change.type === 'ADAPTATION' ? 'text-purple-400 bg-purple-400/10 border-purple-500/20' :
|
|
206
|
+
'text-gray-400 bg-gray-400/10 border-gray-500/20'
|
|
207
|
+
}`}>
|
|
208
|
+
{change.type}
|
|
209
|
+
</span>
|
|
210
|
+
<span className="text-[9px] text-white/40 font-mono tracking-tighter truncate max-w-[80px]">{vFrom} <ArrowRight size={8} className="inline opacity-30 mx-0.5" /> {vTo}</span>
|
|
211
|
+
</div>
|
|
212
|
+
<div className="font-serif text-[#e0e0e0] mb-1 leading-snug text-sm tracking-tight">{change.title}</div>
|
|
213
|
+
</button>
|
|
214
|
+
)
|
|
215
|
+
}) : (
|
|
216
|
+
<div className="p-8 text-center text-[10px] text-white/20 font-mono italic">No changes found matching criteria</div>
|
|
217
|
+
)}
|
|
218
|
+
{(changes || []).length === 0 && <div className="p-6 text-xs text-white/40 italic text-center">No changes logged.</div>}
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
{/* Impact details */}
|
|
223
|
+
<div className="flex-1 overflow-y-auto bg-grid-white/[0.02]">
|
|
224
|
+
{selectedChange ? (
|
|
225
|
+
<div className="max-w-5xl mx-auto p-12">
|
|
226
|
+
<div className="flex flex-col md:flex-row gap-8 items-start mb-12">
|
|
227
|
+
<div className="flex-1">
|
|
228
|
+
<div className="flex items-center gap-3 mb-4">
|
|
229
|
+
<div className="h-[1px] w-8 bg-emerald-500/50" />
|
|
230
|
+
<span className="text-[10px] font-mono text-emerald-500 uppercase tracking-[0.3em]">Lifecycle Intervention</span>
|
|
231
|
+
</div>
|
|
232
|
+
<h2 className="text-4xl font-serif italic tracking-tight mb-4 text-white">
|
|
233
|
+
{selectedChange.title}
|
|
234
|
+
</h2>
|
|
235
|
+
<p className="text-sm leading-relaxed opacity-60 max-w-2xl font-sans">{selectedChange.description}</p>
|
|
236
|
+
|
|
237
|
+
{(affectedSystems || []).length > 0 && (
|
|
238
|
+
<div className="mt-8 flex flex-col gap-3">
|
|
239
|
+
<span className="text-[9px] uppercase tracking-widest text-white/20 font-bold">Related Systems</span>
|
|
240
|
+
<div className="flex gap-2 flex-wrap">
|
|
241
|
+
{(affectedSystems || []).map(sys => (
|
|
242
|
+
<div key={sys.id} className="flex items-center gap-3 bg-emerald-500/5 border border-emerald-500/20 px-3 py-1.5 rounded-sm">
|
|
243
|
+
<div className="w-1.5 h-1.5 bg-emerald-400 rounded-full animate-pulse" />
|
|
244
|
+
<div className="flex flex-col">
|
|
245
|
+
<span className="text-[10px] font-bold text-white/80">{sys.component}</span>
|
|
246
|
+
<span className="text-[9px] font-mono text-white/40">{sys.version}</span>
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
))}
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
)}
|
|
253
|
+
</div>
|
|
254
|
+
|
|
255
|
+
<div className="w-full md:w-64 bg-white/[0.02] border border-white/10 p-6 rounded-sm backdrop-blur-sm">
|
|
256
|
+
<div className="text-[9px] uppercase tracking-widest text-white/30 font-bold mb-4 flex items-center gap-2">
|
|
257
|
+
<Zap size={12} className="text-amber-400" /> Magnitude of Change
|
|
258
|
+
</div>
|
|
259
|
+
<div className="space-y-3">
|
|
260
|
+
{Object.entries(phaseImpact || {}).length > 0 ? Object.entries(phaseImpact || {}).map(([phase, count]) => (
|
|
261
|
+
<div key={phase} className="flex justify-between items-center bg-black/40 px-3 py-1.5 border border-white/5">
|
|
262
|
+
<span className="text-[9px] font-mono text-white/50">{phase}</span>
|
|
263
|
+
<span className="text-[10px] font-bold text-emerald-400">+{count}</span>
|
|
264
|
+
</div>
|
|
265
|
+
)) : <div className="text-[10px] text-white/20 italic">No nodes affected</div>}
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
|
|
270
|
+
<div className="grid grid-cols-1 gap-12">
|
|
271
|
+
<section>
|
|
272
|
+
<h3 className="text-[10px] uppercase tracking-[0.3em] text-white/40 mb-8 flex items-center gap-4">
|
|
273
|
+
<span>Affected Node Inventory</span>
|
|
274
|
+
<div className="flex-1 h-[1px] bg-white/5 shadow-[0_0_10px_rgba(255,255,255,0.05)]" />
|
|
275
|
+
</h3>
|
|
276
|
+
|
|
277
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
278
|
+
{(selectedChange.affects || []).map(artifactId => {
|
|
279
|
+
const artifact = graph?.artifacts.find(a => a.id === artifactId);
|
|
280
|
+
// Also check if it's a direct system version
|
|
281
|
+
const sys = systemVersions.find(s => s.id === artifactId);
|
|
282
|
+
|
|
283
|
+
if (!artifact && !sys) return (
|
|
284
|
+
<div key={artifactId} className="bg-[#0c0c0c] p-6 border border-white/5 flex items-center gap-3 text-red-400/40 opacity-50">
|
|
285
|
+
<ShieldAlert size={16} />
|
|
286
|
+
<span className="text-[11px] italic">Artifact {artifactId} (Not in current snapshot)</span>
|
|
287
|
+
</div>
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
return (
|
|
291
|
+
<div key={artifactId} className="bg-[#0c0c0c] p-6 border border-white/5 hover:border-emerald-500/30 transition-all flex items-start flex-col group relative overflow-hidden">
|
|
292
|
+
<div className="absolute top-0 right-0 p-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
293
|
+
<Search size={14} className="text-white/20" />
|
|
294
|
+
</div>
|
|
295
|
+
{artifact ? (
|
|
296
|
+
<>
|
|
297
|
+
<div className="flex items-center gap-3 mb-4 w-full">
|
|
298
|
+
<span className="text-[8px] font-bold px-2 py-0.5 bg-white/5 border border-white/10 uppercase tracking-widest text-[#888]">{artifact.type}</span>
|
|
299
|
+
<span className="font-mono text-[9px] text-emerald-500/50">{artifact.id}</span>
|
|
300
|
+
</div>
|
|
301
|
+
<div className="font-serif text-[#f0f0f0] text-base mb-3 group-hover:text-emerald-400 transition-colors">{artifact.title}</div>
|
|
302
|
+
<div className="flex flex-col gap-2">
|
|
303
|
+
<div className="flex gap-2 items-center mb-1">
|
|
304
|
+
{artifact.layer && <span className="text-[8px] border border-emerald-500/20 text-emerald-500/60 font-mono px-1 rounded-sm uppercase">{artifact.layer}</span>}
|
|
305
|
+
{artifact.ownership?.owner && <span className="text-[8px] text-blue-400 border border-blue-400/20 bg-blue-400/5 font-mono px-1 rounded-sm">OWNER: {artifact.ownership.owner}</span>}
|
|
306
|
+
{artifact.ownership?.team && <span className="text-[8px] text-emerald-400 border border-emerald-400/20 bg-emerald-400/5 font-mono px-1 rounded-sm">TEAM: {artifact.ownership.team}</span>}
|
|
307
|
+
</div>
|
|
308
|
+
<div className="text-[11px] text-[#e0e0e0]/40 leading-relaxed font-sans line-clamp-3">
|
|
309
|
+
{artifact.description}
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
</>
|
|
313
|
+
) : sys && (
|
|
314
|
+
<>
|
|
315
|
+
<div className="flex items-center gap-3 mb-4 w-full">
|
|
316
|
+
<span className="text-[8px] font-bold px-2 py-0.5 bg-emerald-500/10 border border-emerald-500/20 uppercase tracking-widest text-emerald-400">SYSTEM ENTITY</span>
|
|
317
|
+
<span className="font-mono text-[9px] text-emerald-500/50">{sys.id}</span>
|
|
318
|
+
</div>
|
|
319
|
+
<div className="font-serif text-[#f0f0f0] text-base mb-3 group-hover:text-emerald-400 transition-colors">{sys.component}</div>
|
|
320
|
+
<div className="text-[11px] text-[#e0e0e0]/40 leading-relaxed font-sans">
|
|
321
|
+
Core system component released on {new Date(sys.releaseDate).toLocaleDateString()}.
|
|
322
|
+
</div>
|
|
323
|
+
</>
|
|
324
|
+
)}
|
|
325
|
+
</div>
|
|
326
|
+
)
|
|
327
|
+
})}
|
|
328
|
+
</div>
|
|
329
|
+
</section>
|
|
330
|
+
|
|
331
|
+
<section className="bg-emerald-500/[0.02] border-y border-white/5 p-12 -mx-12">
|
|
332
|
+
<div className="max-w-3xl">
|
|
333
|
+
<h4 className="text-[10px] uppercase tracking-widest text-emerald-500/60 font-bold mb-4">Architectural Integrity Report</h4>
|
|
334
|
+
<p className="text-xs text-white/40 leading-relaxed italic">
|
|
335
|
+
This intervention enabled the transition between <span className="text-white/60">{(versions || []).find(v => v.id === selectedChange.versionFrom)?.name}</span> and <span className="text-white/60">{(versions || []).find(v => v.id === selectedChange.versionTo)?.name}</span>.
|
|
336
|
+
The impact is primarily concentrated at the <span className="text-white/60">
|
|
337
|
+
{Object.keys(phaseImpact || {}).join(', ') || 'Core'} layer
|
|
338
|
+
</span>, affecting critical systems such as <span className="text-white/60">
|
|
339
|
+
{(affectedSystems || []).map(s => s.component).join(', ') || 'Base Components'}
|
|
340
|
+
</span>.
|
|
341
|
+
{(() => {
|
|
342
|
+
switch (selectedChange.type) {
|
|
343
|
+
case 'ERROR': return ' This change addresses technical debt or a previously identified operational failure.';
|
|
344
|
+
case 'FEATURE': return ' The implementation adds capabilities that expand the existing architecture.';
|
|
345
|
+
case 'EVOLUTION': return ' This change provides gradual improvements, security, or compliance updates.';
|
|
346
|
+
case 'REFACTOR': return ' This change focuses on code re-structuring without altering external behavior.';
|
|
347
|
+
case 'ADAPTATION': return ' This change adjusts the system to new environmental or integration constraints.';
|
|
348
|
+
default: return ' This change modifies the system architecture.';
|
|
349
|
+
}
|
|
350
|
+
})()}
|
|
351
|
+
</p>
|
|
352
|
+
</div>
|
|
353
|
+
</section>
|
|
354
|
+
</div>
|
|
355
|
+
</div>
|
|
356
|
+
) : (
|
|
357
|
+
<div className="h-full flex flex-col items-center justify-center opacity-20 gap-4">
|
|
358
|
+
<GitCommit size={64} className="animate-pulse" />
|
|
359
|
+
<div className="text-[10px] uppercase tracking-[0.5em] font-bold">Select a change for analysis</div>
|
|
360
|
+
</div>
|
|
361
|
+
)}
|
|
362
|
+
</div>
|
|
363
|
+
</div>
|
|
364
|
+
);
|
|
365
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from 'react';
|
|
2
|
+
import Markdown from 'react-markdown';
|
|
3
|
+
import mermaid from 'mermaid';
|
|
4
|
+
import { useStore } from '../store';
|
|
5
|
+
|
|
6
|
+
mermaid.initialize({
|
|
7
|
+
startOnLoad: false,
|
|
8
|
+
theme: 'base',
|
|
9
|
+
themeVariables: {
|
|
10
|
+
background: 'transparent',
|
|
11
|
+
fontFamily: 'ui-sans-serif, system-ui, sans-serif',
|
|
12
|
+
primaryColor: '#0a0a0a',
|
|
13
|
+
primaryTextColor: '#e0e0e0',
|
|
14
|
+
primaryBorderColor: '#10b981', // emerald-500
|
|
15
|
+
lineColor: '#34d399', // emerald-400
|
|
16
|
+
secondaryColor: '#064e3b', // emerald-900
|
|
17
|
+
tertiaryColor: '#111111',
|
|
18
|
+
noteBkgColor: '#022c22', // emerald-950
|
|
19
|
+
noteTextColor: '#e0e0e0',
|
|
20
|
+
noteBorderColor: '#059669', // emerald-600
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const Mermaid = ({ chart }: { chart: string }) => {
|
|
25
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
let mounted = true;
|
|
29
|
+
if (containerRef.current) {
|
|
30
|
+
mermaid.render(`mermaid-${Math.random().toString(36).substring(2, 9)}`, chart)
|
|
31
|
+
.then(({ svg }) => {
|
|
32
|
+
if (mounted && containerRef.current) {
|
|
33
|
+
containerRef.current.innerHTML = svg;
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
.catch(err => {
|
|
37
|
+
console.error("Mermaid parsing issue", err);
|
|
38
|
+
if (mounted && containerRef.current) {
|
|
39
|
+
containerRef.current.innerHTML = `<div class="text-red-400 text-xs p-2 border border-red-500/20 bg-red-950/30">Failed to render Mermaid diagram</div>`;
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return () => { mounted = false; };
|
|
44
|
+
}, [chart]);
|
|
45
|
+
|
|
46
|
+
return <div ref={containerRef} className="my-6 flex justify-center bg-transparent border border-white/5 p-6 rounded-lg overflow-auto w-full mermaid-wrapper" />;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const MarkdownRenderer = ({ content }: { content: string }) => {
|
|
50
|
+
const { setSelectedArtifact, setView } = useStore();
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div className="markdown-body prose prose-invert prose-sm max-w-none text-[#e0e0e0]/80">
|
|
54
|
+
<Markdown
|
|
55
|
+
components={{
|
|
56
|
+
code({node, className, children, ...props}: any) {
|
|
57
|
+
const match = /language-(\w+)/.exec(className || '');
|
|
58
|
+
if (match && match[1] === 'mermaid') {
|
|
59
|
+
return <Mermaid chart={String(children).replace(/\n$/, '')} />;
|
|
60
|
+
}
|
|
61
|
+
return match ? (
|
|
62
|
+
<code className={className} {...props}>{children}</code>
|
|
63
|
+
) : (
|
|
64
|
+
<code className="bg-white/10 px-1 py-0.5 rounded text-emerald-400 font-mono text-[11px] whitespace-pre-wrap" {...props}>{children}</code>
|
|
65
|
+
);
|
|
66
|
+
},
|
|
67
|
+
pre({children, ...props}: any) {
|
|
68
|
+
const isMermaid = (children as any)?.props?.className?.includes('language-mermaid');
|
|
69
|
+
if (isMermaid) return <>{children}</>;
|
|
70
|
+
return <pre className="bg-[#050505] p-3 rounded-md overflow-x-auto text-xs my-3 border border-white/10" {...props}>{children}</pre>;
|
|
71
|
+
},
|
|
72
|
+
p({children, ...props}: any) {
|
|
73
|
+
return <p className="mb-3 leading-relaxed" {...props}>{children}</p>;
|
|
74
|
+
},
|
|
75
|
+
ul({children, ...props}: any) {
|
|
76
|
+
return <ul className="list-disc pl-5 mb-3 space-y-1" {...props}>{children}</ul>;
|
|
77
|
+
},
|
|
78
|
+
ol({children, ...props}: any) {
|
|
79
|
+
return <ol className="list-decimal pl-5 mb-3 space-y-1" {...props}>{children}</ol>;
|
|
80
|
+
},
|
|
81
|
+
h1({children, ...props}: any) {
|
|
82
|
+
return <h1 className="text-xl font-serif text-white mt-6 mb-3" {...props}>{children}</h1>;
|
|
83
|
+
},
|
|
84
|
+
h2({children, ...props}: any) {
|
|
85
|
+
return <h2 className="text-lg font-serif text-white mt-5 mb-2 border-b border-white/10 pb-1" {...props}>{children}</h2>;
|
|
86
|
+
},
|
|
87
|
+
h3({children, ...props}: any) {
|
|
88
|
+
return <h3 className="text-base font-serif text-white mt-4 mb-2" {...props}>{children}</h3>;
|
|
89
|
+
},
|
|
90
|
+
h4({children, ...props}: any) {
|
|
91
|
+
return <h4 className="text-sm font-semibold text-white mt-3 mb-1" {...props}>{children}</h4>;
|
|
92
|
+
},
|
|
93
|
+
a({href, children, ...props}: any) {
|
|
94
|
+
const isInternal = href?.startsWith('#') || (!href?.startsWith('http') && !href?.startsWith('mailto') && !href?.startsWith('tel'));
|
|
95
|
+
|
|
96
|
+
if (isInternal) {
|
|
97
|
+
const targetId = href.replace(/^#/, '');
|
|
98
|
+
return (
|
|
99
|
+
<button
|
|
100
|
+
onClick={(e) => {
|
|
101
|
+
e.preventDefault();
|
|
102
|
+
setSelectedArtifact(targetId);
|
|
103
|
+
setView('docs');
|
|
104
|
+
}}
|
|
105
|
+
className="text-emerald-400 hover:text-emerald-300 underline cursor-pointer font-medium p-0 bg-transparent border-0 inline"
|
|
106
|
+
>
|
|
107
|
+
{children}
|
|
108
|
+
</button>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return <a href={href} target="_blank" rel="noreferrer" className="text-blue-400 hover:text-blue-300 underline" {...props}>{children}</a>;
|
|
113
|
+
}
|
|
114
|
+
}}
|
|
115
|
+
>
|
|
116
|
+
{content}
|
|
117
|
+
</Markdown>
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
};
|