/** * JSONL File-Based Persistence for Medicine Wheel MCP Server * * Mirror of lib/jsonl-store.ts adapted for the MCP server process. * Both the Web UI and MCP server read/write the same .mw/store/ JSONL files, * enabling data created in either interface to be visible across both. * * Improvements over initial version (PR #27 Copilot review): * - readJsonl() throws on real FS errors instead of silently returning [] * - EdgeCollection uses upsert keyed by from_id:to_id — no duplicate edges * - flush() uses file lock + read-modify-write — concurrent writes merge * * @see https://github.com/jgwill/medicine-wheel/issues/26 */ import { type WebHubHold } from './relational-web.js'; import type { NodeFilters } from './types.js'; interface StoredNode { id: string; type: string; name: string; description?: string; direction?: string; metadata?: Record; created_at: string; updated_at: string; } interface StoredEdge { /** * Stable identity for the edge. `edgeKey` falls back to `from_id:to_id` when * absent — which silently collapses two distinct relations between the same * pair. A service binding both :80 and :443 on one host needs two, so an edge * whose identity is finer than its endpoints must carry an `id`. */ id?: string; from_id: string; to_id: string; relationship_type: string; strength: number; ceremony_honored: boolean; ceremony_id?: string; obligations: string[]; /** Why the two are related, in the words of whoever wove the relation. */ description?: string; /** Annotation carried by the relation itself — e.g. which port a `binds-port` claims. */ metadata?: Record; created_at: string; } interface StoredCeremony { id: string; type: string; direction: string; participants: string[]; medicines_used: string[]; intentions: string[]; timestamp: string; research_context?: string; relations_honored?: string[]; episode_path?: string; episode_number?: number; circle_id?: string; subject_id?: string; closes?: string; source?: string; } interface StoredBeat { id: string; direction: string; title: string; description: string; prose?: string; ceremonies: string[]; learnings: string[]; timestamp: string; act: number; relations_honored: string[]; cycle_id?: string; parent_beat_id?: string; sub_beats?: string[]; origin?: { producer: string; source_ref?: string; method?: string; }; } interface StoredCycle { id: string; research_question: string; current_direction: string; start_date: string; beats?: string[]; ceremonies_conducted: number; relations_mapped: number; wilson_alignment: number; ocap_compliant: boolean; archived?: boolean; } interface StoredChart { id: string; desired_outcome: string; current_reality: string; direction: string; action_steps: any[]; due_date?: string; created_at: string; updated_at: string; phase: string; ceremonies_linked: string[]; wilson_alignment?: number; cycle_id?: string; } interface StoredMmot { id: string; chart_id: string; timestamp: string; step1_expected: string; step1_actual: string; step2_analysis: string; step3_adjustments: string[]; step4_feedback: string; } interface StoredInquiryWeave { id: string; weave: 1; artefact: { id: string; path?: string; [key: string]: unknown; }; issue: string; issue_url?: string; episode: { path: string; number: number; [key: string]: unknown; }; last_sync: { state: 'never-synced' | 'in-sync' | 'stale' | 'episode-copy-diverged'; at?: string; tree_sha256?: string; file_count?: number; bytes_total?: number; [key: string]: unknown; }; source: { package: string; record_path?: string; registered_at: string; updated_at: string; [key: string]: unknown; }; [key: string]: unknown; } interface InquiryWeaveFilters { episode_path?: string; episode_number?: number; issue?: string; artefact?: string; } interface StoredPlanPerspectiveEpisode { path: string; number?: number; [key: string]: unknown; } interface StoredPlanPerspective { id: string; perspective: 1; plan: { session_id: string; plan_path?: string; plan_filename: string; plan_sha256: string; captured_at?: string; [key: string]: unknown; }; narrative: { title: string; body_markdown: string; mia_context?: string; [key: string]: unknown; }; lineage?: { user_inputs_path?: string; input_count?: number; first_input_at?: string; last_input_at?: string; excerpts?: string[]; [key: string]: unknown; }; episodes: StoredPlanPerspectiveEpisode[]; source: { package?: string; generator?: { system?: string; agent?: string; model?: string; producer_session_id?: string; [key: string]: unknown; }; registered_at: string; updated_at: string; [key: string]: unknown; }; [key: string]: unknown; } interface PlanPerspectiveFilters { episode_path?: string; session_id?: string; id?: string; } declare class JsonlCollection { private items; private filePath; private lastStamp; private loaded; constructor(filePath: string); private sync; /** * Read-modify-write inside a file lock so concurrent writers merge * instead of clobbering each other. */ private flush; get(id: string): T | undefined; getAll(): T[]; has(id: string): boolean; size(): number; set(id: string, item: T): void; filter(predicate: (item: T) => boolean): T[]; search(query: string, fields: (keyof T)[]): T[]; } declare class EdgeCollection { private items; private filePath; private lastStamp; private loaded; constructor(filePath: string); private edgeKey; private sync; private flush; /** Upsert: replaces any existing edge with the same endpoints. */ add(edge: StoredEdge): void; getAll(): StoredEdge[]; getForNode(nodeId: string): StoredEdge[]; getRelatedNodeIds(nodeId: string): string[]; updateCeremony(fromId: string, toId: string, ceremonyId: string): void; } export declare class JsonlStore { readonly name = "jsonl"; readonly dataDir: string; readonly nodes: JsonlCollection; readonly edges: EdgeCollection; readonly ceremonies: JsonlCollection; readonly beats: JsonlCollection; readonly cycles: JsonlCollection; readonly charts: JsonlCollection; readonly mmots: JsonlCollection; readonly inquiryWeaves: JsonlCollection; readonly planPerspectives: JsonlCollection; constructor(dataDir?: string); /** * The event log (jgwill/medicine-wheel#164). Writes that reach the wheel's * HTTP routes are recorded there; these are the writes the MCP server makes * to its own files, recorded the same way — the same decider, the same * line — in `events.jsonl` beside this store. A line that cannot be written * is reported on stderr; the write it describes stands. */ private record; /** The nodes a relation joins: between two chronicle episodes it is lineage. */ private endsOf; createNode(node: StoredNode): void; getNode(id: string): StoredNode | undefined; getAllNodes(limit?: number): StoredNode[]; getNodesByType(type: string): StoredNode[]; getNodesByDirection(direction: string): StoredNode[]; /** * Every supplied filter narrows (AND); an absent one does not constrain. * `kind` and `parent_id` read `metadata`, which is where the closed * `type` enum (six values then, eight since 0.17.0) forced every artifact kind and containment link to live. * * @see mcp/src/http-store.ts — the server-backed twin, which pushes the same * filters into the `/api/nodes` query string rather than filtering locally */ getNodesFiltered(filters: NodeFilters): StoredNode[]; /** * Term-based, ranked node search — the same contract the HTTP store serves, * so `search_nodes` answers identically whichever backend is mounted. * Filters run before ranking so `limit` is spent on receivable rows. * * @see mcp/src/node-search.ts — the matching and ranking contract */ searchNodes(query: string, opts?: { type?: string; direction?: string; kind?: string; limit?: number; }): StoredNode[]; createEdge(edge: StoredEdge): void; getAllEdges(): StoredEdge[]; getEdgesForNode(nodeId: string): StoredEdge[]; getRelatedNodeIds(nodeId: string): string[]; updateEdgeCeremony(fromId: string, toId: string, ceremonyId: string): void; /** * Delegates to the shared walk so the JSONL and HTTP stores cannot answer the * same question differently. Both had their own BFS and both had the same two * defects — edges returned for nodes that were not, and no notion of a * container. See `relational-web.ts`. */ getRelationalWeb(nodeId: string, depth?: number): { nodes: StoredNode[]; edges: StoredEdge[]; hubs: WebHubHold[]; truncated: boolean; capped?: { returned: number; available: number; }; }; logCeremony(ceremony: StoredCeremony): void; getCeremony(id: string): StoredCeremony | undefined; getAllCeremonies(limit?: number): StoredCeremony[]; getCeremoniesByDirection(direction: string): StoredCeremony[]; getCeremoniesByType(type: string): StoredCeremony[]; /** Filters combine — see the HTTP store's note on the if/else-if that dropped one. */ getCeremoniesFiltered(filters?: { direction?: string; type?: string; }): StoredCeremony[]; createBeat(beat: StoredBeat): void; getBeat(id: string): StoredBeat | undefined; getAllBeats(limit?: number): StoredBeat[]; getBeatsByDirection(direction: string): StoredBeat[]; createCycle(cycle: StoredCycle): void; getCycle(id: string): StoredCycle | undefined; getAllCycles(): { active: StoredCycle[]; archived: StoredCycle[]; }; archiveCycle(id: string): void; saveChart(chart: StoredChart): void; getChart(id: string): StoredChart | undefined; getAllCharts(direction?: string): StoredChart[]; saveMmot(mmot: StoredMmot): void; getMmotsByChart(chartId: string): StoredMmot[]; registerInquiryWeave(record: StoredInquiryWeave): void; getInquiryWeave(id: string): StoredInquiryWeave | undefined; listInquiryWeaves(filters?: InquiryWeaveFilters): StoredInquiryWeave[]; registerPlanPerspective(record: StoredPlanPerspective): StoredPlanPerspective; getPlanPerspective(id: string): StoredPlanPerspective | undefined; listPlanPerspectives(filters?: PlanPerspectiveFilters): StoredPlanPerspective[]; } export declare function getJsonlStore(dataDir?: string): JsonlStore; export {}; //# sourceMappingURL=jsonl-store.d.ts.map