Files
dev-intel-v2/subsystem.js

206 lines
5.9 KiB
JavaScript
Raw Normal View History

const fs = require('fs');
const path = require('path');
const GraphStore = require('./graph.js');
/**
* Phase 7A: Subsystem Aggregator
* Groups files into subsystems and computes cross-subsystem dependency matrix.
*/
/** Normalize absolute paths to relative (strips up to /src/ or configurable srcDir) */
function relPath(file, srcMarker) {
if (!file) return '';
srcMarker = srcMarker || '/src/';
if (file.startsWith('/')) {
const idx = file.indexOf(srcMarker);
if (idx !== -1) return file.substring(idx + srcMarker.length);
return file.split('/').slice(-2).join('/');
}
return file;
}
/** Get subsystem name from a relative file path */
function subsystemOf(relFile) {
const parts = relFile.split('/');
return parts.length > 1 ? parts[0] : 'root';
}
function buildSubsystems(graph, opts = {}) {
const srcMarker = opts.srcDir || '/src/';
const crossCuttingMinTraffic = opts.minTraffic || 10;
const crossCuttingThreshold = opts.crossCuttingThreshold || 0.75;
const subsystems = new Map();
// Build function name → qualified node ID lookup
const funcLookup = new Map();
for (const [id, node] of graph.nodes) {
if (node.type === 'Function' || node.type === 'Class') {
if (!funcLookup.has(node.name)) funcLookup.set(node.name, []);
funcLookup.get(node.name).push(id);
}
}
// 1. Directory-based clustering (using relPath for DRY)
for (const file of graph.fileIndex.keys()) {
const rel = relPath(file, srcMarker);
if (rel.startsWith('dep:')) continue;
const subName = subsystemOf(rel);
if (!subsystems.has(subName)) {
subsystems.set(subName, {
name: subName,
kind: 'domain',
files: new Set(),
entities: { functions: 0, classes: 0, modules: 0 },
publicExports: new Set(),
});
}
const sub = subsystems.get(subName);
sub.files.add(rel);
const ids = graph.fileIndex.get(file);
if (ids) {
for (const id of ids) {
const node = graph.nodes.get(id);
if (!node) continue;
if (node.type === 'Module') sub.entities.modules++;
else if (node.type === 'Function') {
sub.entities.functions++;
if (node.visibility === 'public') sub.publicExports.add(node.name);
}
else if (node.type === 'Class') {
sub.entities.classes++;
if (node.visibility === 'public') sub.publicExports.add(node.name);
}
}
}
}
// 2. Compute inter-subsystem edges
const matrix = {};
const inboundCross = {};
const outboundCross = {};
for (const name of subsystems.keys()) {
inboundCross[name] = 0;
outboundCross[name] = 0;
}
// Cache: nodeId → subsystem name
const nodeSubCache = new Map();
function resolveSubsystem(nodeId) {
if (!nodeId) return null;
if (nodeSubCache.has(nodeId)) return nodeSubCache.get(nodeId);
const rel = relPath(nodeId.split(':')[0], srcMarker);
const sub = subsystemOf(rel);
const result = subsystems.has(sub) ? sub : null;
nodeSubCache.set(nodeId, result);
return result;
}
function resolveCallTarget(callerSub, targetName) {
const candidates = funcLookup.get(targetName);
if (!candidates || candidates.length === 0) return null;
if (candidates.length === 1) return resolveSubsystem(candidates[0]);
let sameSub = null;
let diffSub = null;
for (const cid of candidates) {
const s = resolveSubsystem(cid);
if (s === callerSub) sameSub = s;
else if (s) diffSub = s;
}
return sameSub || diffSub;
}
for (const e of graph.edges) {
if (e.type === 'CONTAINS') continue;
let srcSub = resolveSubsystem(e.source);
if (!srcSub) continue;
let tgtSub = null;
if (e.type === 'IMPORTS') {
const dep = e.target.replace('dep:', '');
tgtSub = subsystemOf(dep);
} else if (e.type === 'CALLS') {
if (e.target.includes('/')) tgtSub = resolveSubsystem(e.target);
else tgtSub = resolveCallTarget(srcSub, e.target);
} else if (e.type === 'IMPLEMENTS') {
if (e.target.includes('/')) tgtSub = resolveSubsystem(e.target);
}
if (!tgtSub || !subsystems.has(tgtSub)) continue;
if (srcSub === tgtSub) continue;
const key = `${srcSub}${tgtSub}`;
if (!matrix[key]) matrix[key] = { calls: 0, imports: 0, via: new Set() };
if (e.type === 'CALLS') matrix[key].calls++;
else if (e.type === 'IMPORTS') matrix[key].imports++;
if (matrix[key].via.size < 5) {
matrix[key].via.add(`${e.source}${e.target}`);
}
inboundCross[tgtSub]++;
outboundCross[srcSub]++;
}
// 3. Cross-cutting detection (high fan-in ratio + minimum traffic)
const crossCutting = [];
for (const name of subsystems.keys()) {
const totalCross = inboundCross[name] + outboundCross[name];
if (totalCross >= crossCuttingMinTraffic) {
const inboundRatio = inboundCross[name] / totalCross;
if (inboundRatio > crossCuttingThreshold) {
subsystems.get(name).kind = 'cross-cutting';
crossCutting.push(name);
}
}
}
// Format output
const result = {
subsystems: [],
crossCutting,
dependencyMatrix: {}
};
for (const sub of subsystems.values()) {
result.subsystems.push({
name: sub.name,
kind: sub.kind,
files: Array.from(sub.files).sort(),
entities: sub.entities,
publicExports: Array.from(sub.publicExports).sort()
});
}
for (const [key, val] of Object.entries(matrix)) {
result.dependencyMatrix[key] = {
calls: val.calls,
imports: val.imports,
via: Array.from(val.via)
};
}
return result;
}
if (require.main === module) {
const snapshotPath = process.argv[2];
if (!snapshotPath) {
console.error('Usage: node subsystem.js <snapshot.json>');
process.exit(1);
}
const graph = GraphStore.loadSnapshot(snapshotPath);
const result = buildSubsystems(graph);
console.log(JSON.stringify(result, null, 2));
}
module.exports = { buildSubsystems, relPath, subsystemOf };