From 084bf84258ab82accf755d1b2da4f7373d91f975 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Wed, 1 Jul 2026 22:32:37 +0200 Subject: [PATCH] =?UTF-8?q?feat(graph):=20vault=20link-graph=20model=20?= =?UTF-8?q?=E2=80=94=20notes=20to=20nodes,=20wikilinks=20to=20edges=20(T-3?= =?UTF-8?q?23)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit VaultGraph.fromOutlinks builds the graph from pql's per-file outlinks: every file a node (labelled by basename), each outlink to a known file an edge; self/dangling links dropped, parallels de-duped. neighborhood() drives the hover-highlight; edgePairs feeds the ForceLayout solver. Pure Dart, dart-tested (6 cases). The graph pane's data layer, on top of the solver. Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/src/graph/vault_graph.dart | Bin 0 -> 2401 bytes test/src/graph/vault_graph_test.dart | 51 +++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 lib/src/graph/vault_graph.dart create mode 100644 test/src/graph/vault_graph_test.dart diff --git a/lib/src/graph/vault_graph.dart b/lib/src/graph/vault_graph.dart new file mode 100644 index 0000000000000000000000000000000000000000..a38f70975e4cbafedc8955b8131b8ace721c908c GIT binary patch literal 2401 zcmZuy&2Aev5boJeF$Ds67m-$JbF(8AD4G;SQ#(L{_Fx1xC62U&>E(vx+7TQBJ@o;K zK4G4u-waoho%A4k$@%~0o8fFWqjxRSjTw9<@4746MANray_-J&{&{^uUC5lsM8=op zG@)B}DqawFyvxYT|#m=z7Xl+vOcAZ zJk}wum{RaJ9MxfU@k*{iomY&jWYk(T9yjxvS z=($7dh}Nz%p5Er@x(&4VzJGG5_hw6-xdt#xlUToQU2X+0{+Qtay{Nx(qFJ6E-9IY& zC8+Xc(Q*gKQL1oeQmW1Y^?i|+h`+kb;Xw>VLeYH?lHe5VXNX(MX zSnJ~MezZP>$4CMvWU%XgBR7us03g;uJDaHV>PYRH8{-GwK_z*)H5yu!=JQcWIP~cg zeXHO!*fiO=E{$q+ExRYXZs3yJ&Wohp7;CHJCwC)@q~AEJ8&uKbOhhuA%!M>P(l`cxC_jI!CzWJd}CEcW3l`>iD0;Y*fdpbKZ;Q{}pq zAFQxi&h=sSVHgdyL7tJ~u63K%I;&an*+U5|Jrmg0VN`iPT4;e_Y21;%pU;5+|< z$M)m-x;ma6D-uObo+)S*?Zup)(GSHW4a=lYTvcRG>ARXvR3?ZOYl~etC-Lz(L{mbH W{1vT*dREPY=VudwOx%h n.id), containsAll(['notes/alpha.md', 'beta.md'])); + expect(g.nodes.firstWhere((n) => n.id == 'notes/alpha.md').label, 'alpha'); + }); + + test('outlinks to known files become edges', () { + final g = VaultGraph.fromOutlinks({ + 'a.md': const ['b.md'], + 'b.md': const [], + }); + expect(g.edgePairs, [('a.md', 'b.md')]); + }); + + test('self-links and dangling links are dropped', () { + final g = VaultGraph.fromOutlinks({ + 'a.md': const ['a.md', 'ghost.md', 'b.md'], + 'b.md': const [], + }); + expect(g.edgePairs, [('a.md', 'b.md')]); // no self, no ghost + }); + + test('parallel edges are de-duplicated', () { + final g = VaultGraph.fromOutlinks({ + 'a.md': const ['b.md', 'b.md'], + 'b.md': const [], + }); + expect(g.edgePairs, hasLength(1)); + }); + + test('an empty vault is empty', () { + expect(VaultGraph.fromOutlinks(const {}).isEmpty, isTrue); + }); + }); + + test('neighborhood returns a node plus its direct links, both directions', () { + final g = VaultGraph.fromOutlinks({ + 'a.md': const ['b.md'], + 'b.md': const ['c.md'], + 'c.md': const [], + 'x.md': const [], + }); + expect(g.neighborhood('b.md'), {'b.md', 'a.md', 'c.md'}); + expect(g.neighborhood('x.md'), {'x.md'}); // isolated node + }); +}