262 lines
6.4 KiB
TypeScript
262 lines
6.4 KiB
TypeScript
// src/graph-view.ts
|
|
|
|
import { VaultIndexEntry } from './types';
|
|
|
|
/**
|
|
* Represents a node in the dependency graph
|
|
*/
|
|
export interface GraphNode {
|
|
id: string;
|
|
label: string;
|
|
file: VaultIndexEntry;
|
|
type: 'file' | 'concept';
|
|
properties: Record<string, unknown>;
|
|
}
|
|
|
|
/**
|
|
* Represents an edge in the dependency graph
|
|
*/
|
|
export interface GraphEdge {
|
|
id: string;
|
|
source: string;
|
|
target: string;
|
|
label: string;
|
|
relationship: string;
|
|
properties: Record<string, unknown>;
|
|
}
|
|
|
|
/**
|
|
* Represents a dependency graph structure
|
|
*/
|
|
export interface DependencyGraph {
|
|
nodes: GraphNode[];
|
|
edges: GraphEdge[];
|
|
}
|
|
|
|
/**
|
|
* Graph visualization formats
|
|
*/
|
|
export type GraphFormat = 'dot' | 'json' | 'cytoscape';
|
|
|
|
/**
|
|
* Extracts concepts from file content
|
|
*/
|
|
export function extractConcepts(content: string, _filePath: string): string[] {
|
|
// Extract concepts from headings, bold text, and mentions
|
|
const concepts: string[] = [];
|
|
|
|
// Extract all headings as potential concepts
|
|
const headingMatches = content.match(/^#{1,6} (.*?)$/gm);
|
|
if (headingMatches) {
|
|
headingMatches.forEach((heading) => {
|
|
const concept = heading.replace(/^#{1,6} /, '').trim();
|
|
if (concept && !concepts.includes(concept)) {
|
|
concepts.push(concept);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Extract bold text as potential concepts
|
|
const boldMatches = content.match(/\*\*(.*?)\*\*/g);
|
|
if (boldMatches) {
|
|
boldMatches.forEach((match) => {
|
|
const concept = match.replace(/\*\*/g, '').trim();
|
|
if (concept && !concepts.includes(concept)) {
|
|
concepts.push(concept);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Extract italic text as potential concepts
|
|
const italicMatches = content.match(/\*(.*?)\*/g);
|
|
if (italicMatches) {
|
|
italicMatches.forEach((match) => {
|
|
const concept = match.replace(/\*/g, '').trim();
|
|
if (concept && !concepts.includes(concept)) {
|
|
concepts.push(concept);
|
|
}
|
|
});
|
|
}
|
|
|
|
return concepts;
|
|
}
|
|
|
|
/**
|
|
* Finds relationships between files based on concept mentions
|
|
*/
|
|
export function findRelationships(
|
|
files: VaultIndexEntry[],
|
|
conceptIndex: Record<string, string[]>
|
|
): { source: string; target: string; relationship: string }[] {
|
|
const relationships: { source: string; target: string; relationship: string }[] = [];
|
|
|
|
// For each file, look for concepts that are defined in other files
|
|
files.forEach((file) => {
|
|
const fileContent = file.content;
|
|
const fileConcepts = extractConcepts(fileContent, file.path);
|
|
|
|
fileConcepts.forEach((concept) => {
|
|
// Check if this concept is defined in another file
|
|
if (conceptIndex[concept]) {
|
|
conceptIndex[concept].forEach((definedIn) => {
|
|
if (definedIn !== file.path) {
|
|
relationships.push({
|
|
source: file.path,
|
|
target: definedIn,
|
|
relationship: `mentions "${concept}" which is defined in`,
|
|
});
|
|
}
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
return relationships;
|
|
}
|
|
|
|
/**
|
|
* Builds a dependency graph from vault entries
|
|
*/
|
|
export function buildDependencyGraph(files: VaultIndexEntry[]): DependencyGraph {
|
|
// Create a concept index: concept -> files where it's defined
|
|
const conceptIndex: Record<string, string[]> = {};
|
|
|
|
files.forEach((file) => {
|
|
const concepts = extractConcepts(file.content, file.path);
|
|
concepts.forEach((concept) => {
|
|
if (!conceptIndex[concept]) {
|
|
conceptIndex[concept] = [];
|
|
}
|
|
if (!conceptIndex[concept].includes(file.path)) {
|
|
conceptIndex[concept].push(file.path);
|
|
}
|
|
});
|
|
});
|
|
|
|
// Build graph nodes
|
|
const nodes: GraphNode[] = [];
|
|
const edges: GraphEdge[] = [];
|
|
|
|
// Add file nodes
|
|
files.forEach((file) => {
|
|
nodes.push({
|
|
id: file.path,
|
|
label: file.title || file.path,
|
|
file: file,
|
|
type: 'file' as const,
|
|
properties: {
|
|
path: file.path,
|
|
title: file.title,
|
|
contentPreview: file.content.substring(0, 100) + '.',
|
|
},
|
|
});
|
|
});
|
|
|
|
// Find relationships and add edges
|
|
const fileRelationships = findRelationships(files, conceptIndex);
|
|
|
|
fileRelationships.forEach((rel) => {
|
|
// Only add edge if both source and target files exist
|
|
if (files.some((f) => f.path === rel.source) && files.some((f) => f.path === rel.target)) {
|
|
edges.push({
|
|
id: `${rel.source}--${rel.target}`,
|
|
source: rel.source,
|
|
target: rel.target,
|
|
label: rel.relationship,
|
|
relationship: rel.relationship,
|
|
properties: {
|
|
relationship: rel.relationship,
|
|
},
|
|
});
|
|
}
|
|
});
|
|
|
|
return {
|
|
nodes,
|
|
edges,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Converts dependency graph to DOT format
|
|
*/
|
|
export function toDotFormat(graph: DependencyGraph): string {
|
|
let dot = 'digraph G {\n';
|
|
dot += ' rankdir=LR;\n';
|
|
dot += ' node [shape=box, style=filled, fillcolor=lightblue];\n';
|
|
dot += ' edge [arrowhead=vee];\n\n';
|
|
|
|
// Add nodes
|
|
graph.nodes.forEach((node) => {
|
|
const label = node.label.replace(/"/g, '\\"');
|
|
dot += ` "${node.id}" [label="${label}"];\n`;
|
|
});
|
|
|
|
// Add edges
|
|
graph.edges.forEach((edge) => {
|
|
const label = edge.label.replace(/"/g, '\\"');
|
|
dot += ` "${edge.source}" -> "${edge.target}" [label="${label}"];\n`;
|
|
});
|
|
|
|
dot += '}\n';
|
|
return dot;
|
|
}
|
|
|
|
/**
|
|
* Converts dependency graph to JSON format
|
|
*/
|
|
export function toJsonFormat(graph: DependencyGraph): string {
|
|
return JSON.stringify(graph, null, 2);
|
|
}
|
|
|
|
/**
|
|
* Converts dependency graph to Cytoscape format
|
|
*/
|
|
export function toCytoscapeFormat(graph: DependencyGraph): string {
|
|
const cytoscapeFormat = {
|
|
elements: {
|
|
nodes: graph.nodes.map((node) => ({
|
|
data: {
|
|
id: node.id,
|
|
label: node.label,
|
|
type: node.type,
|
|
...node.properties,
|
|
},
|
|
})),
|
|
edges: graph.edges.map((edge) => ({
|
|
data: {
|
|
id: edge.id,
|
|
source: edge.source,
|
|
target: edge.target,
|
|
label: edge.label,
|
|
relationship: edge.relationship,
|
|
...edge.properties,
|
|
},
|
|
})),
|
|
},
|
|
};
|
|
|
|
return JSON.stringify(cytoscapeFormat, null, 2);
|
|
}
|
|
|
|
/**
|
|
* Generates graph visualization in specified format
|
|
*/
|
|
export function generateGraphVisualization(
|
|
files: VaultIndexEntry[],
|
|
format: GraphFormat = 'dot'
|
|
): string {
|
|
const graph = buildDependencyGraph(files);
|
|
|
|
switch (format) {
|
|
case 'dot':
|
|
return toDotFormat(graph);
|
|
case 'json':
|
|
return toJsonFormat(graph);
|
|
case 'cytoscape':
|
|
return toCytoscapeFormat(graph);
|
|
default:
|
|
return toDotFormat(graph);
|
|
}
|
|
}
|