concept-atlas-dense-explain 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.
@@ -0,0 +1,479 @@
1
+ import React from 'react';
2
+ import mermaid from 'mermaid';
3
+
4
+ let mermaidReady = false;
5
+
6
+ function ensureMermaid() {
7
+ if (mermaidReady) return;
8
+ mermaid.initialize({
9
+ startOnLoad: false,
10
+ securityLevel: 'loose',
11
+ theme: 'base',
12
+ look: 'handDrawn',
13
+ themeVariables: {
14
+ primaryColor: '#172554',
15
+ primaryTextColor: '#e0f2fe',
16
+ primaryBorderColor: '#38bdf8',
17
+ lineColor: '#64748b',
18
+ secondaryColor: '#172554',
19
+ tertiaryColor: '#0f172a',
20
+ fontFamily: 'Plus Jakarta Sans, sans-serif',
21
+ },
22
+ });
23
+ mermaidReady = true;
24
+ }
25
+
26
+ // Data Layer Components
27
+ export function ExplainPage({ id, title, summary, layout = 'editorial', density = 'reading', children }) {
28
+ return <div data-component="ExplainPage" data-id={id} data-title={title} data-summary={summary} data-layout={layout} data-density={density}>{children}</div>;
29
+ }
30
+ ExplainPage.displayName = 'ExplainPage';
31
+
32
+ export function ConceptGraph({ root, children }) {
33
+ return <div data-component="ConceptGraph" data-root={root}>{children}</div>;
34
+ }
35
+ ConceptGraph.displayName = 'ConceptGraph';
36
+
37
+ export function ConceptNode({ id, title, level = 'L2', parent = null, children, input, output, summary }) {
38
+ return <div data-component="ConceptNode" data-id={id} data-title={title} data-level={level} data-parent={parent}>{children}</div>;
39
+ }
40
+ ConceptNode.displayName = 'ConceptNode';
41
+
42
+ export function ConceptRef({ id }) {
43
+ return <span data-component="ConceptRef" data-id={id} />;
44
+ }
45
+ ConceptRef.displayName = 'ConceptRef';
46
+
47
+ export function Children({ children }) {
48
+ return <div data-component="Children">{children}</div>;
49
+ }
50
+ Children.displayName = 'Children';
51
+
52
+ export function Relation({ from, to, type = 'depends-on', label, description, children }) {
53
+ return <div data-component="Relation" data-from={from} data-to={to} data-type={type} data-label={label}>{children || description}</div>;
54
+ }
55
+ Relation.displayName = 'Relation';
56
+
57
+ // Content Layer Components
58
+ export function Overview({ children }) {
59
+ return <div className="semantic-overview">{children}</div>;
60
+ }
61
+ Overview.displayName = 'Overview';
62
+
63
+ export function Definition({ children }) {
64
+ return <div className="semantic-definition">{children}</div>;
65
+ }
66
+ Definition.displayName = 'Definition';
67
+
68
+ export function Mechanism({ children }) {
69
+ return <div className="semantic-mechanism">{children}</div>;
70
+ }
71
+ Mechanism.displayName = 'Mechanism';
72
+
73
+ export function Implementation({ language = 'text', title = '实现代码', children }) {
74
+ return (
75
+ <div className="semantic-implementation" data-language={language}>
76
+ {title && <div className="impl-header">{title} <span className="lang-badge">{language}</span></div>}
77
+ <pre><code>{typeof children === 'string' ? children.trim() : children}</code></pre>
78
+ </div>
79
+ );
80
+ }
81
+ Implementation.displayName = 'Implementation';
82
+
83
+ export function Boundary({ title = '边界与约束', children }) {
84
+ return (
85
+ <div className="semantic-boundary">
86
+ <div className="semantic-tag">⚠ {title}</div>
87
+ <div className="boundary-body">{children}</div>
88
+ </div>
89
+ );
90
+ }
91
+ Boundary.displayName = 'Boundary';
92
+
93
+ export function Example({ title = '典型示例', children }) {
94
+ return (
95
+ <div className="semantic-example">
96
+ <div className="semantic-tag">✦ {title}</div>
97
+ <div className="example-body">{children}</div>
98
+ </div>
99
+ );
100
+ }
101
+ Example.displayName = 'Example';
102
+
103
+ export function Counterexample({ title = '反例与误区', children }) {
104
+ return (
105
+ <div className="semantic-counterexample">
106
+ <div className="semantic-tag">✕ {title}</div>
107
+ <div className="counter-body">{children}</div>
108
+ </div>
109
+ );
110
+ }
111
+ Counterexample.displayName = 'Counterexample';
112
+
113
+ export function Prerequisite({ children }) {
114
+ return <div className="semantic-prerequisite">{children}</div>;
115
+ }
116
+ Prerequisite.displayName = 'Prerequisite';
117
+
118
+ export function Input({ children }) {
119
+ return <div className="semantic-input">{children}</div>;
120
+ }
121
+ Input.displayName = 'Input';
122
+
123
+ export function Output({ children }) {
124
+ return <div className="semantic-output">{children}</div>;
125
+ }
126
+ Output.displayName = 'Output';
127
+
128
+ export function Glossary({ term, children }) {
129
+ return (
130
+ <div className="semantic-glossary-item">
131
+ <span className="term">{term}</span>: <span className="def">{children}</span>
132
+ </div>
133
+ );
134
+ }
135
+ Glossary.displayName = 'Glossary';
136
+
137
+ // Presentation Semantic Layer Components (for structuring inside nodes without direct CSS)
138
+ export function Compare({ items = [], children }) {
139
+ if (items && items.length > 0) {
140
+ return (
141
+ <div className="semantic-compare-table">
142
+ <table className="compare-grid">
143
+ <thead>
144
+ <tr>
145
+ <th>维度 / 对象</th>
146
+ {items.map((it, idx) => (
147
+ <th key={idx}>{it.label || it.title || `方案 ${idx + 1}`}</th>
148
+ ))}
149
+ </tr>
150
+ </thead>
151
+ <tbody>
152
+ {items[0]?.rows ? (
153
+ items[0].rows.map((rowKey, rIdx) => (
154
+ <tr key={rIdx}>
155
+ <td className="compare-row-label">{rowKey}</td>
156
+ {items.map((it, idx) => (
157
+ <td key={idx}>{it.values?.[rIdx] || '-'}</td>
158
+ ))}
159
+ </tr>
160
+ ))
161
+ ) : null}
162
+ </tbody>
163
+ </table>
164
+ </div>
165
+ );
166
+ }
167
+ return <div className="semantic-compare-block">{children}</div>;
168
+ }
169
+ Compare.displayName = 'Compare';
170
+
171
+ export function DecisionMatrix({ title = '权衡矩阵', headers = [], rows = [], children }) {
172
+ if (headers.length > 0 && rows.length > 0) {
173
+ return (
174
+ <div className="semantic-decision-matrix">
175
+ {title && <div className="matrix-title">⚖ {title}</div>}
176
+ <table className="matrix-table">
177
+ <thead>
178
+ <tr>
179
+ {headers.map((h, i) => <th key={i}>{h}</th>)}
180
+ </tr>
181
+ </thead>
182
+ <tbody>
183
+ {rows.map((row, i) => (
184
+ <tr key={i}>
185
+ {row.map((cell, j) => <td key={j}>{cell}</td>)}
186
+ </tr>
187
+ ))}
188
+ </tbody>
189
+ </table>
190
+ </div>
191
+ );
192
+ }
193
+ return <div className="semantic-decision-matrix">{children}</div>;
194
+ }
195
+ DecisionMatrix.displayName = 'DecisionMatrix';
196
+
197
+ export function Flow({ steps = [], children }) {
198
+ if (steps.length > 0) {
199
+ return (
200
+ <div className="semantic-flow-steps">
201
+ {steps.map((step, idx) => (
202
+ <div key={idx} className="flow-step">
203
+ <span className="step-num">{idx + 1}</span>
204
+ <span className="step-text">{step}</span>
205
+ {idx < steps.length - 1 && <span className="step-arrow">→</span>}
206
+ </div>
207
+ ))}
208
+ </div>
209
+ );
210
+ }
211
+ return <div className="semantic-flow-block">{children}</div>;
212
+ }
213
+ Flow.displayName = 'Flow';
214
+
215
+ export function Timeline({ events = [], children }) {
216
+ if (events.length > 0) {
217
+ return (
218
+ <div className="semantic-timeline">
219
+ {events.map((ev, i) => (
220
+ <div key={i} className="timeline-item">
221
+ <div className="timeline-point" />
222
+ <div className="timeline-label">{ev.label || ev.time}</div>
223
+ <div className="timeline-content">{ev.content || ev.desc}</div>
224
+ </div>
225
+ ))}
226
+ </div>
227
+ );
228
+ }
229
+ return <div className="semantic-timeline">{children}</div>;
230
+ }
231
+ Timeline.displayName = 'Timeline';
232
+
233
+ export function Callout({ type = 'info', title, children }) {
234
+ return (
235
+ <div className={`semantic-callout callout-${type}`}>
236
+ {title && <div className="callout-title">{title}</div>}
237
+ <div className="callout-content">{children}</div>
238
+ </div>
239
+ );
240
+ }
241
+ Callout.displayName = 'Callout';
242
+
243
+ export function Details({ summary = '详细展开', children }) {
244
+ return (
245
+ <details className="semantic-details">
246
+ <summary>{summary}</summary>
247
+ <div className="details-body">{children}</div>
248
+ </details>
249
+ );
250
+ }
251
+ Details.displayName = 'Details';
252
+
253
+ export function LearningObjectives({ items = [], children }) {
254
+ const goals = Array.isArray(items) ? items.filter(Boolean) : [];
255
+ return (
256
+ <section className="semantic-learning-objectives">
257
+ <div className="semantic-tag">◎ 学习目标</div>
258
+ {goals.length > 0 ? <ul>{goals.map((item, index) => <li key={index}>{item}</li>)}</ul> : children}
259
+ </section>
260
+ );
261
+ }
262
+ LearningObjectives.displayName = 'LearningObjectives';
263
+
264
+ export function KeyQuestion({ children }) {
265
+ return (
266
+ <aside className="semantic-key-question">
267
+ <span className="semantic-tag">? 引导问题</span>
268
+ <strong>{children}</strong>
269
+ </aside>
270
+ );
271
+ }
272
+ KeyQuestion.displayName = 'KeyQuestion';
273
+
274
+ export function Evidence({ command, observes, children }) {
275
+ return (
276
+ <div className="semantic-evidence">
277
+ <div className="semantic-tag">⌕ 可验证证据</div>
278
+ {command && <code className="evidence-command">{command}</code>}
279
+ {(observes || children) && <div className="evidence-observes">{observes || children}</div>}
280
+ </div>
281
+ );
282
+ }
283
+ Evidence.displayName = 'Evidence';
284
+
285
+ export function Invariant({ title = '不变量', children }) {
286
+ return (
287
+ <div className="semantic-invariant">
288
+ <div className="semantic-tag">◆ {title}</div>
289
+ <div>{children}</div>
290
+ </div>
291
+ );
292
+ }
293
+ Invariant.displayName = 'Invariant';
294
+
295
+ export function FailureMode({ symptom, cause, evidence, remedy, children }) {
296
+ const rows = [['现象', symptom], ['原因', cause], ['证据', evidence], ['建议', remedy]].filter(([, value]) => value);
297
+ return (
298
+ <div className="semantic-failure-mode">
299
+ <div className="semantic-tag">⚠ 故障模式</div>
300
+ {rows.length > 0 && <dl>{rows.map(([label, value]) => <React.Fragment key={label}><dt>{label}</dt><dd>{value}</dd></React.Fragment>)}</dl>}
301
+ {children && <div className="failure-details">{children}</div>}
302
+ </div>
303
+ );
304
+ }
305
+ FailureMode.displayName = 'FailureMode';
306
+
307
+ export function Tradeoff({ title = '工程权衡', options = [], children }) {
308
+ return (
309
+ <div className="semantic-tradeoff">
310
+ <div className="semantic-tag">⚖ {title}</div>
311
+ {options.length > 0 ? (
312
+ <div className="tradeoff-options">
313
+ {options.map((option, index) => (
314
+ <div className="tradeoff-option" key={index}>
315
+ <strong>{option.name || option.label || `方案 ${index + 1}`}</strong>
316
+ {option.benefit && <span><b>收益</b>{option.benefit}</span>}
317
+ {option.cost && <span><b>代价</b>{option.cost}</span>}
318
+ {option.when && <span><b>适用</b>{option.when}</span>}
319
+ </div>
320
+ ))}
321
+ </div>
322
+ ) : children}
323
+ </div>
324
+ );
325
+ }
326
+ Tradeoff.displayName = 'Tradeoff';
327
+
328
+ export function Columns({ children }) {
329
+ return <div className="semantic-columns">{children}</div>;
330
+ }
331
+ Columns.displayName = 'Columns';
332
+
333
+ /** Optional layout primitives. They express intent while the template owns the CSS. */
334
+ export function Stack({ gap = 'md', children }) {
335
+ return <div className={`semantic-stack gap-${gap}`}>{children}</div>;
336
+ }
337
+ Stack.displayName = 'Stack';
338
+
339
+ export function Grid({ columns = 'auto', gap = 'md', children }) {
340
+ return <div className={`semantic-grid grid-${columns} gap-${gap}`}>{children}</div>;
341
+ }
342
+ Grid.displayName = 'Grid';
343
+
344
+ export function Split({ ratio = '1fr 1fr', children }) {
345
+ return <div className="semantic-split" style={{ '--split-ratio': ratio }}>{children}</div>;
346
+ }
347
+ Split.displayName = 'Split';
348
+
349
+ export function Tabs({ items = [], children }) {
350
+ return <div className="semantic-tabs" data-tab-count={items.length || undefined}>{items.length ? items.map((item, i) => <details key={i} open={i === 0}><summary>{item.label || item.title}</summary><div>{item.content}</div></details>) : children}</div>;
351
+ }
352
+ Tabs.displayName = 'Tabs';
353
+
354
+ export function Mermaid({ chart = '', title = '关系草图', width = 'auto', height = 'auto', x = 0, y = 0, position = 'flow' }) {
355
+ const ref = React.useRef(null);
356
+ const id = React.useId().replace(/:/g, '');
357
+ const [error, setError] = React.useState('');
358
+ const [scale, setScale] = React.useState(1);
359
+ const [pan, setPan] = React.useState({ x: 0, y: 0 });
360
+
361
+ React.useEffect(() => {
362
+ let cancelled = false;
363
+ async function renderChart() {
364
+ if (!ref.current || !chart.trim()) return;
365
+ try {
366
+ ensureMermaid();
367
+ const { svg } = await mermaid.render(`mermaid-${id}`, chart.trim());
368
+ if (!cancelled && ref.current) {
369
+ ref.current.innerHTML = svg;
370
+ setError('');
371
+ }
372
+ } catch (err) {
373
+ if (!cancelled) setError(err.message || 'Mermaid 图表语法错误');
374
+ }
375
+ }
376
+ renderChart();
377
+ return () => { cancelled = true; };
378
+ }, [chart, id]);
379
+
380
+ const zoom = (delta) => setScale(value => Math.max(0.5, Math.min(3, +(value + delta).toFixed(2))));
381
+ const reset = () => { setScale(1); setPan({ x: 0, y: 0 }); };
382
+ return (
383
+ <div className={`semantic-mermaid ${widgetClass(position)}`} style={widgetStyle({ width, height, x, y, position })}>
384
+ <div className="semantic-widget-head">
385
+ <span>{title}</span>
386
+ <div className="mermaid-tools"><code>MERMAID</code><button type="button" onClick={() => zoom(-0.1)} aria-label="缩小图表">−</button><span>{Math.round(scale * 100)}%</span><button type="button" onClick={() => zoom(0.1)} aria-label="放大图表">+</button><button type="button" onClick={reset} aria-label="重置图表">↺</button></div>
387
+ </div>
388
+ {error ? <pre className="mermaid-error">{error}</pre> : <div className="mermaid-canvas" onWheel={(event) => { event.preventDefault(); zoom(event.deltaY < 0 ? 0.1 : -0.1); }}><div className="mermaid-canvas-inner" style={{ transform: `translate(${pan.x}px, ${pan.y}px) scale(${scale})` }} ref={ref} /></div>}
389
+ </div>
390
+ );
391
+ }
392
+ Mermaid.displayName = 'Mermaid';
393
+
394
+ export function RelationMap({ title = '关系速览', items = [], children, width = 'auto', height = 'auto', x = 0, y = 0, position = 'flow' }) {
395
+ return (
396
+ <div className={`semantic-relation-map ${widgetClass(position)}`} style={widgetStyle({ width, height, x, y, position })}>
397
+ <div className="semantic-widget-head"><span>{title}</span><code>RELATIONS</code></div>
398
+ {items.length > 0 ? (
399
+ <div className="relation-map-grid">
400
+ {items.map((item, index) => (
401
+ <div className="relation-map-row" key={index}>
402
+ <span className="relation-map-source">{item.from || item.source}</span>
403
+ <span className="relation-map-arrow">{item.type || '→'}</span>
404
+ <span className="relation-map-target">{item.to || item.target}</span>
405
+ {item.note && <small>{item.note}</small>}
406
+ </div>
407
+ ))}
408
+ </div>
409
+ ) : children}
410
+ </div>
411
+ );
412
+ }
413
+ RelationMap.displayName = 'RelationMap';
414
+
415
+ export function RelationPath({ title = '关系链', steps = [], children }) {
416
+ const safeSteps = Array.isArray(steps) ? steps.filter(Boolean) : [];
417
+ return (
418
+ <div className="semantic-relation-path">
419
+ {title && <div className="semantic-widget-head"><span>{title}</span><code>PATH</code></div>}
420
+ {safeSteps.length > 0 ? (
421
+ <div className="relation-path-steps">
422
+ {safeSteps.map((step, index) => (
423
+ <React.Fragment key={index}>
424
+ <div className={`relation-path-node tone-${step.tone || 'info'}`}>
425
+ <span className="relation-path-kicker">{step.level || `0${index + 1}`}</span>
426
+ <strong>{step.node || step.title}</strong>
427
+ {step.note && <small>{step.note}</small>}
428
+ </div>
429
+ {index < safeSteps.length - 1 && <div className="relation-path-edge"><span>{safeSteps[index].relation || '→'}</span></div>}
430
+ </React.Fragment>
431
+ ))}
432
+ </div>
433
+ ) : children}
434
+ </div>
435
+ );
436
+ }
437
+ RelationPath.displayName = 'RelationPath';
438
+
439
+ export function Insight({ title = '关键判断', tone = 'info', children, width = 'auto', height = 'auto', x = 0, y = 0, position = 'flow' }) {
440
+ return (
441
+ <aside className={`semantic-insight insight-${tone} ${widgetClass(position)}`} style={widgetStyle({ width, height, x, y, position })}>
442
+ <div className="insight-kicker">{tone === 'warn' ? '⚠' : tone === 'success' ? '✓' : '◆'} {title}</div>
443
+ <div className="insight-body">{children}</div>
444
+ </aside>
445
+ );
446
+ }
447
+ Insight.displayName = 'Insight';
448
+
449
+ export function NoteGrid({ notes = [], children, width = 'auto', height = 'auto', x = 0, y = 0, position = 'flow' }) {
450
+ return (
451
+ <div className={`semantic-note-grid ${widgetClass(position)}`} style={widgetStyle({ width, height, x, y, position })}>
452
+ {notes.length > 0 ? notes.map((note, index) => (
453
+ <div className="semantic-note" key={index}>
454
+ <strong>{note.title || note.label}</strong>
455
+ <span>{note.content || note.text || note.description}</span>
456
+ </div>
457
+ )) : children}
458
+ </div>
459
+ );
460
+ }
461
+ NoteGrid.displayName = 'NoteGrid';
462
+
463
+ function widgetStyle({ width, height, x, y, position }) {
464
+ const style = {};
465
+ if (width && width !== 'auto') style.width = width;
466
+ if (height && height !== 'auto') style.height = height;
467
+ if (position === 'absolute') {
468
+ style.position = 'absolute';
469
+ style.left = `${x}px`;
470
+ style.top = `${y}px`;
471
+ } else if (x || y) {
472
+ style.transform = `translate(${x}px, ${y}px)`;
473
+ }
474
+ return style;
475
+ }
476
+
477
+ function widgetClass(position) {
478
+ return position === 'absolute' ? 'semantic-widget-absolute' : '';
479
+ }
@@ -0,0 +1 @@
1
+ export * from './MDXComponents.jsx';
@@ -0,0 +1,16 @@
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+ import { App } from './app/App.jsx';
4
+ import * as Components from './components/index.js';
5
+ import CompileRuntimeDoc from '../content/compile-runtime.mdx';
6
+
7
+ // Mount MDX application to DOM
8
+ const rootElement = document.getElementById('root') || document.getElementById('app');
9
+ if (rootElement) {
10
+ const root = ReactDOM.createRoot(rootElement);
11
+ root.render(
12
+ <React.StrictMode>
13
+ <App mdxContent={<CompileRuntimeDoc components={Components} />} />
14
+ </React.StrictMode>
15
+ );
16
+ }
@@ -0,0 +1,169 @@
1
+ import React from 'react';
2
+ import { RELATION_TYPES, LEVEL_DEFS } from './relation-types.js';
3
+
4
+ /**
5
+ * Creates an empty Concept Knowledge Graph structure
6
+ */
7
+ export function createConceptGraphState() {
8
+ return {
9
+ meta: {
10
+ id: '',
11
+ title: '',
12
+ summary: '',
13
+ rootId: ''
14
+ },
15
+ nodes: new Map(), // id -> NodeObject
16
+ relations: [], // Array<{ from, to, type, label, description }>
17
+ };
18
+ }
19
+
20
+ /**
21
+ * Normalizes raw node data extracted from MDX AST or React component tree
22
+ */
23
+ export function normalizeNode(raw) {
24
+ const id = raw.id || String(Math.random());
25
+ return {
26
+ id,
27
+ title: raw.title || id,
28
+ level: raw.level || 'L2',
29
+ parent: raw.parent || null,
30
+ children: Array.isArray(raw.children) ? raw.children : [],
31
+
32
+ // Semantic Content Sections
33
+ summary: raw.summary || '',
34
+ overview: raw.overview || null,
35
+ definition: raw.definition || null,
36
+ mechanism: raw.mechanism || null,
37
+ input: raw.input || null,
38
+ output: raw.output || null,
39
+ prerequisites: raw.prerequisites || [],
40
+ implementation: raw.implementation || null, // { language, code } or React node
41
+ examples: raw.examples || [],
42
+ counterexamples: raw.counterexamples || [],
43
+ boundaries: raw.boundaries || [],
44
+ glossary: raw.glossary || [],
45
+ customSections: raw.customSections || [],
46
+ };
47
+ }
48
+
49
+ /**
50
+ * Validates and indexes nodes and relations to build a graph model
51
+ */
52
+ export function buildGraphModel(rawGraph) {
53
+ const nodes = new Map();
54
+ const diagnostics = [];
55
+ const rawNodes = rawGraph.nodes || [];
56
+ const rawRelations = rawGraph.relations || [];
57
+
58
+ // Register all nodes
59
+ rawNodes.forEach(n => {
60
+ const node = normalizeNode(n);
61
+ if (nodes.has(node.id)) diagnostics.push({ level: 'error', code: 'DUPLICATE_NODE_ID', nodeId: node.id });
62
+ nodes.set(node.id, node);
63
+ });
64
+
65
+ // Build tree hierarchy: ensure parent <-> children consistency
66
+ nodes.forEach(node => {
67
+ if (node.parent && nodes.has(node.parent)) {
68
+ const parentNode = nodes.get(node.parent);
69
+ if (!parentNode.children.includes(node.id)) {
70
+ parentNode.children.push(node.id);
71
+ }
72
+ }
73
+ });
74
+
75
+ // Explicit children references (from <ConceptRef />)
76
+ nodes.forEach(node => {
77
+ node.children.forEach(childId => {
78
+ if (nodes.has(childId)) {
79
+ const childNode = nodes.get(childId);
80
+ if (!childNode.parent) {
81
+ childNode.parent = node.id;
82
+ }
83
+ }
84
+ else diagnostics.push({ level: 'warning', code: 'MISSING_CHILD', nodeId: node.id, targetId: childId });
85
+ });
86
+ if (node.parent && !nodes.has(node.parent)) diagnostics.push({ level: 'warning', code: 'ORPHAN_NODE', nodeId: node.id, targetId: node.parent });
87
+ if (!LEVEL_DEFS[node.level]) diagnostics.push({ level: 'warning', code: 'UNKNOWN_LEVEL', nodeId: node.id, level: node.level });
88
+ });
89
+
90
+ // Filter valid relations
91
+ const validRelations = [];
92
+ rawRelations.forEach(rel => {
93
+ if (nodes.has(rel.from) && nodes.has(rel.to)) {
94
+ const typeDef = RELATION_TYPES[rel.type] || {
95
+ label: rel.type,
96
+ color: '#94a3b8',
97
+ hasArrow: true
98
+ };
99
+ validRelations.push({
100
+ ...rel,
101
+ typeLabel: rel.label || typeDef.label,
102
+ typeInfo: typeDef
103
+ });
104
+ }
105
+ });
106
+
107
+ // Determine root
108
+ let rootId = rawGraph.meta?.rootId;
109
+ if (!rootId || !nodes.has(rootId)) {
110
+ // Find node with no parent or level L0
111
+ for (const [id, node] of nodes.entries()) {
112
+ if (!node.parent || node.level === 'L0') {
113
+ rootId = id;
114
+ break;
115
+ }
116
+ }
117
+ }
118
+
119
+ return {
120
+ meta: {
121
+ ...rawGraph.meta,
122
+ rootId: rootId || (nodes.size > 0 ? Array.from(nodes.keys())[0] : null)
123
+ },
124
+ nodes,
125
+ relations: validRelations,
126
+ diagnostics
127
+ };
128
+ }
129
+
130
+ /**
131
+ * Helper to compute ancestor path from root to current node
132
+ */
133
+ export function getAncestorPath(nodes, nodeId) {
134
+ const path = [];
135
+ let current = nodes.get(nodeId);
136
+ const visited = new Set();
137
+ while (current && !visited.has(current.id)) {
138
+ visited.add(current.id);
139
+ path.unshift(current);
140
+ if (!current.parent) break;
141
+ current = nodes.get(current.parent);
142
+ }
143
+ return path;
144
+ }
145
+
146
+ /**
147
+ * Helper to get siblings of a node
148
+ */
149
+ export function getSiblingNodes(nodes, nodeId) {
150
+ const current = nodes.get(nodeId);
151
+ if (!current || !current.parent) {
152
+ // If root or has no parent, find other root nodes
153
+ return Array.from(nodes.values()).filter(n => !n.parent && n.id !== nodeId);
154
+ }
155
+ const parent = nodes.get(current.parent);
156
+ if (!parent) return [];
157
+ return parent.children
158
+ .filter(id => id !== nodeId && nodes.has(id))
159
+ .map(id => nodes.get(id));
160
+ }
161
+
162
+ /**
163
+ * Helper to get all incoming and outgoing relations for a node
164
+ */
165
+ export function getNodeRelations(relations, nodeId) {
166
+ const incoming = relations.filter(r => r.to === nodeId);
167
+ const outgoing = relations.filter(r => r.from === nodeId);
168
+ return { incoming, outgoing };
169
+ }