Plan: Persistent Graph Store + Backlinks
Context
The graph crawler (client/internal/graph/) works but is entirely in-memory — every crawl starts from scratch. Cross-session graph knowledge is lost. Phase 4 of the roadmap calls for making the graph persistent as the foundation for backlinks, graph-as-content export, and agent discovery.
Scope
Three deliverables in one increment:
- New
graphstorepackage — persistent graph on disk - MCP/TUI integration — crawls merge into the store, store seeds the TUI graph view
mark_backlinksMCP tool — first consumer of the persistent graph
New Package: client/internal/graphstore/
store.go
Types:
StoredNode—URL,Title,Status,LinkCount,Etag,CrawledAtStoredEdge—From,Todocument— JSON envelope withVersion(schema version = 1),Nodes,EdgesStore— in-memory state withpath,sync.RWMutex,nodes map[string]*StoredNode,edges []StoredEdge,edgeSet map[StoredEdge]struct{}
Functions:
DefaultPath()—~/.mark/graph.jsonLoad(path)— file-not-exist returns empty store; JSON unmarshalSave()— atomic write: marshal JSON → write.tmp→os.RenameMerge(g *graph.Graph, etags map[string]string) int— upsert nodes, dedup edgesBacklinks(url) []string— reverse edge lookup, sortedGetNode(url) *StoredNode— read-locked lookupToGraph() *graph.Graph— reconstruct in-memory graph from stored state
Tests
Table-driven with t.TempDir(): LoadEmpty, SaveLoad round-trip, MergeUpdatesNode, MergeAddsEdges, Backlinks, BacklinksNone, ToGraph.
MCP Changes
Modify markGraph handler
- Collect etags in
FetchFuncclosure (mutex-protected) - After
graph.Crawl(): load store → merge → save (non-fatal on error)
New mark_backlinks tool
- Input:
url(required, bare path supported) - Load store, reverse edge lookup, format as markdown list with titles
- Empty result hints to run
mark_graphfirst
TUI Changes
- Add
graphStore *graphstore.Storeto model struct - Load on startup (non-fatal)
- Merge + save in
crawlResulthandler (on update loop, not in goroutine) - Seed graph view from store when entering with no active crawl data
On-Disk Format
{
"version": 1,
"nodes": [{"url": "...", "title": "...", "status": "ok", "link_count": 3, "etag": "...", "crawled_at": "..."}],
"edges": [{"from": "...", "to": "..."}]
}
What Stays Unchanged
client/internal/graph/— all types and Crawl() untouched- No server changes, no protocol changes, no new verbs
Implementation Order
graphstorepackage (store.go + store_test.go)- MCP
markGraphmodification (etag collection + merge/save) - MCP
mark_backlinkstool - TUI integration
bash pre-commit.sh