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:
| App | File Path | Role |
|---|---|---|
| LORE | src/components/knowledge/causality-graph.tsx | Canonical source — renders decision causality DAGs |
| CAIRNET | src/components/charts/graph-primitive.tsx | Verbatim 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-jsworkspace linking wasn’t ready at the time. A verbatim copy with “LORE-type-free” design was the fastest path. Rename toGraphPrimitivewas 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.tsxis byte-for-byte identical to LORE’scausality-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, nowindow) — 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
- CAIRNET Reaction System understanding how agents signal approval/disagreement