medicine-wheel

data-store — RISE Specification

Persistence layers for the Medicine Wheel ecosystem — JSONL file-based storage for development/community use, and Redis-backed persistence for production scale.

Version: 0.2.0
Package: medicine-wheel-data-store
Document ID: rispec-data-store-v2
Last Updated: 2026-04-04


Desired Outcome

Users create persistent relational knowledge graphs that survive process restarts and are visible across all interfaces (Web UI, MCP tools, terminal agents) without requiring external infrastructure for development and community deployments.


Creative Intent

What this enables: Any Medicine Wheel application can persist its ontological data — ceremonies, cycles, nodes, edges, beats, structural tension charts — to shared storage that all interfaces read/write simultaneously. Communities can start with zero-infrastructure JSONL files and graduate to Redis when scale demands it.

Structural Tension: Between in-memory-only ephemeral data (fast but invisible across processes) and durable queryable persistence (survives restarts, shareable). The JSONL layer resolves this with file-based persistence; the Redis layer resolves it with network-accessible persistence.


Architecture: Two Persistence Backends

1. JSONL File Store (Default — Zero Dependencies)

Location: lib/jsonl-store.ts (Web UI) + mcp/src/jsonl-store.ts (MCP server)
Data directory: .mw/store/ (configurable via MW_DATA_DIR)

Both the Next.js Web UI and MCP server processes read/write the same JSONL files on disk. Cross-process synchronization is handled via file mtime checking — if one process writes, the other detects the change and reloads from disk.

File Layout

.mw/store/
├── nodes.jsonl        # Relational nodes (human, land, spirit, ancestor, future, knowledge)
├── edges.jsonl        # Relational edges between nodes
├── ceremonies.jsonl   # Ceremony logs
├── beats.jsonl        # Narrative beats
├── cycles.jsonl       # Medicine wheel research cycles
├── charts.jsonl       # Structural tension charts
└── mmots.jsonl        # Moment of truth reviews

Each file is JSONL format: one JSON record per line. Entity collections (nodes, ceremonies, beats, cycles, charts, mmots) are keyed by the record’s id field with full upsert semantics — writing the same id twice replaces the first record. Edges are keyed by ${from_id}:${to_id} (or the edge’s explicit id) and also use upsert — re-adding the same edge endpoints updates the existing record rather than duplicating it.

Cross-Process Sync Protocol

Process A (Web UI)              Shared Disk               Process B (MCP)
     │                              │                          │
     ├── write ceremony ──────────► │ ceremonies.jsonl         │
     │   (atomic: tmp+rename)       │ mtime updated            │
     │                              │                          │
     │                              │ ◄───────── list_ceremonies│
     │                              │   (check mtime → reload) │
     │                              │   returns ceremony ──────┤

Configuration

Variable Default Purpose
MW_DATA_DIR .mw/store/ (project root) Override JSONL data directory

The MCP server resolves the project root automatically (from mcp/ subdirectory → parent .mw/store/).

Atomic Writes & Concurrent Safety

Each write is a read-modify-write inside a file lock:

  1. Acquire lock via O_EXCL create of file.jsonl.lock (atomic on POSIX) and write an ownership token into the lock file
  2. Re-read current disk state inside the lock (picks up any concurrent writes)
  3. Merge in-memory changes with disk state (in-memory items take precedence)
  4. Write merged result to file.jsonl.tmp.<pid>
  5. fs.renameSync() to file.jsonl (atomic)
  6. If the lock is busy, retry asynchronously with backoff so the event loop can continue serving work
  7. Release lock only if the current process still owns the matching lock token

This prevents last-writer-wins data loss when the Web UI and MCP server write simultaneously.

When to Use JSONL

2. Redis Store (Production Scale)

Location: src/data-store/
Package: medicine-wheel-data-store

Redis-backed persistence for production deployments. Supports Upstash, Vercel KV, and local Redis.

Connection Management

