knowledge-graph-preview / cli /analyzer /tour-builder.js
mr4's picture
Upload 136 files
fd8cdf5 verified
import * as path from 'node:path/posix';
/**
* Entry point file names in priority order.
* The first match found wins.
*/
const ENTRY_POINT_NAMES = [
'index.ts',
'index.tsx',
'index.js',
'main.ts',
'main.py',
'App.tsx',
'App.ts',
'app.ts',
'app.py',
'main.go',
'main.rs',
'Main.java',
];
/**
* Finds the entry point node from the list of file nodes.
*
* Strategy:
* 1. Check for common entry point file names (with and without src/ prefix)
* 2. Fallback: use the file with the most outgoing import edges
*/
export function findEntryPoint(fileNodes, edges) {
if (fileNodes.length === 0)
return undefined;
// Try each entry point name in priority order
for (const entryName of ENTRY_POINT_NAMES) {
const match = fileNodes.find(node => {
const basename = path.basename(node.id);
if (basename !== entryName)
return false;
// Match root-level or src/-level files
const dir = path.dirname(node.id);
return dir === '.' || dir === 'src' || node.id === entryName || node.id === `src/${entryName}`;
});
if (match)
return match;
}
// Fallback: file with the most outgoing import edges
const importEdges = edges.filter(e => e.type === 'imports');
let maxOutgoing = -1;
let bestNode;
for (const node of fileNodes) {
const outgoing = importEdges.filter(e => e.source === node.id).length;
if (outgoing > maxOutgoing) {
maxOutgoing = outgoing;
bestNode = node;
}
}
return bestNode;
}
/**
* Finds the layer a file node belongs to.
*/
function getNodeLayer(nodeId, layers) {
return layers.find(layer => layer.nodeIds.includes(nodeId));
}
/**
* Gets child node IDs (functions/classes) that belong to a file.
* These are nodes whose ID starts with the file path followed by "::".
*/
function getChildNodeIds(fileNodeId, allNodes) {
const prefix = fileNodeId + '::';
return allNodes
.filter(n => n.id.startsWith(prefix))
.map(n => n.id);
}
/**
* Generates a description for a tour step based on the node's summary and layer.
*/
function generateDescription(node, layer) {
const layerInfo = layer ? ` (${layer.name} layer)` : '';
if (node.summary) {
return `${node.summary}${layerInfo}`;
}
return `Source file${layerInfo}`;
}
/**
* Performs BFS from the entry point following import edges.
* Returns file node IDs in BFS order (no duplicates).
*/
function bfsFromEntryPoint(entryPointId, fileNodes, edges, maxSteps) {
const fileNodeIds = new Set(fileNodes.map(n => n.id));
const importEdges = edges.filter(e => e.type === 'imports');
const visited = new Set();
const queue = [entryPointId];
const result = [];
visited.add(entryPointId);
while (queue.length > 0 && result.length < maxSteps) {
const current = queue.shift();
result.push(current);
if (result.length >= maxSteps)
break;
// Find all files imported by the current file
const neighbors = importEdges
.filter(e => e.source === current)
.map(e => e.target)
.filter(target => fileNodeIds.has(target) && !visited.has(target));
for (const neighbor of neighbors) {
visited.add(neighbor);
queue.push(neighbor);
}
}
return result;
}
/**
* Supplements BFS results with representative files from uncovered layers.
* Picks the file with the most edges (most connected) from each uncovered layer.
*/
function supplementWithLayers(bfsFileIds, fileNodes, edges, layers, maxSteps) {
if (layers.length === 0)
return bfsFileIds;
const coveredIds = new Set(bfsFileIds);
// Find which layers are already covered
const coveredLayerIds = new Set();
for (const fileId of bfsFileIds) {
const layer = getNodeLayer(fileId, layers);
if (layer)
coveredLayerIds.add(layer.id);
}
const result = [...bfsFileIds];
const fileNodeIds = new Set(fileNodes.map(n => n.id));
// For each uncovered layer, pick the most connected file
for (const layer of layers) {
if (result.length >= maxSteps)
break;
if (coveredLayerIds.has(layer.id))
continue;
// Get file nodes in this layer
const layerFileIds = layer.nodeIds.filter(id => fileNodeIds.has(id) && !coveredIds.has(id));
if (layerFileIds.length === 0)
continue;
// Pick the file with the most edges (incoming + outgoing)
let bestFileId = layerFileIds[0];
let maxEdges = 0;
for (const fileId of layerFileIds) {
const edgeCount = edges.filter(e => e.source === fileId || e.target === fileId).length;
if (edgeCount > maxEdges) {
maxEdges = edgeCount;
bestFileId = fileId;
}
}
result.push(bestFileId);
coveredIds.add(bestFileId);
coveredLayerIds.add(layer.id);
}
return result;
}
/**
* Generates a guided tour through the project architecture.
*
* Tour generation strategy:
* 1. Find entry point (index.ts, main.py, App.tsx, etc.)
* 2. Follow import chain from entry point (BFS, max 10 steps)
* 3. Each step covers a new layer or key component
* 4. Generate title from file/function name, description from structure
*
* If BFS produces fewer than 5 steps, supplements with one representative
* file from each layer not yet covered (most connected file).
*
* @param nodes - All graph nodes
* @param edges - All graph edges (used to follow import chains)
* @param layers - Detected architectural layers
* @returns Ordered array of tour steps
*/
export function buildTour(nodes, edges, layers) {
// Edge cases
if (nodes.length === 0)
return [];
const fileNodes = nodes.filter(n => n.type === 'file');
if (fileNodes.length === 0)
return [];
const maxSteps = 10;
const minBfsSteps = 5;
// Find entry point
const entryPoint = findEntryPoint(fileNodes, edges);
if (!entryPoint)
return [];
// BFS from entry point
let tourFileIds = bfsFromEntryPoint(entryPoint.id, fileNodes, edges, maxSteps);
// Supplement with layer representatives if BFS is short
if (tourFileIds.length < minBfsSteps) {
tourFileIds = supplementWithLayers(tourFileIds, fileNodes, edges, layers, maxSteps);
}
// Generate tour steps
const steps = [];
for (let i = 0; i < tourFileIds.length; i++) {
const fileId = tourFileIds[i];
const fileNode = fileNodes.find(n => n.id === fileId);
if (!fileNode)
continue;
const layer = getNodeLayer(fileId, layers);
const childIds = getChildNodeIds(fileId, nodes);
steps.push({
order: i + 1,
title: fileNode.name,
description: generateDescription(fileNode, layer),
nodeIds: [fileId, ...childIds],
});
}
return steps;
}