Adds the pure filtering primitives the graph pane composes: VaultGraph nodesWithin (depth-bounded BFS over undirected edges = the local graph around a note) and subgraph (retain a node set + the edges between them), plus GraphFilter, which combines depth-from-active with tag include/exclude over a caller-supplied tag map. Flutter-free; runs under dart test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
112 lines
3.7 KiB
Dart
112 lines
3.7 KiB
Dart
/// The vault link graph (T-323): notes are nodes, wikilinks are edges.
|
|
///
|
|
/// Built from pql's per-file outlinks (`pql outlinks <file>`); the force solver
|
|
/// ([ForceLayout]) positions it and the graph pane renders it. Pure data — no
|
|
/// Flutter, no pql shell-out here (the pane feeds in the already-queried links),
|
|
/// so it runs under `dart test`.
|
|
library;
|
|
|
|
class GraphNode {
|
|
const GraphNode({required this.id, required this.label});
|
|
|
|
/// Vault-relative path — the stable identity + what a click opens.
|
|
final String id;
|
|
|
|
/// Display name (basename without extension).
|
|
final String label;
|
|
}
|
|
|
|
class GraphEdge {
|
|
const GraphEdge(this.from, this.to);
|
|
final String from;
|
|
final String to;
|
|
}
|
|
|
|
class VaultGraph {
|
|
const VaultGraph(this.nodes, this.edges);
|
|
|
|
final List<GraphNode> nodes;
|
|
final List<GraphEdge> edges;
|
|
|
|
bool get isEmpty => nodes.isEmpty;
|
|
|
|
/// Build from a file→outlinks map. Every key is a node; each outlink to a
|
|
/// KNOWN file becomes one edge. Self-links and dangling links (to files not in
|
|
/// the map) are dropped, and parallel edges are de-duplicated.
|
|
factory VaultGraph.fromOutlinks(Map<String, List<String>> outlinks) {
|
|
final ids = outlinks.keys.toList();
|
|
final known = ids.toSet();
|
|
final nodes = [for (final id in ids) GraphNode(id: id, label: _label(id))];
|
|
final edges = <GraphEdge>[];
|
|
final seen = <String>{};
|
|
for (final entry in outlinks.entries) {
|
|
for (final target in entry.value) {
|
|
if (target == entry.key || !known.contains(target)) continue;
|
|
if (seen.add('${entry.key} |