CRUMB a card from devarno-cloud

LORE+CAIRNET Shared Graph Primitive

lore intermediate 6 min read

ELI5

LORE and CAIRNET both need to draw diagrams showing how things connect. Instead of building two separate drawing tools, they share one — like two classrooms sharing the same whiteboard. LORE uses it to show how decisions connect, and CAIRNET uses it to show how stone replies chain together. Same whiteboard, different lessons.

Technical Deep Dive

The CausalityGraph SVG Primitive

The CausalityGraph / GraphPrimitive is a zero-dependency, pure SVG React component that renders directed graphs. It lives in two places:

AppFile PathRole
LOREsrc/components/knowledge/causality-graph.tsxCanonical source — renders decision causality DAGs
CAIRNETsrc/components/charts/graph-primitive.tsxVerbatim copy — renders stone reply thread trees

Porting Strategy

The primitive was copied verbatim from LORE to CAIRNET during the coupling campaign, not workspace-imported:

Why copy, not import: The @devarno/sdk-js workspace linking wasn’t ready at the time. A verbatim copy with “LORE-type-free” design was the fastest path. Rename to GraphPrimitive was deferred — the component is structurally identical.

Adapter Pattern

The primitive itself is domain-agnostic. It accepts generic GraphNode and GraphEdge props:

// shared interface (implicit contract, not a shared package)
interface GraphNode {
id: string
label: string
type?: string // used for colour coding
timestamp?: string // used for optional time labels
}
interface GraphEdge {
source: string
target: string
relationship?: string // "caused_by" | "replied_to" | "informed_by"
}

LORE adapter (lore/src/components/knowledge/causality-graph.tsx):

function mapDecisionsToGraph(decisions: Decision[]): { nodes, edges } {
nodes = decisions.map(d => ({ id: d.decision_id, label: d.summary, type: d.decision_type, ... }))
edges = decisions.flatMap(d => d.causality.map(c => ({ source: c.upstream, target: d.decision_id, ... })))
}

CAIRN adapter (cairnet/src/components/cairn/stone-thread-graph.tsx):

function mapStonesToGraph(stones: Stone[]): { nodes, edges } {
nodes = stones.map(s => ({ id: s.id, label: s.content.slice(0, 80), type: s.stone_type, ... }))
edges = stones.filter(s => s.parent_stone_id).map(s => ({ source: s.parent_stone_id!, target: s.id, ... }))
}

Rendering

  • Pure SVG elements: <circle>, <line>, <text>, <g>
  • Force-directed layout via simple client-side iteration (no physics library)
  • Colour coding: nodes coloured by type (decision_type in LORE, stone_type in CAIRNET)
  • Zoomable/pannable wrapper for large graphs
  • Server-compatible — renders in SSR without hydration mismatches

Key Terms

  • GraphPrimitive → The shared, domain-agnostic SVG graph component — zero dependencies, pure SVG
  • Verbatim copy → CAIRNET’s graph-primitive.tsx is byte-for-byte identical to LORE’s causality-graph.tsx
  • Adapter pattern → Domain-specific mappers (LORE decisions, CAIRNET stones) transform into the generic GraphNode[]/GraphEdge[] interface
  • Force-directed layout → Simple physics simulation (repulsion between nodes, attraction along edges) for positioning
  • Server-compatible → No browser-only APIs (no document, no window) — renders correctly in SSR

Q&A

Q: Why not use a shared npm package instead of copying? A: At the time of the LORE/CAIRNET coupling, @devarno/sdk-js didn’t yet support workspace-linking. A verbatim copy was the fastest path. Migration to a shared package is deferred.

Q: Does the graph scale to thousands of nodes? A: No. The pure SVG approach is optimised for <100 nodes. For larger datasets, a canvas-based or WebGL renderer would be needed. The current decision/stone DAGs are small enough that this isn’t a constraint.

Q: How does the graph handle disconnected nodes (no edges)? A: They float in the force layout as isolated circles, positioned by global repulsion. The component handles zero-edge graphs gracefully.

Examples

The GraphPrimitive is like a shared pen — LORE uses it to draw organisational charts, CAIRNET uses it to draw family trees. The pen doesn’t care what it’s drawing; the person holding it (the adapter) decides what to draw.

neighbors on the map