/** 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); }