knowledge-graph-preview / cli /analyzer /layer-detector.js
mr4's picture
Upload 136 files
fd8cdf5 verified
/** Known tag-to-layer mappings in priority order */
const TAG_LAYER_MAP = {
component: { id: 'presentation', name: 'Presentation', description: 'UI components, views, and pages' },
api: { id: 'api', name: 'API', description: 'Routes, controllers, and API endpoints' },
model: { id: 'data', name: 'Data', description: 'Models, entities, and data schemas' },
util: { id: 'utilities', name: 'Utilities', description: 'Utility functions, helpers, and shared libraries' },
test: { id: 'testing', name: 'Testing', description: 'Test files and test utilities' },
};
/**
* Detects architectural layers by grouping nodes based on tags
* and directory structure.
*
* Layer detection strategy (priority order):
* 1. Check node tags for known patterns (component, api, model, util, test)
* 2. Fall back to grouping by first directory segment
* 3. Root files (no directory) go to "Root" layer
*
* @param nodes - All graph nodes to classify into layers
* @param edges - Graph edges (unused, reserved for future dependency analysis)
* @returns Array of detected layers with assigned node IDs
*/
export function detectLayers(nodes, edges) {
void edges;
// Map from layer id → { name, description, nodeIds }
const layerMap = new Map();
for (const node of nodes) {
// Only process file nodes and nodes with "::" (function/class children)
if (node.type !== 'file' && !node.id.includes('::')) {
continue;
}
// For function/class nodes (contain "::"), extract the file path
const filePath = node.id.includes('::') ? node.id.split('::')[0] : node.id;
// Determine layer from tags first
const layer = getLayerFromTags(node, nodes, filePath) ?? getLayerFromPath(filePath);
if (!layerMap.has(layer.id)) {
layerMap.set(layer.id, { name: layer.name, description: layer.description, nodeIds: [] });
}
layerMap.get(layer.id).nodeIds.push(node.id);
}
// Convert map to array, skip empty layers
const layers = [];
for (const [id, data] of layerMap) {
if (data.nodeIds.length > 0) {
layers.push({ id, name: data.name, description: data.description, nodeIds: data.nodeIds });
}
}
return layers;
}
/**
* Attempts to determine the layer from a node's tags.
* For child nodes (function/class), looks up the parent file node's tags.
*/
function getLayerFromTags(node, allNodes, filePath) {
// For file nodes, check their own tags
if (node.type === 'file') {
return matchTagToLayer(node.tags);
}
// For function/class nodes, find the parent file node and use its tags
const parentFile = allNodes.find(n => n.type === 'file' && n.id === filePath);
if (parentFile) {
return matchTagToLayer(parentFile.tags);
}
return null;
}
/**
* Matches a tags array against known layer patterns.
*/
function matchTagToLayer(tags) {
for (const tag of Object.keys(TAG_LAYER_MAP)) {
if (tags.includes(tag)) {
return TAG_LAYER_MAP[tag];
}
}
return null;
}
/**
* Determines the layer from the file path's first directory segment.
*/
function getLayerFromPath(filePath) {
const segments = filePath.split('/');
if (segments.length === 1) {
// Root file (no directory)
return { id: 'root', name: 'Root', description: 'Root-level files' };
}
// Find the first meaningful directory segment (skip "src" as it's a common wrapper)
let dirSegment;
if (segments[0] === 'src' && segments.length > 2) {
dirSegment = segments[1];
}
else {
dirSegment = segments[0];
}
const name = capitalize(dirSegment);
const id = dirSegment.toLowerCase();
return { id, name, description: `Files in the ${dirSegment}/ directory` };
}
/**
* Capitalizes the first letter of a string.
*/
function capitalize(str) {
if (!str)
return str;
return str.charAt(0).toUpperCase() + str.slice(1);
}