@medicine-wheel/storage-provider 0.4.11 → 0.4.13
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 +2 -1
- package/dist/index.js +2 -0
- package/dist/interface.d.ts +94 -0
- package/dist/jsonl.d.ts +11 -1
- package/dist/jsonl.js +53 -0
- package/dist/neon.d.ts +10 -1
- package/dist/neon.js +138 -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,8 @@
|
|
|
15
15
|
* const node = await store.getNode('elder-1');
|
|
16
16
|
* ```
|
|
17
17
|
*/
|
|
18
|
-
export type { StorageProvider, RelationalNode, RelationalEdge, CeremonyLog, ProviderType, } from './interface.js';
|
|
18
|
+
export type { StorageProvider, RelationalNode, RelationalEdge, CeremonyLog, WeaveRecord, WeaveSyncState, InquiryWeaveFilters, PlanPerspectiveRecord, PlanPerspectiveEpisode, PlanPerspectiveFilters, ProviderType, } from './interface.js';
|
|
19
|
+
export { mergePlanPerspectiveRecords, unionPlanPerspectiveEpisodes, matchesPlanPerspectiveFilters, } from './plan-perspectives.js';
|
|
19
20
|
export { createProvider, detectProvider } from './factory.js';
|
|
20
21
|
export { JsonlProvider } from './jsonl.js';
|
|
21
22
|
export { NeonProvider } from './neon.js';
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* const node = await store.getNode('elder-1');
|
|
16
16
|
* ```
|
|
17
17
|
*/
|
|
18
|
+
// Plan Perspective merge/filter semantics (shared across providers)
|
|
19
|
+
export { mergePlanPerspectiveRecords, unionPlanPerspectiveEpisodes, matchesPlanPerspectiveFilters, } from './plan-perspectives.js';
|
|
18
20
|
// Factory
|
|
19
21
|
export { createProvider, detectProvider } from './factory.js';
|
|
20
22
|
// Providers (for direct instantiation)
|
package/dist/interface.d.ts
CHANGED
|
@@ -13,6 +13,94 @@ export interface RelationalEdge extends Omit<OntologyRelationalEdge, 'id'> {
|
|
|
13
13
|
last_ceremony?: string;
|
|
14
14
|
}
|
|
15
15
|
export type CeremonyLog = OntologyCeremonyLog;
|
|
16
|
+
export type WeaveSyncState = 'never-synced' | 'in-sync' | 'stale' | 'episode-copy-diverged';
|
|
17
|
+
export interface WeaveRecord extends Record<string, unknown> {
|
|
18
|
+
id: string;
|
|
19
|
+
weave: 1;
|
|
20
|
+
artefact: {
|
|
21
|
+
id: string;
|
|
22
|
+
path?: string;
|
|
23
|
+
[key: string]: unknown;
|
|
24
|
+
};
|
|
25
|
+
issue: string;
|
|
26
|
+
issue_url?: string;
|
|
27
|
+
episode: {
|
|
28
|
+
path: string;
|
|
29
|
+
number: number;
|
|
30
|
+
[key: string]: unknown;
|
|
31
|
+
};
|
|
32
|
+
last_sync: {
|
|
33
|
+
state: WeaveSyncState;
|
|
34
|
+
at?: string;
|
|
35
|
+
tree_sha256?: string;
|
|
36
|
+
file_count?: number;
|
|
37
|
+
bytes_total?: number;
|
|
38
|
+
[key: string]: unknown;
|
|
39
|
+
};
|
|
40
|
+
source: {
|
|
41
|
+
package: string;
|
|
42
|
+
record_path?: string;
|
|
43
|
+
registered_at: string;
|
|
44
|
+
updated_at: string;
|
|
45
|
+
[key: string]: unknown;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export interface InquiryWeaveFilters {
|
|
49
|
+
episode_path?: string;
|
|
50
|
+
episode_number?: number;
|
|
51
|
+
issue?: string;
|
|
52
|
+
artefact?: string;
|
|
53
|
+
}
|
|
54
|
+
export interface PlanPerspectiveEpisode {
|
|
55
|
+
path: string;
|
|
56
|
+
number?: number;
|
|
57
|
+
[key: string]: unknown;
|
|
58
|
+
}
|
|
59
|
+
export interface PlanPerspectiveRecord extends Record<string, unknown> {
|
|
60
|
+
id: string;
|
|
61
|
+
perspective: 1;
|
|
62
|
+
plan: {
|
|
63
|
+
session_id: string;
|
|
64
|
+
plan_path?: string;
|
|
65
|
+
plan_filename: string;
|
|
66
|
+
plan_sha256: string;
|
|
67
|
+
captured_at?: string;
|
|
68
|
+
[key: string]: unknown;
|
|
69
|
+
};
|
|
70
|
+
narrative: {
|
|
71
|
+
title: string;
|
|
72
|
+
body_markdown: string;
|
|
73
|
+
mia_context?: string;
|
|
74
|
+
[key: string]: unknown;
|
|
75
|
+
};
|
|
76
|
+
lineage?: {
|
|
77
|
+
user_inputs_path?: string;
|
|
78
|
+
input_count?: number;
|
|
79
|
+
first_input_at?: string;
|
|
80
|
+
last_input_at?: string;
|
|
81
|
+
excerpts?: string[];
|
|
82
|
+
[key: string]: unknown;
|
|
83
|
+
};
|
|
84
|
+
episodes: PlanPerspectiveEpisode[];
|
|
85
|
+
source: {
|
|
86
|
+
package?: string;
|
|
87
|
+
generator?: {
|
|
88
|
+
system?: string;
|
|
89
|
+
agent?: string;
|
|
90
|
+
model?: string;
|
|
91
|
+
producer_session_id?: string;
|
|
92
|
+
[key: string]: unknown;
|
|
93
|
+
};
|
|
94
|
+
registered_at: string;
|
|
95
|
+
updated_at: string;
|
|
96
|
+
[key: string]: unknown;
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
export interface PlanPerspectiveFilters {
|
|
100
|
+
episode_path?: string;
|
|
101
|
+
session_id?: string;
|
|
102
|
+
id?: string;
|
|
103
|
+
}
|
|
16
104
|
export interface StorageProvider {
|
|
17
105
|
readonly name: string;
|
|
18
106
|
connect(): Promise<void>;
|
|
@@ -33,5 +121,11 @@ export interface StorageProvider {
|
|
|
33
121
|
getCeremoniesByDirection(direction: DirectionName): Promise<CeremonyLog[]>;
|
|
34
122
|
getCeremoniesByType(type: CeremonyType): Promise<CeremonyLog[]>;
|
|
35
123
|
getAllCeremonies(limit?: number): Promise<CeremonyLog[]>;
|
|
124
|
+
registerInquiryWeave(record: WeaveRecord): Promise<void>;
|
|
125
|
+
getInquiryWeave(id: string): Promise<WeaveRecord | null>;
|
|
126
|
+
listInquiryWeaves(filters?: InquiryWeaveFilters): Promise<WeaveRecord[]>;
|
|
127
|
+
registerPlanPerspective(record: PlanPerspectiveRecord): Promise<PlanPerspectiveRecord>;
|
|
128
|
+
getPlanPerspective(id: string): Promise<PlanPerspectiveRecord | null>;
|
|
129
|
+
listPlanPerspectives(filters?: PlanPerspectiveFilters): Promise<PlanPerspectiveRecord[]>;
|
|
36
130
|
}
|
|
37
131
|
export type ProviderType = 'jsonl' | 'neon' | 'redis' | 'auto';
|
package/dist/jsonl.d.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { DirectionName, NodeType, CeremonyType } from '@medicine-wheel/ontology-core';
|
|
2
|
-
import type { StorageProvider, RelationalNode, RelationalEdge, CeremonyLog } from './interface.js';
|
|
2
|
+
import type { StorageProvider, RelationalNode, RelationalEdge, CeremonyLog, WeaveRecord, InquiryWeaveFilters, PlanPerspectiveRecord, PlanPerspectiveFilters } from './interface.js';
|
|
3
3
|
export declare class JsonlProvider implements StorageProvider {
|
|
4
4
|
readonly name = "jsonl";
|
|
5
5
|
readonly dataDir: string;
|
|
6
6
|
private readonly nodesFile;
|
|
7
7
|
private readonly edgesFile;
|
|
8
8
|
private readonly ceremoniesFile;
|
|
9
|
+
private readonly inquiryWeavesFile;
|
|
10
|
+
private readonly planPerspectivesFile;
|
|
9
11
|
constructor(dataDir?: string);
|
|
10
12
|
connect(): Promise<void>;
|
|
11
13
|
disconnect(): Promise<void>;
|
|
@@ -25,9 +27,17 @@ export declare class JsonlProvider implements StorageProvider {
|
|
|
25
27
|
getCeremoniesByDirection(direction: DirectionName): Promise<CeremonyLog[]>;
|
|
26
28
|
getCeremoniesByType(type: CeremonyType): Promise<CeremonyLog[]>;
|
|
27
29
|
getAllCeremonies(limit?: number): Promise<CeremonyLog[]>;
|
|
30
|
+
registerInquiryWeave(record: WeaveRecord): Promise<void>;
|
|
31
|
+
getInquiryWeave(id: string): Promise<WeaveRecord | null>;
|
|
32
|
+
listInquiryWeaves(filters?: InquiryWeaveFilters): Promise<WeaveRecord[]>;
|
|
33
|
+
registerPlanPerspective(record: PlanPerspectiveRecord): Promise<PlanPerspectiveRecord>;
|
|
34
|
+
getPlanPerspective(id: string): Promise<PlanPerspectiveRecord | null>;
|
|
35
|
+
listPlanPerspectives(filters?: PlanPerspectiveFilters): Promise<PlanPerspectiveRecord[]>;
|
|
28
36
|
private readNodes;
|
|
29
37
|
private readEdges;
|
|
30
38
|
private readCeremonies;
|
|
39
|
+
private readInquiryWeaves;
|
|
40
|
+
private readPlanPerspectives;
|
|
31
41
|
private upsertById;
|
|
32
42
|
private upsertEdge;
|
|
33
43
|
private parseNode;
|
package/dist/jsonl.js
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { randomUUID } from 'crypto';
|
|
4
|
+
import { mergePlanPerspectiveRecords, matchesPlanPerspectiveFilters, } from './plan-perspectives.js';
|
|
4
5
|
export class JsonlProvider {
|
|
5
6
|
name = 'jsonl';
|
|
6
7
|
dataDir;
|
|
7
8
|
nodesFile;
|
|
8
9
|
edgesFile;
|
|
9
10
|
ceremoniesFile;
|
|
11
|
+
inquiryWeavesFile;
|
|
12
|
+
planPerspectivesFile;
|
|
10
13
|
constructor(dataDir) {
|
|
11
14
|
this.dataDir = path.resolve(dataDir ?? resolveProjectDataDir());
|
|
12
15
|
this.nodesFile = path.join(this.dataDir, 'nodes.jsonl');
|
|
13
16
|
this.edgesFile = path.join(this.dataDir, 'edges.jsonl');
|
|
14
17
|
this.ceremoniesFile = path.join(this.dataDir, 'ceremonies.jsonl');
|
|
18
|
+
this.inquiryWeavesFile = path.join(this.dataDir, 'inquiry-weaves.jsonl');
|
|
19
|
+
this.planPerspectivesFile = path.join(this.dataDir, 'plan-perspectives.jsonl');
|
|
15
20
|
}
|
|
16
21
|
async connect() {
|
|
17
22
|
ensureDirectory(this.dataDir);
|
|
@@ -118,6 +123,33 @@ export class JsonlProvider {
|
|
|
118
123
|
.slice(0, limit)
|
|
119
124
|
.map((ceremony) => this.parseCeremony(ceremony));
|
|
120
125
|
}
|
|
126
|
+
async registerInquiryWeave(record) {
|
|
127
|
+
await this.upsertById(this.inquiryWeavesFile, record);
|
|
128
|
+
}
|
|
129
|
+
async getInquiryWeave(id) {
|
|
130
|
+
return this.readInquiryWeaves().find((record) => record.id === id) ?? null;
|
|
131
|
+
}
|
|
132
|
+
async listInquiryWeaves(filters = {}) {
|
|
133
|
+
return this.readInquiryWeaves().filter((record) => matchesInquiryWeaveFilters(record, filters));
|
|
134
|
+
}
|
|
135
|
+
async registerPlanPerspective(record) {
|
|
136
|
+
let merged = record;
|
|
137
|
+
await withWriteLock(this.planPerspectivesFile, () => {
|
|
138
|
+
const records = readJsonl(this.planPerspectivesFile);
|
|
139
|
+
const existing = records.find((candidate) => candidate.id === record.id);
|
|
140
|
+
merged = existing ? mergePlanPerspectiveRecords(existing, record) : record;
|
|
141
|
+
const nextRecords = records.filter((candidate) => candidate.id !== record.id);
|
|
142
|
+
nextRecords.push(merged);
|
|
143
|
+
writeJsonl(this.planPerspectivesFile, nextRecords);
|
|
144
|
+
});
|
|
145
|
+
return merged;
|
|
146
|
+
}
|
|
147
|
+
async getPlanPerspective(id) {
|
|
148
|
+
return this.readPlanPerspectives().find((record) => record.id === id) ?? null;
|
|
149
|
+
}
|
|
150
|
+
async listPlanPerspectives(filters = {}) {
|
|
151
|
+
return this.readPlanPerspectives().filter((record) => matchesPlanPerspectiveFilters(record, filters));
|
|
152
|
+
}
|
|
121
153
|
readNodes() {
|
|
122
154
|
return readJsonl(this.nodesFile);
|
|
123
155
|
}
|
|
@@ -127,6 +159,12 @@ export class JsonlProvider {
|
|
|
127
159
|
readCeremonies() {
|
|
128
160
|
return readJsonl(this.ceremoniesFile);
|
|
129
161
|
}
|
|
162
|
+
readInquiryWeaves() {
|
|
163
|
+
return readJsonl(this.inquiryWeavesFile);
|
|
164
|
+
}
|
|
165
|
+
readPlanPerspectives() {
|
|
166
|
+
return readJsonl(this.planPerspectivesFile);
|
|
167
|
+
}
|
|
130
168
|
async upsertById(filePath, item) {
|
|
131
169
|
await withWriteLock(filePath, () => {
|
|
132
170
|
const nextRecords = readJsonl(filePath).filter((candidate) => candidate.id !== item.id);
|
|
@@ -254,6 +292,21 @@ function edgeKey(edge) {
|
|
|
254
292
|
function sortByNewest(field) {
|
|
255
293
|
return (left, right) => Date.parse(right[field]) - Date.parse(left[field]);
|
|
256
294
|
}
|
|
295
|
+
function matchesInquiryWeaveFilters(record, filters) {
|
|
296
|
+
if (filters.episode_path !== undefined && record.episode?.path !== filters.episode_path) {
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
if (filters.episode_number !== undefined && record.episode?.number !== filters.episode_number) {
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
if (filters.issue !== undefined && record.issue !== filters.issue) {
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
if (filters.artefact !== undefined && record.artefact?.id !== filters.artefact) {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
257
310
|
function isProcessAlive(pid) {
|
|
258
311
|
try {
|
|
259
312
|
// Signal 0 checks process existence without sending a real signal.
|
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 } from './interface.js';
|
|
5
|
+
import type { StorageProvider, RelationalNode, RelationalEdge, CeremonyLog, WeaveRecord, InquiryWeaveFilters, PlanPerspectiveRecord, PlanPerspectiveFilters } from './interface.js';
|
|
6
6
|
export declare class NeonProvider implements StorageProvider {
|
|
7
7
|
readonly name = "neon";
|
|
8
8
|
private sql;
|
|
@@ -28,4 +28,13 @@ export declare class NeonProvider implements StorageProvider {
|
|
|
28
28
|
getCeremoniesByType(type: CeremonyType): Promise<CeremonyLog[]>;
|
|
29
29
|
getAllCeremonies(limit?: number): Promise<CeremonyLog[]>;
|
|
30
30
|
private parseCeremony;
|
|
31
|
+
registerInquiryWeave(record: WeaveRecord): Promise<void>;
|
|
32
|
+
getInquiryWeave(id: string): Promise<WeaveRecord | null>;
|
|
33
|
+
listInquiryWeaves(filters?: InquiryWeaveFilters): Promise<WeaveRecord[]>;
|
|
34
|
+
private getAllInquiryWeaves;
|
|
35
|
+
private ensureInquiryWeavesTable;
|
|
36
|
+
registerPlanPerspective(record: PlanPerspectiveRecord): Promise<PlanPerspectiveRecord>;
|
|
37
|
+
getPlanPerspective(id: string): Promise<PlanPerspectiveRecord | null>;
|
|
38
|
+
listPlanPerspectives(filters?: PlanPerspectiveFilters): Promise<PlanPerspectiveRecord[]>;
|
|
39
|
+
private ensurePlanPerspectivesTable;
|
|
31
40
|
}
|
package/dist/neon.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Neon (Postgres) Storage Provider
|
|
3
3
|
*/
|
|
4
|
+
import { mergePlanPerspectiveRecords, matchesPlanPerspectiveFilters, } from './plan-perspectives.js';
|
|
4
5
|
export class NeonProvider {
|
|
5
6
|
name = 'neon';
|
|
6
7
|
sql = null;
|
|
@@ -10,6 +11,8 @@ export class NeonProvider {
|
|
|
10
11
|
throw new Error('DATABASE_URL, POSTGRES_URL, or NEON_DATABASE_URL must be set');
|
|
11
12
|
const { neon } = await import('@neondatabase/serverless');
|
|
12
13
|
this.sql = neon(url);
|
|
14
|
+
await this.ensureInquiryWeavesTable();
|
|
15
|
+
await this.ensurePlanPerspectivesTable();
|
|
13
16
|
}
|
|
14
17
|
async disconnect() {
|
|
15
18
|
this.sql = null;
|
|
@@ -174,6 +177,126 @@ export class NeonProvider {
|
|
|
174
177
|
research_context: row.research_context || undefined,
|
|
175
178
|
};
|
|
176
179
|
}
|
|
180
|
+
// ── Inquiry Weave Operations ──
|
|
181
|
+
async registerInquiryWeave(record) {
|
|
182
|
+
await this.db `
|
|
183
|
+
INSERT INTO inquiry_weaves (id, payload, episode_path, episode_number, issue, artefact_id, updated_at)
|
|
184
|
+
VALUES (
|
|
185
|
+
${record.id},
|
|
186
|
+
${JSON.stringify(record)},
|
|
187
|
+
${record.episode.path},
|
|
188
|
+
${record.episode.number},
|
|
189
|
+
${record.issue},
|
|
190
|
+
${record.artefact.id},
|
|
191
|
+
NOW()
|
|
192
|
+
)
|
|
193
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
194
|
+
payload = EXCLUDED.payload,
|
|
195
|
+
episode_path = EXCLUDED.episode_path,
|
|
196
|
+
episode_number = EXCLUDED.episode_number,
|
|
197
|
+
issue = EXCLUDED.issue,
|
|
198
|
+
artefact_id = EXCLUDED.artefact_id,
|
|
199
|
+
updated_at = NOW()
|
|
200
|
+
`;
|
|
201
|
+
}
|
|
202
|
+
async getInquiryWeave(id) {
|
|
203
|
+
const rows = await this.db `SELECT payload FROM inquiry_weaves WHERE id = ${id}`;
|
|
204
|
+
if (rows.length === 0)
|
|
205
|
+
return null;
|
|
206
|
+
return parseJsonValue(rows[0].payload, null);
|
|
207
|
+
}
|
|
208
|
+
async listInquiryWeaves(filters = {}) {
|
|
209
|
+
const records = await this.getAllInquiryWeaves();
|
|
210
|
+
return records.filter((record) => matchesInquiryWeaveFilters(record, filters));
|
|
211
|
+
}
|
|
212
|
+
async getAllInquiryWeaves() {
|
|
213
|
+
const rows = await this.db `SELECT payload FROM inquiry_weaves ORDER BY updated_at DESC`;
|
|
214
|
+
return rows.map((row) => parseJsonValue(row.payload, null));
|
|
215
|
+
}
|
|
216
|
+
async ensureInquiryWeavesTable() {
|
|
217
|
+
await this.db `
|
|
218
|
+
CREATE TABLE IF NOT EXISTS inquiry_weaves (
|
|
219
|
+
id TEXT PRIMARY KEY,
|
|
220
|
+
payload JSONB NOT NULL,
|
|
221
|
+
episode_path TEXT NOT NULL,
|
|
222
|
+
episode_number INTEGER NOT NULL,
|
|
223
|
+
issue TEXT NOT NULL,
|
|
224
|
+
artefact_id TEXT NOT NULL,
|
|
225
|
+
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
226
|
+
)
|
|
227
|
+
`;
|
|
228
|
+
await this.db `CREATE INDEX IF NOT EXISTS idx_inquiry_weaves_episode_path ON inquiry_weaves(episode_path)`;
|
|
229
|
+
await this.db `CREATE INDEX IF NOT EXISTS idx_inquiry_weaves_episode_number ON inquiry_weaves(episode_number)`;
|
|
230
|
+
await this.db `CREATE INDEX IF NOT EXISTS idx_inquiry_weaves_issue ON inquiry_weaves(issue)`;
|
|
231
|
+
await this.db `CREATE INDEX IF NOT EXISTS idx_inquiry_weaves_artefact_id ON inquiry_weaves(artefact_id)`;
|
|
232
|
+
}
|
|
233
|
+
// ── Plan Perspective Operations ──
|
|
234
|
+
async registerPlanPerspective(record) {
|
|
235
|
+
const existing = await this.getPlanPerspective(record.id);
|
|
236
|
+
const merged = existing ? mergePlanPerspectiveRecords(existing, record) : record;
|
|
237
|
+
const episodePaths = merged.episodes.map((episode) => episode.path);
|
|
238
|
+
await this.db `
|
|
239
|
+
INSERT INTO plan_perspectives (id, payload, session_id, episode_paths, updated_at)
|
|
240
|
+
VALUES (
|
|
241
|
+
${merged.id},
|
|
242
|
+
${JSON.stringify(merged)},
|
|
243
|
+
${merged.plan.session_id},
|
|
244
|
+
${episodePaths},
|
|
245
|
+
NOW()
|
|
246
|
+
)
|
|
247
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
248
|
+
payload = EXCLUDED.payload,
|
|
249
|
+
session_id = EXCLUDED.session_id,
|
|
250
|
+
episode_paths = EXCLUDED.episode_paths,
|
|
251
|
+
updated_at = NOW()
|
|
252
|
+
`;
|
|
253
|
+
return merged;
|
|
254
|
+
}
|
|
255
|
+
async getPlanPerspective(id) {
|
|
256
|
+
const rows = await this.db `SELECT payload FROM plan_perspectives WHERE id = ${id}`;
|
|
257
|
+
if (rows.length === 0)
|
|
258
|
+
return null;
|
|
259
|
+
return parseJsonValue(rows[0].payload, null);
|
|
260
|
+
}
|
|
261
|
+
async listPlanPerspectives(filters = {}) {
|
|
262
|
+
let rows;
|
|
263
|
+
if (filters.id !== undefined) {
|
|
264
|
+
rows = await this.db `SELECT payload FROM plan_perspectives WHERE id = ${filters.id}`;
|
|
265
|
+
}
|
|
266
|
+
else if (filters.session_id !== undefined) {
|
|
267
|
+
rows = await this.db `
|
|
268
|
+
SELECT payload FROM plan_perspectives
|
|
269
|
+
WHERE session_id = ${filters.session_id}
|
|
270
|
+
ORDER BY updated_at DESC
|
|
271
|
+
`;
|
|
272
|
+
}
|
|
273
|
+
else if (filters.episode_path !== undefined) {
|
|
274
|
+
rows = await this.db `
|
|
275
|
+
SELECT payload FROM plan_perspectives
|
|
276
|
+
WHERE episode_paths @> ARRAY[${filters.episode_path}]
|
|
277
|
+
ORDER BY updated_at DESC
|
|
278
|
+
`;
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
rows = await this.db `SELECT payload FROM plan_perspectives ORDER BY updated_at DESC`;
|
|
282
|
+
}
|
|
283
|
+
return rows
|
|
284
|
+
.map((row) => parseJsonValue(row.payload, null))
|
|
285
|
+
.filter((record) => record !== null && matchesPlanPerspectiveFilters(record, filters));
|
|
286
|
+
}
|
|
287
|
+
async ensurePlanPerspectivesTable() {
|
|
288
|
+
await this.db `
|
|
289
|
+
CREATE TABLE IF NOT EXISTS plan_perspectives (
|
|
290
|
+
id TEXT PRIMARY KEY,
|
|
291
|
+
payload JSONB NOT NULL,
|
|
292
|
+
session_id TEXT NOT NULL,
|
|
293
|
+
episode_paths TEXT[] NOT NULL DEFAULT '{}',
|
|
294
|
+
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
295
|
+
)
|
|
296
|
+
`;
|
|
297
|
+
await this.db `CREATE INDEX IF NOT EXISTS idx_plan_perspectives_session_id ON plan_perspectives(session_id)`;
|
|
298
|
+
await this.db `CREATE INDEX IF NOT EXISTS idx_plan_perspectives_episode_paths ON plan_perspectives USING GIN(episode_paths)`;
|
|
299
|
+
}
|
|
177
300
|
}
|
|
178
301
|
function getConnectionString() {
|
|
179
302
|
return process.env.DATABASE_URL ?? process.env.POSTGRES_URL ?? process.env.NEON_DATABASE_URL;
|
|
@@ -198,3 +321,18 @@ function parseJsonValue(value, fallback) {
|
|
|
198
321
|
}
|
|
199
322
|
return value;
|
|
200
323
|
}
|
|
324
|
+
function matchesInquiryWeaveFilters(record, filters) {
|
|
325
|
+
if (filters.episode_path !== undefined && record.episode?.path !== filters.episode_path) {
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
if (filters.episode_number !== undefined && record.episode?.number !== filters.episode_number) {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
if (filters.issue !== undefined && record.issue !== filters.issue) {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
if (filters.artefact !== undefined && record.artefact?.id !== filters.artefact) {
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
@@ -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.4.
|
|
3
|
+
"version": "0.4.13",
|
|
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.4.
|
|
19
|
+
"@medicine-wheel/ontology-core": "^0.4.13"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"typescript": "^5.7.0"
|