@medicine-wheel/storage-provider 0.4.12 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +3 -1
- package/dist/index.js +4 -0
- package/dist/interface.d.ts +97 -0
- package/dist/interface.js +37 -1
- package/dist/jsonl.d.ts +10 -1
- package/dist/jsonl.js +94 -0
- package/dist/neon.d.ts +9 -1
- package/dist/neon.js +142 -0
- package/dist/plan-perspectives.d.ts +18 -0
- package/dist/plan-perspectives.js +53 -0
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
* const node = await store.getNode('elder-1');
|
|
16
16
|
* ```
|
|
17
17
|
*/
|
|
18
|
-
export type { StorageProvider, RelationalNode, RelationalEdge, CeremonyLog, WeaveRecord, WeaveSyncState, InquiryWeaveFilters, ProviderType, } from './interface.js';
|
|
18
|
+
export type { StorageProvider, RelationalNode, RelationalEdge, CeremonyLog, WeaveRecord, WeaveSyncState, InquiryWeaveFilters, PlanPerspectiveRecord, PlanPerspectiveEpisode, PlanPerspectiveFilters, ProviderType, NodePatch, EdgePatch, } from './interface.js';
|
|
19
|
+
export { NodeNotFoundError, EdgeNotFoundError, NodeHasRelationsError, } from './interface.js';
|
|
20
|
+
export { mergePlanPerspectiveRecords, unionPlanPerspectiveEpisodes, matchesPlanPerspectiveFilters, } from './plan-perspectives.js';
|
|
19
21
|
export { createProvider, detectProvider } from './factory.js';
|
|
20
22
|
export { JsonlProvider } from './jsonl.js';
|
|
21
23
|
export { NeonProvider } from './neon.js';
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,10 @@
|
|
|
15
15
|
* const node = await store.getNode('elder-1');
|
|
16
16
|
* ```
|
|
17
17
|
*/
|
|
18
|
+
// Typed errors (thrown by update/delete operations)
|
|
19
|
+
export { NodeNotFoundError, EdgeNotFoundError, NodeHasRelationsError, } from './interface.js';
|
|
20
|
+
// Plan Perspective merge/filter semantics (shared across providers)
|
|
21
|
+
export { mergePlanPerspectiveRecords, unionPlanPerspectiveEpisodes, matchesPlanPerspectiveFilters, } from './plan-perspectives.js';
|
|
18
22
|
// Factory
|
|
19
23
|
export { createProvider, detectProvider } from './factory.js';
|
|
20
24
|
// Providers (for direct instantiation)
|
package/dist/interface.d.ts
CHANGED
|
@@ -13,6 +13,42 @@ export interface RelationalEdge extends Omit<OntologyRelationalEdge, 'id'> {
|
|
|
13
13
|
last_ceremony?: string;
|
|
14
14
|
}
|
|
15
15
|
export type CeremonyLog = OntologyCeremonyLog;
|
|
16
|
+
/** Partial update for a node. `direction: null` releases the node from its direction. */
|
|
17
|
+
export interface NodePatch {
|
|
18
|
+
name?: string;
|
|
19
|
+
type?: NodeType;
|
|
20
|
+
description?: string;
|
|
21
|
+
direction?: DirectionName | null;
|
|
22
|
+
metadata?: Record<string, unknown>;
|
|
23
|
+
}
|
|
24
|
+
/** Partial update for an edge (identified by from_id + to_id). */
|
|
25
|
+
export interface EdgePatch {
|
|
26
|
+
relationship_type?: string;
|
|
27
|
+
strength?: number;
|
|
28
|
+
ceremony_honored?: boolean;
|
|
29
|
+
obligations?: string[];
|
|
30
|
+
}
|
|
31
|
+
export declare class NodeNotFoundError extends Error {
|
|
32
|
+
readonly nodeId: string;
|
|
33
|
+
readonly code = "NODE_NOT_FOUND";
|
|
34
|
+
constructor(nodeId: string);
|
|
35
|
+
}
|
|
36
|
+
export declare class EdgeNotFoundError extends Error {
|
|
37
|
+
readonly fromId: string;
|
|
38
|
+
readonly toId: string;
|
|
39
|
+
readonly code = "EDGE_NOT_FOUND";
|
|
40
|
+
constructor(fromId: string, toId: string);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Deleting a node with living relations is refused: relational accountability
|
|
44
|
+
* means severing each relation consciously before releasing the node.
|
|
45
|
+
*/
|
|
46
|
+
export declare class NodeHasRelationsError extends Error {
|
|
47
|
+
readonly nodeId: string;
|
|
48
|
+
readonly relationCount: number;
|
|
49
|
+
readonly code = "NODE_HAS_RELATIONS";
|
|
50
|
+
constructor(nodeId: string, relationCount: number);
|
|
51
|
+
}
|
|
16
52
|
export type WeaveSyncState = 'never-synced' | 'in-sync' | 'stale' | 'episode-copy-diverged';
|
|
17
53
|
export interface WeaveRecord extends Record<string, unknown> {
|
|
18
54
|
id: string;
|
|
@@ -51,6 +87,56 @@ export interface InquiryWeaveFilters {
|
|
|
51
87
|
issue?: string;
|
|
52
88
|
artefact?: string;
|
|
53
89
|
}
|
|
90
|
+
export interface PlanPerspectiveEpisode {
|
|
91
|
+
path: string;
|
|
92
|
+
number?: number;
|
|
93
|
+
[key: string]: unknown;
|
|
94
|
+
}
|
|
95
|
+
export interface PlanPerspectiveRecord extends Record<string, unknown> {
|
|
96
|
+
id: string;
|
|
97
|
+
perspective: 1;
|
|
98
|
+
plan: {
|
|
99
|
+
session_id: string;
|
|
100
|
+
plan_path?: string;
|
|
101
|
+
plan_filename: string;
|
|
102
|
+
plan_sha256: string;
|
|
103
|
+
captured_at?: string;
|
|
104
|
+
[key: string]: unknown;
|
|
105
|
+
};
|
|
106
|
+
narrative: {
|
|
107
|
+
title: string;
|
|
108
|
+
body_markdown: string;
|
|
109
|
+
mia_context?: string;
|
|
110
|
+
[key: string]: unknown;
|
|
111
|
+
};
|
|
112
|
+
lineage?: {
|
|
113
|
+
user_inputs_path?: string;
|
|
114
|
+
input_count?: number;
|
|
115
|
+
first_input_at?: string;
|
|
116
|
+
last_input_at?: string;
|
|
117
|
+
excerpts?: string[];
|
|
118
|
+
[key: string]: unknown;
|
|
119
|
+
};
|
|
120
|
+
episodes: PlanPerspectiveEpisode[];
|
|
121
|
+
source: {
|
|
122
|
+
package?: string;
|
|
123
|
+
generator?: {
|
|
124
|
+
system?: string;
|
|
125
|
+
agent?: string;
|
|
126
|
+
model?: string;
|
|
127
|
+
producer_session_id?: string;
|
|
128
|
+
[key: string]: unknown;
|
|
129
|
+
};
|
|
130
|
+
registered_at: string;
|
|
131
|
+
updated_at: string;
|
|
132
|
+
[key: string]: unknown;
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
export interface PlanPerspectiveFilters {
|
|
136
|
+
episode_path?: string;
|
|
137
|
+
session_id?: string;
|
|
138
|
+
id?: string;
|
|
139
|
+
}
|
|
54
140
|
export interface StorageProvider {
|
|
55
141
|
readonly name: string;
|
|
56
142
|
connect(): Promise<void>;
|
|
@@ -60,11 +146,19 @@ export interface StorageProvider {
|
|
|
60
146
|
getNodesByType(type: NodeType): Promise<RelationalNode[]>;
|
|
61
147
|
getNodesByDirection(direction: DirectionName): Promise<RelationalNode[]>;
|
|
62
148
|
getAllNodes(limit?: number): Promise<RelationalNode[]>;
|
|
149
|
+
/** Throws NodeNotFoundError when the node does not exist. */
|
|
150
|
+
updateNode(id: string, patch: NodePatch): Promise<RelationalNode>;
|
|
151
|
+
/** Throws NodeNotFoundError or NodeHasRelationsError (refusal, never cascade). */
|
|
152
|
+
deleteNode(id: string): Promise<void>;
|
|
63
153
|
createEdge(edge: RelationalEdge): Promise<void>;
|
|
64
154
|
getEdge(fromId: string, toId: string): Promise<RelationalEdge | null>;
|
|
65
155
|
getAllEdges(limit?: number): Promise<RelationalEdge[]>;
|
|
66
156
|
getRelatedNodes(nodeId: string, direction?: 'from' | 'to' | 'both'): Promise<string[]>;
|
|
67
157
|
updateEdgeCeremony(fromId: string, toId: string, ceremonyId: string): Promise<void>;
|
|
158
|
+
/** Throws EdgeNotFoundError when no relation exists between the pair. */
|
|
159
|
+
updateEdge(fromId: string, toId: string, patch: EdgePatch): Promise<RelationalEdge>;
|
|
160
|
+
/** Throws EdgeNotFoundError when no relation exists between the pair. */
|
|
161
|
+
deleteEdge(fromId: string, toId: string): Promise<void>;
|
|
68
162
|
logCeremony(ceremony: CeremonyLog): Promise<void>;
|
|
69
163
|
getCeremony(id: string): Promise<CeremonyLog | null>;
|
|
70
164
|
getCeremoniesTimeline(limit?: number): Promise<CeremonyLog[]>;
|
|
@@ -74,5 +168,8 @@ export interface StorageProvider {
|
|
|
74
168
|
registerInquiryWeave(record: WeaveRecord): Promise<void>;
|
|
75
169
|
getInquiryWeave(id: string): Promise<WeaveRecord | null>;
|
|
76
170
|
listInquiryWeaves(filters?: InquiryWeaveFilters): Promise<WeaveRecord[]>;
|
|
171
|
+
registerPlanPerspective(record: PlanPerspectiveRecord): Promise<PlanPerspectiveRecord>;
|
|
172
|
+
getPlanPerspective(id: string): Promise<PlanPerspectiveRecord | null>;
|
|
173
|
+
listPlanPerspectives(filters?: PlanPerspectiveFilters): Promise<PlanPerspectiveRecord[]>;
|
|
77
174
|
}
|
|
78
175
|
export type ProviderType = 'jsonl' | 'neon' | 'redis' | 'auto';
|
package/dist/interface.js
CHANGED
|
@@ -4,4 +4,40 @@
|
|
|
4
4
|
* Abstract interface for Medicine Wheel data persistence.
|
|
5
5
|
* Implementations: JsonlProvider (local/default), NeonProvider (Postgres), RedisProvider (future)
|
|
6
6
|
*/
|
|
7
|
-
|
|
7
|
+
// ── Typed Errors ──
|
|
8
|
+
// Providers throw these so API layers can answer with honest status codes.
|
|
9
|
+
export class NodeNotFoundError extends Error {
|
|
10
|
+
nodeId;
|
|
11
|
+
code = 'NODE_NOT_FOUND';
|
|
12
|
+
constructor(nodeId) {
|
|
13
|
+
super(`Node not found: ${nodeId}`);
|
|
14
|
+
this.nodeId = nodeId;
|
|
15
|
+
this.name = 'NodeNotFoundError';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export class EdgeNotFoundError extends Error {
|
|
19
|
+
fromId;
|
|
20
|
+
toId;
|
|
21
|
+
code = 'EDGE_NOT_FOUND';
|
|
22
|
+
constructor(fromId, toId) {
|
|
23
|
+
super(`Relation not found between ${fromId} and ${toId}`);
|
|
24
|
+
this.fromId = fromId;
|
|
25
|
+
this.toId = toId;
|
|
26
|
+
this.name = 'EdgeNotFoundError';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Deleting a node with living relations is refused: relational accountability
|
|
31
|
+
* means severing each relation consciously before releasing the node.
|
|
32
|
+
*/
|
|
33
|
+
export class NodeHasRelationsError extends Error {
|
|
34
|
+
nodeId;
|
|
35
|
+
relationCount;
|
|
36
|
+
code = 'NODE_HAS_RELATIONS';
|
|
37
|
+
constructor(nodeId, relationCount) {
|
|
38
|
+
super(`This node holds ${relationCount} relation${relationCount === 1 ? '' : 's'} — release them first`);
|
|
39
|
+
this.nodeId = nodeId;
|
|
40
|
+
this.relationCount = relationCount;
|
|
41
|
+
this.name = 'NodeHasRelationsError';
|
|
42
|
+
}
|
|
43
|
+
}
|
package/dist/jsonl.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DirectionName, NodeType, CeremonyType } from '@medicine-wheel/ontology-core';
|
|
2
|
-
import type { StorageProvider, RelationalNode, RelationalEdge, CeremonyLog, WeaveRecord, InquiryWeaveFilters } from './interface.js';
|
|
2
|
+
import type { StorageProvider, RelationalNode, RelationalEdge, CeremonyLog, WeaveRecord, InquiryWeaveFilters, PlanPerspectiveRecord, PlanPerspectiveFilters, NodePatch, EdgePatch } from './interface.js';
|
|
3
3
|
export declare class JsonlProvider implements StorageProvider {
|
|
4
4
|
readonly name = "jsonl";
|
|
5
5
|
readonly dataDir: string;
|
|
@@ -7,6 +7,7 @@ export declare class JsonlProvider implements StorageProvider {
|
|
|
7
7
|
private readonly edgesFile;
|
|
8
8
|
private readonly ceremoniesFile;
|
|
9
9
|
private readonly inquiryWeavesFile;
|
|
10
|
+
private readonly planPerspectivesFile;
|
|
10
11
|
constructor(dataDir?: string);
|
|
11
12
|
connect(): Promise<void>;
|
|
12
13
|
disconnect(): Promise<void>;
|
|
@@ -15,11 +16,15 @@ export declare class JsonlProvider implements StorageProvider {
|
|
|
15
16
|
getNodesByType(type: NodeType): Promise<RelationalNode[]>;
|
|
16
17
|
getNodesByDirection(direction: DirectionName): Promise<RelationalNode[]>;
|
|
17
18
|
getAllNodes(limit?: number): Promise<RelationalNode[]>;
|
|
19
|
+
updateNode(id: string, patch: NodePatch): Promise<RelationalNode>;
|
|
20
|
+
deleteNode(id: string): Promise<void>;
|
|
18
21
|
createEdge(edge: RelationalEdge): Promise<void>;
|
|
19
22
|
getEdge(fromId: string, toId: string): Promise<RelationalEdge | null>;
|
|
20
23
|
getRelatedNodes(nodeId: string, direction?: 'from' | 'to' | 'both'): Promise<string[]>;
|
|
21
24
|
getAllEdges(limit?: number): Promise<RelationalEdge[]>;
|
|
22
25
|
updateEdgeCeremony(fromId: string, toId: string, ceremonyId: string): Promise<void>;
|
|
26
|
+
updateEdge(fromId: string, toId: string, patch: EdgePatch): Promise<RelationalEdge>;
|
|
27
|
+
deleteEdge(fromId: string, toId: string): Promise<void>;
|
|
23
28
|
logCeremony(ceremony: CeremonyLog): Promise<void>;
|
|
24
29
|
getCeremony(id: string): Promise<CeremonyLog | null>;
|
|
25
30
|
getCeremoniesTimeline(limit?: number): Promise<CeremonyLog[]>;
|
|
@@ -29,10 +34,14 @@ export declare class JsonlProvider implements StorageProvider {
|
|
|
29
34
|
registerInquiryWeave(record: WeaveRecord): Promise<void>;
|
|
30
35
|
getInquiryWeave(id: string): Promise<WeaveRecord | null>;
|
|
31
36
|
listInquiryWeaves(filters?: InquiryWeaveFilters): Promise<WeaveRecord[]>;
|
|
37
|
+
registerPlanPerspective(record: PlanPerspectiveRecord): Promise<PlanPerspectiveRecord>;
|
|
38
|
+
getPlanPerspective(id: string): Promise<PlanPerspectiveRecord | null>;
|
|
39
|
+
listPlanPerspectives(filters?: PlanPerspectiveFilters): Promise<PlanPerspectiveRecord[]>;
|
|
32
40
|
private readNodes;
|
|
33
41
|
private readEdges;
|
|
34
42
|
private readCeremonies;
|
|
35
43
|
private readInquiryWeaves;
|
|
44
|
+
private readPlanPerspectives;
|
|
36
45
|
private upsertById;
|
|
37
46
|
private upsertEdge;
|
|
38
47
|
private parseNode;
|
package/dist/jsonl.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { randomUUID } from 'crypto';
|
|
4
|
+
import { NodeNotFoundError, EdgeNotFoundError, NodeHasRelationsError, } from './interface.js';
|
|
5
|
+
import { mergePlanPerspectiveRecords, matchesPlanPerspectiveFilters, } from './plan-perspectives.js';
|
|
4
6
|
export class JsonlProvider {
|
|
5
7
|
name = 'jsonl';
|
|
6
8
|
dataDir;
|
|
@@ -8,12 +10,14 @@ export class JsonlProvider {
|
|
|
8
10
|
edgesFile;
|
|
9
11
|
ceremoniesFile;
|
|
10
12
|
inquiryWeavesFile;
|
|
13
|
+
planPerspectivesFile;
|
|
11
14
|
constructor(dataDir) {
|
|
12
15
|
this.dataDir = path.resolve(dataDir ?? resolveProjectDataDir());
|
|
13
16
|
this.nodesFile = path.join(this.dataDir, 'nodes.jsonl');
|
|
14
17
|
this.edgesFile = path.join(this.dataDir, 'edges.jsonl');
|
|
15
18
|
this.ceremoniesFile = path.join(this.dataDir, 'ceremonies.jsonl');
|
|
16
19
|
this.inquiryWeavesFile = path.join(this.dataDir, 'inquiry-weaves.jsonl');
|
|
20
|
+
this.planPerspectivesFile = path.join(this.dataDir, 'plan-perspectives.jsonl');
|
|
17
21
|
}
|
|
18
22
|
async connect() {
|
|
19
23
|
ensureDirectory(this.dataDir);
|
|
@@ -46,6 +50,42 @@ export class JsonlProvider {
|
|
|
46
50
|
.slice(0, limit)
|
|
47
51
|
.map((node) => this.parseNode(node));
|
|
48
52
|
}
|
|
53
|
+
async updateNode(id, patch) {
|
|
54
|
+
return withWriteLock(this.nodesFile, () => {
|
|
55
|
+
const nodes = this.readNodes();
|
|
56
|
+
const index = nodes.findIndex((candidate) => candidate.id === id);
|
|
57
|
+
if (index === -1)
|
|
58
|
+
throw new NodeNotFoundError(id);
|
|
59
|
+
const current = nodes[index];
|
|
60
|
+
const next = {
|
|
61
|
+
...current,
|
|
62
|
+
...(patch.name !== undefined ? { name: patch.name } : {}),
|
|
63
|
+
...(patch.type !== undefined ? { type: patch.type } : {}),
|
|
64
|
+
...(patch.description !== undefined ? { description: patch.description } : {}),
|
|
65
|
+
...(patch.metadata !== undefined ? { metadata: patch.metadata } : {}),
|
|
66
|
+
updated_at: new Date().toISOString(),
|
|
67
|
+
};
|
|
68
|
+
if (patch.direction !== undefined) {
|
|
69
|
+
next.direction = patch.direction === null ? undefined : patch.direction;
|
|
70
|
+
}
|
|
71
|
+
nodes[index] = next;
|
|
72
|
+
writeJsonl(this.nodesFile, nodes);
|
|
73
|
+
return this.parseNode(next);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
async deleteNode(id) {
|
|
77
|
+
await withWriteLock(this.nodesFile, () => {
|
|
78
|
+
const nodes = this.readNodes();
|
|
79
|
+
if (!nodes.some((candidate) => candidate.id === id)) {
|
|
80
|
+
throw new NodeNotFoundError(id);
|
|
81
|
+
}
|
|
82
|
+
const relationCount = this.readEdges().filter((edge) => edge.from_id === id || edge.to_id === id).length;
|
|
83
|
+
if (relationCount > 0) {
|
|
84
|
+
throw new NodeHasRelationsError(id, relationCount);
|
|
85
|
+
}
|
|
86
|
+
writeJsonl(this.nodesFile, nodes.filter((candidate) => candidate.id !== id));
|
|
87
|
+
});
|
|
88
|
+
}
|
|
49
89
|
async createEdge(edge) {
|
|
50
90
|
await this.upsertEdge({
|
|
51
91
|
...edge,
|
|
@@ -89,6 +129,38 @@ export class JsonlProvider {
|
|
|
89
129
|
writeJsonl(this.edgesFile, nextEdges);
|
|
90
130
|
});
|
|
91
131
|
}
|
|
132
|
+
async updateEdge(fromId, toId, patch) {
|
|
133
|
+
return withWriteLock(this.edgesFile, () => {
|
|
134
|
+
const edges = this.readEdges();
|
|
135
|
+
const index = edges.findIndex((candidate) => candidate.from_id === fromId && candidate.to_id === toId);
|
|
136
|
+
if (index === -1)
|
|
137
|
+
throw new EdgeNotFoundError(fromId, toId);
|
|
138
|
+
const next = {
|
|
139
|
+
...edges[index],
|
|
140
|
+
...(patch.relationship_type !== undefined
|
|
141
|
+
? { relationship_type: patch.relationship_type }
|
|
142
|
+
: {}),
|
|
143
|
+
...(patch.strength !== undefined ? { strength: patch.strength } : {}),
|
|
144
|
+
...(patch.ceremony_honored !== undefined
|
|
145
|
+
? { ceremony_honored: patch.ceremony_honored }
|
|
146
|
+
: {}),
|
|
147
|
+
...(patch.obligations !== undefined ? { obligations: patch.obligations } : {}),
|
|
148
|
+
};
|
|
149
|
+
edges[index] = next;
|
|
150
|
+
writeJsonl(this.edgesFile, edges);
|
|
151
|
+
return this.parseEdge(next);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
async deleteEdge(fromId, toId) {
|
|
155
|
+
await withWriteLock(this.edgesFile, () => {
|
|
156
|
+
const edges = this.readEdges();
|
|
157
|
+
const remaining = edges.filter((candidate) => !(candidate.from_id === fromId && candidate.to_id === toId));
|
|
158
|
+
if (remaining.length === edges.length) {
|
|
159
|
+
throw new EdgeNotFoundError(fromId, toId);
|
|
160
|
+
}
|
|
161
|
+
writeJsonl(this.edgesFile, remaining);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
92
164
|
async logCeremony(ceremony) {
|
|
93
165
|
await this.upsertById(this.ceremoniesFile, ceremony);
|
|
94
166
|
}
|
|
@@ -129,6 +201,24 @@ export class JsonlProvider {
|
|
|
129
201
|
async listInquiryWeaves(filters = {}) {
|
|
130
202
|
return this.readInquiryWeaves().filter((record) => matchesInquiryWeaveFilters(record, filters));
|
|
131
203
|
}
|
|
204
|
+
async registerPlanPerspective(record) {
|
|
205
|
+
let merged = record;
|
|
206
|
+
await withWriteLock(this.planPerspectivesFile, () => {
|
|
207
|
+
const records = readJsonl(this.planPerspectivesFile);
|
|
208
|
+
const existing = records.find((candidate) => candidate.id === record.id);
|
|
209
|
+
merged = existing ? mergePlanPerspectiveRecords(existing, record) : record;
|
|
210
|
+
const nextRecords = records.filter((candidate) => candidate.id !== record.id);
|
|
211
|
+
nextRecords.push(merged);
|
|
212
|
+
writeJsonl(this.planPerspectivesFile, nextRecords);
|
|
213
|
+
});
|
|
214
|
+
return merged;
|
|
215
|
+
}
|
|
216
|
+
async getPlanPerspective(id) {
|
|
217
|
+
return this.readPlanPerspectives().find((record) => record.id === id) ?? null;
|
|
218
|
+
}
|
|
219
|
+
async listPlanPerspectives(filters = {}) {
|
|
220
|
+
return this.readPlanPerspectives().filter((record) => matchesPlanPerspectiveFilters(record, filters));
|
|
221
|
+
}
|
|
132
222
|
readNodes() {
|
|
133
223
|
return readJsonl(this.nodesFile);
|
|
134
224
|
}
|
|
@@ -141,6 +231,9 @@ export class JsonlProvider {
|
|
|
141
231
|
readInquiryWeaves() {
|
|
142
232
|
return readJsonl(this.inquiryWeavesFile);
|
|
143
233
|
}
|
|
234
|
+
readPlanPerspectives() {
|
|
235
|
+
return readJsonl(this.planPerspectivesFile);
|
|
236
|
+
}
|
|
144
237
|
async upsertById(filePath, item) {
|
|
145
238
|
await withWriteLock(filePath, () => {
|
|
146
239
|
const nextRecords = readJsonl(filePath).filter((candidate) => candidate.id !== item.id);
|
|
@@ -169,6 +262,7 @@ export class JsonlProvider {
|
|
|
169
262
|
}
|
|
170
263
|
parseEdge(edge) {
|
|
171
264
|
return {
|
|
265
|
+
id: edge.id ?? `${edge.from_id}:${edge.to_id}`,
|
|
172
266
|
from_id: edge.from_id,
|
|
173
267
|
to_id: edge.to_id,
|
|
174
268
|
relationship_type: edge.relationship_type,
|
package/dist/neon.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Neon (Postgres) Storage Provider
|
|
3
3
|
*/
|
|
4
4
|
import type { DirectionName, NodeType, CeremonyType } from '@medicine-wheel/ontology-core';
|
|
5
|
-
import type { StorageProvider, RelationalNode, RelationalEdge, CeremonyLog, WeaveRecord, InquiryWeaveFilters } from './interface.js';
|
|
5
|
+
import type { StorageProvider, RelationalNode, RelationalEdge, CeremonyLog, WeaveRecord, InquiryWeaveFilters, PlanPerspectiveRecord, PlanPerspectiveFilters, NodePatch, EdgePatch } from './interface.js';
|
|
6
6
|
export declare class NeonProvider implements StorageProvider {
|
|
7
7
|
readonly name = "neon";
|
|
8
8
|
private sql;
|
|
@@ -14,12 +14,16 @@ export declare class NeonProvider implements StorageProvider {
|
|
|
14
14
|
getNodesByType(type: NodeType): Promise<RelationalNode[]>;
|
|
15
15
|
getNodesByDirection(direction: DirectionName): Promise<RelationalNode[]>;
|
|
16
16
|
getAllNodes(limit?: number): Promise<RelationalNode[]>;
|
|
17
|
+
updateNode(id: string, patch: NodePatch): Promise<RelationalNode>;
|
|
18
|
+
deleteNode(id: string): Promise<void>;
|
|
17
19
|
private parseNode;
|
|
18
20
|
createEdge(edge: RelationalEdge): Promise<void>;
|
|
19
21
|
getEdge(fromId: string, toId: string): Promise<RelationalEdge | null>;
|
|
20
22
|
getRelatedNodes(nodeId: string, direction?: 'from' | 'to' | 'both'): Promise<string[]>;
|
|
21
23
|
getAllEdges(limit?: number): Promise<RelationalEdge[]>;
|
|
22
24
|
updateEdgeCeremony(fromId: string, toId: string, ceremonyId: string): Promise<void>;
|
|
25
|
+
updateEdge(fromId: string, toId: string, patch: EdgePatch): Promise<RelationalEdge>;
|
|
26
|
+
deleteEdge(fromId: string, toId: string): Promise<void>;
|
|
23
27
|
private parseEdge;
|
|
24
28
|
logCeremony(ceremony: CeremonyLog): Promise<void>;
|
|
25
29
|
getCeremony(id: string): Promise<CeremonyLog | null>;
|
|
@@ -33,4 +37,8 @@ export declare class NeonProvider implements StorageProvider {
|
|
|
33
37
|
listInquiryWeaves(filters?: InquiryWeaveFilters): Promise<WeaveRecord[]>;
|
|
34
38
|
private getAllInquiryWeaves;
|
|
35
39
|
private ensureInquiryWeavesTable;
|
|
40
|
+
registerPlanPerspective(record: PlanPerspectiveRecord): Promise<PlanPerspectiveRecord>;
|
|
41
|
+
getPlanPerspective(id: string): Promise<PlanPerspectiveRecord | null>;
|
|
42
|
+
listPlanPerspectives(filters?: PlanPerspectiveFilters): Promise<PlanPerspectiveRecord[]>;
|
|
43
|
+
private ensurePlanPerspectivesTable;
|
|
36
44
|
}
|
package/dist/neon.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Neon (Postgres) Storage Provider
|
|
3
3
|
*/
|
|
4
|
+
import { NodeNotFoundError, EdgeNotFoundError, NodeHasRelationsError, } from './interface.js';
|
|
5
|
+
import { mergePlanPerspectiveRecords, matchesPlanPerspectiveFilters, } from './plan-perspectives.js';
|
|
4
6
|
export class NeonProvider {
|
|
5
7
|
name = 'neon';
|
|
6
8
|
sql = null;
|
|
@@ -11,6 +13,7 @@ export class NeonProvider {
|
|
|
11
13
|
const { neon } = await import('@neondatabase/serverless');
|
|
12
14
|
this.sql = neon(url);
|
|
13
15
|
await this.ensureInquiryWeavesTable();
|
|
16
|
+
await this.ensurePlanPerspectivesTable();
|
|
14
17
|
}
|
|
15
18
|
async disconnect() {
|
|
16
19
|
this.sql = null;
|
|
@@ -54,6 +57,45 @@ export class NeonProvider {
|
|
|
54
57
|
const rows = await this.db `SELECT * FROM nodes ORDER BY created_at DESC LIMIT ${limit}`;
|
|
55
58
|
return rows.map((row) => this.parseNode(row));
|
|
56
59
|
}
|
|
60
|
+
async updateNode(id, patch) {
|
|
61
|
+
const existing = await this.getNode(id);
|
|
62
|
+
if (!existing)
|
|
63
|
+
throw new NodeNotFoundError(id);
|
|
64
|
+
const next = {
|
|
65
|
+
...existing,
|
|
66
|
+
...(patch.name !== undefined ? { name: patch.name } : {}),
|
|
67
|
+
...(patch.type !== undefined ? { type: patch.type } : {}),
|
|
68
|
+
...(patch.description !== undefined ? { description: patch.description } : {}),
|
|
69
|
+
...(patch.metadata !== undefined ? { metadata: patch.metadata } : {}),
|
|
70
|
+
updated_at: new Date().toISOString(),
|
|
71
|
+
};
|
|
72
|
+
if (patch.direction !== undefined) {
|
|
73
|
+
next.direction = patch.direction === null ? undefined : patch.direction;
|
|
74
|
+
}
|
|
75
|
+
await this.db `
|
|
76
|
+
UPDATE nodes SET
|
|
77
|
+
name = ${next.name},
|
|
78
|
+
type = ${next.type},
|
|
79
|
+
description = ${next.description ?? ''},
|
|
80
|
+
direction = ${next.direction || null},
|
|
81
|
+
metadata = ${JSON.stringify(next.metadata)},
|
|
82
|
+
updated_at = ${next.updated_at}
|
|
83
|
+
WHERE id = ${id}
|
|
84
|
+
`;
|
|
85
|
+
return next;
|
|
86
|
+
}
|
|
87
|
+
async deleteNode(id) {
|
|
88
|
+
const existing = await this.db `SELECT id FROM nodes WHERE id = ${id}`;
|
|
89
|
+
if (existing.length === 0)
|
|
90
|
+
throw new NodeNotFoundError(id);
|
|
91
|
+
const counts = await this.db `
|
|
92
|
+
SELECT COUNT(*)::int AS count FROM edges WHERE from_id = ${id} OR to_id = ${id}
|
|
93
|
+
`;
|
|
94
|
+
const relationCount = Number(counts[0]?.count ?? 0);
|
|
95
|
+
if (relationCount > 0)
|
|
96
|
+
throw new NodeHasRelationsError(id, relationCount);
|
|
97
|
+
await this.db `DELETE FROM nodes WHERE id = ${id}`;
|
|
98
|
+
}
|
|
57
99
|
parseNode(row) {
|
|
58
100
|
return {
|
|
59
101
|
id: row.id,
|
|
@@ -113,8 +155,41 @@ export class NeonProvider {
|
|
|
113
155
|
WHERE from_id = ${fromId} AND to_id = ${toId}
|
|
114
156
|
`;
|
|
115
157
|
}
|
|
158
|
+
async updateEdge(fromId, toId, patch) {
|
|
159
|
+
const existing = await this.getEdge(fromId, toId);
|
|
160
|
+
if (!existing)
|
|
161
|
+
throw new EdgeNotFoundError(fromId, toId);
|
|
162
|
+
const next = {
|
|
163
|
+
...existing,
|
|
164
|
+
...(patch.relationship_type !== undefined
|
|
165
|
+
? { relationship_type: patch.relationship_type }
|
|
166
|
+
: {}),
|
|
167
|
+
...(patch.strength !== undefined ? { strength: patch.strength } : {}),
|
|
168
|
+
...(patch.ceremony_honored !== undefined
|
|
169
|
+
? { ceremony_honored: patch.ceremony_honored }
|
|
170
|
+
: {}),
|
|
171
|
+
...(patch.obligations !== undefined ? { obligations: patch.obligations } : {}),
|
|
172
|
+
};
|
|
173
|
+
await this.db `
|
|
174
|
+
UPDATE edges SET
|
|
175
|
+
relationship_type = ${next.relationship_type},
|
|
176
|
+
strength = ${next.strength},
|
|
177
|
+
ceremony_honored = ${next.ceremony_honored},
|
|
178
|
+
obligations = ${JSON.stringify(next.obligations)}
|
|
179
|
+
WHERE from_id = ${fromId} AND to_id = ${toId}
|
|
180
|
+
`;
|
|
181
|
+
return next;
|
|
182
|
+
}
|
|
183
|
+
async deleteEdge(fromId, toId) {
|
|
184
|
+
const deleted = await this.db `
|
|
185
|
+
DELETE FROM edges WHERE from_id = ${fromId} AND to_id = ${toId} RETURNING from_id
|
|
186
|
+
`;
|
|
187
|
+
if (deleted.length === 0)
|
|
188
|
+
throw new EdgeNotFoundError(fromId, toId);
|
|
189
|
+
}
|
|
116
190
|
parseEdge(row) {
|
|
117
191
|
return {
|
|
192
|
+
id: `${row.from_id}:${row.to_id}`,
|
|
118
193
|
from_id: row.from_id,
|
|
119
194
|
to_id: row.to_id,
|
|
120
195
|
relationship_type: row.relationship_type,
|
|
@@ -228,6 +303,73 @@ export class NeonProvider {
|
|
|
228
303
|
await this.db `CREATE INDEX IF NOT EXISTS idx_inquiry_weaves_issue ON inquiry_weaves(issue)`;
|
|
229
304
|
await this.db `CREATE INDEX IF NOT EXISTS idx_inquiry_weaves_artefact_id ON inquiry_weaves(artefact_id)`;
|
|
230
305
|
}
|
|
306
|
+
// ── Plan Perspective Operations ──
|
|
307
|
+
async registerPlanPerspective(record) {
|
|
308
|
+
const existing = await this.getPlanPerspective(record.id);
|
|
309
|
+
const merged = existing ? mergePlanPerspectiveRecords(existing, record) : record;
|
|
310
|
+
const episodePaths = merged.episodes.map((episode) => episode.path);
|
|
311
|
+
await this.db `
|
|
312
|
+
INSERT INTO plan_perspectives (id, payload, session_id, episode_paths, updated_at)
|
|
313
|
+
VALUES (
|
|
314
|
+
${merged.id},
|
|
315
|
+
${JSON.stringify(merged)},
|
|
316
|
+
${merged.plan.session_id},
|
|
317
|
+
${episodePaths},
|
|
318
|
+
NOW()
|
|
319
|
+
)
|
|
320
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
321
|
+
payload = EXCLUDED.payload,
|
|
322
|
+
session_id = EXCLUDED.session_id,
|
|
323
|
+
episode_paths = EXCLUDED.episode_paths,
|
|
324
|
+
updated_at = NOW()
|
|
325
|
+
`;
|
|
326
|
+
return merged;
|
|
327
|
+
}
|
|
328
|
+
async getPlanPerspective(id) {
|
|
329
|
+
const rows = await this.db `SELECT payload FROM plan_perspectives WHERE id = ${id}`;
|
|
330
|
+
if (rows.length === 0)
|
|
331
|
+
return null;
|
|
332
|
+
return parseJsonValue(rows[0].payload, null);
|
|
333
|
+
}
|
|
334
|
+
async listPlanPerspectives(filters = {}) {
|
|
335
|
+
let rows;
|
|
336
|
+
if (filters.id !== undefined) {
|
|
337
|
+
rows = await this.db `SELECT payload FROM plan_perspectives WHERE id = ${filters.id}`;
|
|
338
|
+
}
|
|
339
|
+
else if (filters.session_id !== undefined) {
|
|
340
|
+
rows = await this.db `
|
|
341
|
+
SELECT payload FROM plan_perspectives
|
|
342
|
+
WHERE session_id = ${filters.session_id}
|
|
343
|
+
ORDER BY updated_at DESC
|
|
344
|
+
`;
|
|
345
|
+
}
|
|
346
|
+
else if (filters.episode_path !== undefined) {
|
|
347
|
+
rows = await this.db `
|
|
348
|
+
SELECT payload FROM plan_perspectives
|
|
349
|
+
WHERE episode_paths @> ARRAY[${filters.episode_path}]
|
|
350
|
+
ORDER BY updated_at DESC
|
|
351
|
+
`;
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
rows = await this.db `SELECT payload FROM plan_perspectives ORDER BY updated_at DESC`;
|
|
355
|
+
}
|
|
356
|
+
return rows
|
|
357
|
+
.map((row) => parseJsonValue(row.payload, null))
|
|
358
|
+
.filter((record) => record !== null && matchesPlanPerspectiveFilters(record, filters));
|
|
359
|
+
}
|
|
360
|
+
async ensurePlanPerspectivesTable() {
|
|
361
|
+
await this.db `
|
|
362
|
+
CREATE TABLE IF NOT EXISTS plan_perspectives (
|
|
363
|
+
id TEXT PRIMARY KEY,
|
|
364
|
+
payload JSONB NOT NULL,
|
|
365
|
+
session_id TEXT NOT NULL,
|
|
366
|
+
episode_paths TEXT[] NOT NULL DEFAULT '{}',
|
|
367
|
+
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
368
|
+
)
|
|
369
|
+
`;
|
|
370
|
+
await this.db `CREATE INDEX IF NOT EXISTS idx_plan_perspectives_session_id ON plan_perspectives(session_id)`;
|
|
371
|
+
await this.db `CREATE INDEX IF NOT EXISTS idx_plan_perspectives_episode_paths ON plan_perspectives USING GIN(episode_paths)`;
|
|
372
|
+
}
|
|
231
373
|
}
|
|
232
374
|
function getConnectionString() {
|
|
233
375
|
return process.env.DATABASE_URL ?? process.env.POSTGRES_URL ?? process.env.NEON_DATABASE_URL;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan Perspective merge and filter semantics shared by every provider.
|
|
3
|
+
*
|
|
4
|
+
* Medicine-wheel stores relational projections of Miette perspectives; the
|
|
5
|
+
* session file remains authoritative. Nothing here generates or rewrites
|
|
6
|
+
* narrative prose — records pass through untouched apart from the episode
|
|
7
|
+
* union and source timestamps pinned by @miadi/plan-insight's
|
|
8
|
+
* mergePerspectiveRecords contract.
|
|
9
|
+
*/
|
|
10
|
+
import type { PlanPerspectiveEpisode, PlanPerspectiveFilters, PlanPerspectiveRecord } from './interface.js';
|
|
11
|
+
/** Union episode relations by exact path; a known number backfills an entry missing one. */
|
|
12
|
+
export declare function unionPlanPerspectiveEpisodes(episodes: readonly PlanPerspectiveEpisode[]): PlanPerspectiveEpisode[];
|
|
13
|
+
/**
|
|
14
|
+
* Upsert merge: the incoming projection wins, episodes union by path, and the
|
|
15
|
+
* original registration timestamp survives while updated_at refreshes.
|
|
16
|
+
*/
|
|
17
|
+
export declare function mergePlanPerspectiveRecords(existing: PlanPerspectiveRecord, incoming: PlanPerspectiveRecord): PlanPerspectiveRecord;
|
|
18
|
+
export declare function matchesPlanPerspectiveFilters(record: PlanPerspectiveRecord, filters: PlanPerspectiveFilters): boolean;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan Perspective merge and filter semantics shared by every provider.
|
|
3
|
+
*
|
|
4
|
+
* Medicine-wheel stores relational projections of Miette perspectives; the
|
|
5
|
+
* session file remains authoritative. Nothing here generates or rewrites
|
|
6
|
+
* narrative prose — records pass through untouched apart from the episode
|
|
7
|
+
* union and source timestamps pinned by @miadi/plan-insight's
|
|
8
|
+
* mergePerspectiveRecords contract.
|
|
9
|
+
*/
|
|
10
|
+
/** Union episode relations by exact path; a known number backfills an entry missing one. */
|
|
11
|
+
export function unionPlanPerspectiveEpisodes(episodes) {
|
|
12
|
+
const byPath = new Map();
|
|
13
|
+
for (const episode of episodes) {
|
|
14
|
+
if (typeof episode?.path !== 'string' || episode.path.length === 0)
|
|
15
|
+
continue;
|
|
16
|
+
const existing = byPath.get(episode.path);
|
|
17
|
+
if (!existing) {
|
|
18
|
+
byPath.set(episode.path, { ...episode });
|
|
19
|
+
}
|
|
20
|
+
else if (existing.number === undefined && typeof episode.number === 'number') {
|
|
21
|
+
existing.number = episode.number;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return [...byPath.values()];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Upsert merge: the incoming projection wins, episodes union by path, and the
|
|
28
|
+
* original registration timestamp survives while updated_at refreshes.
|
|
29
|
+
*/
|
|
30
|
+
export function mergePlanPerspectiveRecords(existing, incoming) {
|
|
31
|
+
return {
|
|
32
|
+
...incoming,
|
|
33
|
+
episodes: unionPlanPerspectiveEpisodes([...existing.episodes, ...incoming.episodes]),
|
|
34
|
+
source: {
|
|
35
|
+
...incoming.source,
|
|
36
|
+
registered_at: existing.source.registered_at,
|
|
37
|
+
updated_at: incoming.source.updated_at,
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export function matchesPlanPerspectiveFilters(record, filters) {
|
|
42
|
+
if (filters.id !== undefined && record.id !== filters.id) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
if (filters.session_id !== undefined && record.plan?.session_id !== filters.session_id) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
if (filters.episode_path !== undefined &&
|
|
49
|
+
!record.episodes?.some((episode) => episode?.path === filters.episode_path)) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
return true;
|
|
53
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@medicine-wheel/storage-provider",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@neondatabase/serverless": "^0.10.0",
|
|
18
18
|
"@upstash/redis": "^1.34.3",
|
|
19
|
-
"@medicine-wheel/ontology-core": "^0.
|
|
19
|
+
"@medicine-wheel/ontology-core": "^0.5.0"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"typescript": "^5.7.0"
|