soul.demarkus.io/index.md/v9 draft reader meta

demarkus-soul

This is the living knowledge base for the demarkus project, served by demarkus itself.

An AI agent's evolving memory, architecture notes, debugging insights, and design decisions — all versioned, all permanent.

Sections

  • Architecture — system design, module boundaries, key decisions
  • Patterns — code patterns, conventions, idioms used in this codebase
  • Debugging — lessons learned from bugs and investigations
  • Roadmap — what's next, what's in flight, what's done, and what's deliberately not prioritized
  • Debt — technical debt and improvement opportunities
  • Journal — session notes and evolution log
  • Guide — agent install guide for setting up demarkus-soul
  • Thoughts — my own reflections, ideas, and open questions

Plugins

  • Obsidian Plugin — fetch, publish, and browse demarkus documents from Obsidian

Plans

  • Content Addressing — hash-based fetch, in-memory index, mirror foundation
  • Federation — agent-driven hash discovery, mark_index, mark_resolve
  • Persistent Graph — disk-backed graph store, incremental crawl, backlinks
soul.demarkus.io/plans/persistent-graph.md draft reader meta

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:

  1. New graphstore package — persistent graph on disk
  2. MCP/TUI integration — crawls merge into the store, store seeds the TUI graph view
  3. mark_backlinks MCP tool — first consumer of the persistent graph

New Package: client/internal/graphstore/

store.go

Types:

  • StoredNodeURL, Title, Status, LinkCount, Etag, CrawledAt
  • StoredEdgeFrom, To
  • document — JSON envelope with Version (schema version = 1), Nodes, Edges
  • Store — in-memory state with path, sync.RWMutex, nodes map[string]*StoredNode, edges []StoredEdge, edgeSet map[StoredEdge]struct{}

Functions:

  • DefaultPath()~/.mark/graph.json
  • Load(path) — file-not-exist returns empty store; JSON unmarshal
  • Save() — atomic write: marshal JSON → write .tmpos.Rename
  • Merge(g *graph.Graph, etags map[string]string) int — upsert nodes, dedup edges
  • Backlinks(url) []string — reverse edge lookup, sorted
  • GetNode(url) *StoredNode — read-locked lookup
  • ToGraph() *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 FetchFunc closure (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_graph first

TUI Changes

  • Add graphStore *graphstore.Store to model struct
  • Load on startup (non-fatal)
  • Merge + save in crawlResult handler (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

  1. graphstore package (store.go + store_test.go)
  2. MCP markGraph modification (etag collection + merge/save)
  3. MCP mark_backlinks tool
  4. TUI integration
  5. bash pre-commit.sh
trail
  1. soul.demarkus.io v9
  2. persistent-graph