toJson method
Implementation
Map<String, dynamic> toJson({Map<String, dynamic>? projectSummaryJson}) {
// Generate edges from node dependencies
final edges = <Edge>[];
final Map<String, int> incomingEdges = {};
final Map<String, int> outgoingEdges = {};
final Map<String, List<String>> reverseDependencies = {}; // Who depends on this node?
for (final node in nodes) {
incomingEdges[node.id] = 0;
outgoingEdges[node.id] = 0;
reverseDependencies[node.id] = [];
}
for (final node in nodes) {
for (final dep in node.dependencies) {
edges.add(Edge(source: node.id, target: dep, type: 'imports'));
outgoingEdges[node.id] = (outgoingEdges[node.id] ?? 0) + 1;
incomingEdges[dep] = (incomingEdges[dep] ?? 0) + 1;
if (reverseDependencies.containsKey(dep)) {
reverseDependencies[dep]!.add(node.id);
}
}
}
// Detect Circular Dependencies (Simple DFS)
final circularDependencies = <List<String>>[];
final visited = <String>{};
final recStack = <String>{};
final Map<String, Node> nodeMap = {for (var n in nodes) n.id: n};
void detectCycle(String nodeId, List<String> path) {
visited.add(nodeId);
recStack.add(nodeId);
path.add(nodeId);
final node = nodeMap[nodeId];
if (node != null) {
for (final dep in node.dependencies) {
if (!visited.contains(dep)) {
detectCycle(dep, List.from(path));
} else if (recStack.contains(dep)) {
// Cycle found
final cycleIndex = path.indexOf(dep);
if (cycleIndex != -1) {
circularDependencies.add(path.sublist(cycleIndex));
}
}
}
}
recStack.remove(nodeId);
}
for (final node in nodes) {
if (!visited.contains(node.id)) {
detectCycle(node.id, []);
}
}
// Impact Radius (BFS for every node)
final Map<String, List<String>> impactRadius = {};
for (final node in nodes) {
final queue = [node.id];
final impacted = <String>{};
while (queue.isNotEmpty) {
final current = queue.removeAt(0);
final revDeps = reverseDependencies[current] ?? [];
for (final revDep in revDeps) {
if (!impacted.contains(revDep)) {
impacted.add(revDep);
queue.add(revDep);
}
}
}
impactRadius[node.id] = impacted.toList();
}
// Calculate Architecture Score
int score = 100;
if (circularDependencies.isNotEmpty) {
score -= (circularDependencies.length * 5);
}
// Calculate type metrics
int totalScreens = 0;
int totalModels = 0;
int totalRepositories = 0;
int totalServices = 0;
int totalWidgets = 0;
for (final node in nodes) {
if (node.type == 'screen') {
totalScreens++;
} else if (node.type == 'model') {
totalModels++;
} else if (node.type == 'repository') {
totalRepositories++;
} else if (node.type == 'service') {
totalServices++;
} else if (node.type == 'widget') {
totalWidgets++;
}
}
// Dynamic Threshold Calculation
int threshold = nodes.length;
if (nodes.length <= 50) {
threshold = nodes.length;
} else if (nodes.length <= 200) {
threshold = 50;
} else if (nodes.length <= 1000) {
threshold = 75;
} else {
threshold = 100;
}
// Find Highly Coupled / God Classes & Rank Nodes
final highlyCoupled = <Map<String, dynamic>>[];
final godClasses = <Map<String, dynamic>>[];
final allNodesRanked = <Map<String, dynamic>>[];
for (final node in nodes) {
final inDegree = incomingEdges[node.id] ?? 0;
final outDegree = outgoingEdges[node.id] ?? 0;
final totalDegree = inDegree + outDegree;
allNodesRanked.add({'id': node.id, 'score': totalDegree});
if (totalDegree > 20) {
highlyCoupled.add({'id': node.id, 'score': totalDegree});
score -= 2; // Penalize for high coupling
}
if (outDegree > 15) {
godClasses.add({'id': node.id, 'imports': outDegree});
score -= 3; // Penalize for god classes
}
}
allNodesRanked.sort((a, b) => (b['score'] as int).compareTo(a['score'] as int));
final topNodes = allNodesRanked.take(threshold).map((e) => e['id'] as String).toList();
score = score.clamp(0, 100);
// Dead Code: files with 0 incoming dependencies
final deadCode = nodes.where((n) {
final inDeg = incomingEdges[n.id] ?? 0;
final label = n.label.toLowerCase();
// Exclude main.dart and generated files
return inDeg == 0 && !label.contains('main') && !label.contains('.g.') && !label.contains('.freezed.');
}).map((n) => {'id': n.id, 'label': n.label, 'type': n.type}).toList();
// Folder Map: group nodes by module with type breakdown
final Map<String, Map<String, dynamic>> folderMap = {};
for (final node in nodes) {
final parts = node.id.split('/');
final module = parts.length > 2 ? parts.sublist(1, parts.length - 1).join('/') : 'root';
folderMap.putIfAbsent(module, () => {'count': 0, 'screens': 0, 'models': 0, 'state': 0, 'repository': 0, 'files': 0});
folderMap[module]!['count'] = (folderMap[module]!['count'] as int) + 1;
final typeKey = node.type == 'screen' ? 'screens'
: node.type == 'model' ? 'models'
: node.type == 'state' ? 'state'
: node.type == 'repository' ? 'repository'
: 'files';
folderMap[module]![typeKey] = (folderMap[module]![typeKey] as int) + 1;
}
final folderMapList = folderMap.entries
.map((e) => {'module': e.key, ...e.value})
.toList()
..sort((a, b) => (b['count'] as int).compareTo(a['count'] as int));
return {
'nodes': nodes.map((e) => e.toJson()).toList(),
'edges': edges.map((e) => e.toJson()).toList(),
'metrics': {
'architecture_score': score,
'circular_dependencies': circularDependencies,
'highly_coupled': highlyCoupled..sort((a, b) => (b['score'] as int).compareTo(a['score'] as int)),
'god_classes': godClasses..sort((a, b) => (b['imports'] as int).compareTo(a['imports'] as int)),
'total_nodes': nodes.length,
'total_edges': edges.length,
'total_screens': totalScreens,
'total_models': totalModels,
'total_repositories': totalRepositories,
'total_services': totalServices,
'total_widgets': totalWidgets,
'dynamic_threshold': threshold,
'top_nodes': topNodes,
'all_nodes_ranked': allNodesRanked,
'impact_radius': impactRadius,
'dead_code': deadCode,
'folder_map': folderMapList,
'project_summary': projectSummaryJson,
}
};
}