logDependencyCycles method

void logDependencyCycles(
  1. Set<PluginId> enabledPluginIds,
  2. List<Plugin> pluginSubset, {
  3. Set<PluginId> additionalEnabledPluginIds = const {},
})

Detects strongly connected components in the dependency subgraph restricted to enabledPluginIds (plus additionalEnabledPluginIds for cross-scope dep resolution) and emits one severe log per cycle naming the participants.

Detection is informational: cycles where every member is enabled satisfy each other's dependencies and the plugins remain attached. A cycle is still a structural smell (order ambiguity, fragile startup, harder debugging), so silence is the worst outcome. Logging preserves backward-compat behavior while surfacing the issue.

Uses Tarjan's algorithm to find SCCs in one pass. Components of size

= 2 are cycles; components of size 1 with a self-loop edge are self-cycles.

Implementation

void logDependencyCycles(
  Set<PluginId> enabledPluginIds,
  List<Plugin> pluginSubset, {
  Set<PluginId> additionalEnabledPluginIds = const {},
}) {
  if (enabledPluginIds.isEmpty) return;
  final byId = <PluginId, Plugin>{
    for (final p in pluginSubset)
      if (enabledPluginIds.contains(p.pluginId)) p.pluginId: p,
  };

  final reachable = <PluginId>{
    ...enabledPluginIds,
    ...additionalEnabledPluginIds,
  };

  int index = 0;
  final indexMap = <PluginId, int>{};
  final lowlink = <PluginId, int>{};
  final onStack = <PluginId>{};
  final stack = <PluginId>[];
  final sccs = <List<PluginId>>[];

  void strongConnect(PluginId v) {
    indexMap[v] = index;
    lowlink[v] = index;
    index++;
    stack.add(v);
    onStack.add(v);

    final plugin = byId[v];
    if (plugin != null) {
      for (final w in plugin.dependencies) {
        if (!reachable.contains(w)) continue;
        if (!indexMap.containsKey(w)) {
          strongConnect(w);
          final lw = lowlink[w]!;
          if (lw < lowlink[v]!) lowlink[v] = lw;
        } else if (onStack.contains(w)) {
          final iw = indexMap[w]!;
          if (iw < lowlink[v]!) lowlink[v] = iw;
        }
      }
    }

    if (lowlink[v] == indexMap[v]) {
      final scc = <PluginId>[];
      PluginId w;
      do {
        w = stack.removeLast();
        onStack.remove(w);
        scc.add(w);
      } while (w != v);
      sccs.add(scc);
    }
  }

  for (final v in enabledPluginIds) {
    if (!indexMap.containsKey(v)) strongConnect(v);
  }

  for (final scc in sccs) {
    final isCycle =
        scc.length > 1 ||
        (scc.length == 1 &&
            (byId[scc.first]?.dependencies.contains(scc.first) ?? false));
    if (!isCycle) continue;
    final names = scc.map((p) => p).join(' -> ');
    _runtimeLog.severe(
      'Dependency cycle detected among enabled plugins: $names. '
      'Cycles are functional when every member is enabled but indicate '
      'tight coupling that should usually be one plugin or merged via '
      'a shared service.',
    );
  }
}