validate method

List<TextureGraphIssue> validate()

What is wrong with this graph: a dangling input, a type mismatch, or a cycle — mat-10's own two named checks plus the dangling case neither names but a type check needs answered one way or the other to avoid treating "wired to nothing found" as "wired to nothing," which is a different, silent fact.

Cycles are found before types are, and reported once per node that closes one — a graph with a → b → a reports on whichever of the two the walk reaches second, not on every node the cycle passes through, since one node's own message is enough to say where to cut it.

Implementation

List<TextureGraphIssue> validate() {
  final issues = <TextureGraphIssue>[];
  final visiting = <int>{};
  final done = <int>{};

  void visit(TextureNode node) {
    if (done.contains(node.id)) return;
    if (visiting.contains(node.id)) {
      issues.add(TextureGraphIssue(node.id, 'is part of a cycle'));
      return;
    }
    visiting.add(node.id);
    for (final socket in node.inputs.values) {
      if (socket.from == null) continue;
      final source = nodeById(socket.from!);
      if (source != null) visit(source);
    }
    visiting.remove(node.id);
    done.add(node.id);
  }

  for (final node in nodes) {
    visit(node);
  }

  for (final node in nodes) {
    for (final entry in node.inputs.entries) {
      final from = entry.value.from;
      if (from == null) continue;
      final source = nodeById(from);
      if (source == null) {
        issues.add(
          TextureGraphIssue(
            node.id,
            'input "${entry.key}" reads node $from, which is not in this '
            'graph',
          ),
        );
        continue;
      }
      if (source.outputType != entry.value.type) {
        issues.add(
          TextureGraphIssue(
            node.id,
            'input "${entry.key}" expects ${entry.value.type.name} and '
            'node $from is ${source.outputType.name}',
          ),
        );
      }
    }
  }

  return issues;
}