interface RedisConnectionConfig {
  url?: string;              // Default: "redis://localhost:6379"
  prefix?: string;           // Key prefix, default: "mw:"
  autoConnect?: boolean;     // Default: true
  retryAttempts?: number;    // Default: 3
  retryDelay?: number;       // Default: 1000 (ms)
}

createConnection(config?: RedisConnectionConfig): Promise<MWRedisClient>
getConnection(): MWRedisClient
closeConnection(): Promise<void>
isConnected(): boolean

Store Operations

CRUD for RelationalNode, RelationalEdge, and CeremonyLog types from ontology-core.

Nodes: putNode, getNode, deleteNode, listNodes
Edges: putEdge, getEdge, deleteEdge, listEdges
Ceremonies: putCeremony, getCeremony, deleteCeremony, listCeremonies

Session-Ceremony Linking

Bidirectional links between external sessions and Medicine Wheel ceremonies:

linkSessionToCeremony(sessionId: string, ceremonyId: string): Promise<void>
getCeremoniesForSession(sessionId: string): Promise<string[]>
getSessionsForCeremony(ceremonyId: string): Promise<string[]>

When to Use Redis


API Surface (Shared Across Backends)

Both JSONL and Redis backends expose the same logical operations:

Nodes

createNode(node: RelationalNode): void
getNode(id: string): RelationalNode | undefined
getAllNodes(limit?: number): RelationalNode[]
getNodesByType(type: string): RelationalNode[]
getNodesByDirection(direction: string): RelationalNode[]
searchNodes(query: string, opts?: { type?; direction?; limit? }): RelationalNode[]

Edges

createEdge(edge: RelationalEdge): void
getEdgesForNode(nodeId: string): RelationalEdge[]
getRelatedNodeIds(nodeId: string): string[]
getRelationalWeb(nodeId: string, depth?: number): { nodes; edges }
updateEdgeCeremony(fromId: string, toId: string, ceremonyId: string): void // updates only the directed edge that matches fromId -> toId

Ceremonies

logCeremony(ceremony: CeremonyLog): void
getCeremony(id: string): CeremonyLog | undefined
getAllCeremonies(limit?: number): CeremonyLog[]
getCeremoniesByDirection(direction: string): CeremonyLog[]
getCeremoniesByType(type: string): CeremonyLog[]

Beats

createBeat(beat: NarrativeBeat): void
getBeat(id: string): NarrativeBeat | undefined
getAllBeats(limit?: number): NarrativeBeat[]
getBeatsByDirection(direction: string): NarrativeBeat[]

Cycles

createCycle(cycle: MedicineWheelCycle): void
getCycle(id: string): MedicineWheelCycle | undefined
getAllCycles(): { active: MedicineWheelCycle[]; archived: MedicineWheelCycle[] }
archiveCycle(id: string): void

Charts (Structural Tension)

saveChart(chart: StructuralTensionChart): void
getChart(id: string): StructuralTensionChart | undefined
getAllCharts(direction?: string): StructuralTensionChart[]

MMOT (Moment of Truth)

saveMmot(mmot: MmotReview): void
getMmotsByChart(chartId: string): MmotReview[]

.mw/ Directory Convention

The .mw/ directory follows the Medicine Wheel workspace convention established across the ecosystem:

.mw/
├── store/               # JSONL data files (this spec)
│   ├── nodes.jsonl
│   ├── edges.jsonl
│   ├── ceremonies.jsonl
│   ├── beats.jsonl
│   ├── cycles.jsonl
│   ├── charts.jsonl
│   └── mmots.jsonl
├── east/                # Vision artifacts (optional)
├── south/               # Planning artifacts (optional)
├── west/                # Implementation artifacts (optional)
├── north/               # Reflection artifacts (optional)
├── ceremonies/          # Ceremony crossing artifacts (optional)
└── README.md            # Workspace description

The store/ subdirectory is created automatically by the JSONL persistence engine. The directional subdirectories are optional and follow the .mw/ convention from other ecosystem projects.


Dependencies

JSONL Store

Redis Store


Advancing Patterns


Quality Criteria