@medicine-wheel/storage-provider 0.5.0 → 0.5.2

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.
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Ceremony Event filter semantics shared by every provider.
3
+ *
4
+ * A ceremony event is a GitHub happening (issue, pull request, merge, commit)
5
+ * witnessed through a ceremonial lens and recorded as a relational bead. The
6
+ * detection and processing logic lives in @medicine-wheel/github-ceremony;
7
+ * here we keep only the record shape's filter matching so jsonl and neon agree.
8
+ */
9
+ import type { CeremonyEventFilters, CeremonyEventRecord } from './interface.js';
10
+ /** True when a ceremony-event record satisfies every provided filter. */
11
+ export declare function matchesCeremonyEventFilters(record: CeremonyEventRecord, filters: CeremonyEventFilters): boolean;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Ceremony Event filter semantics shared by every provider.
3
+ *
4
+ * A ceremony event is a GitHub happening (issue, pull request, merge, commit)
5
+ * witnessed through a ceremonial lens and recorded as a relational bead. The
6
+ * detection and processing logic lives in @medicine-wheel/github-ceremony;
7
+ * here we keep only the record shape's filter matching so jsonl and neon agree.
8
+ */
9
+ /** True when a ceremony-event record satisfies every provided filter. */
10
+ export function matchesCeremonyEventFilters(record, filters) {
11
+ if (filters.source !== undefined && record.source !== filters.source) {
12
+ return false;
13
+ }
14
+ if (filters.kind !== undefined && record.kind !== filters.kind) {
15
+ return false;
16
+ }
17
+ if (filters.phase !== undefined && record.phase !== filters.phase) {
18
+ return false;
19
+ }
20
+ if (filters.direction !== undefined && record.direction !== filters.direction) {
21
+ return false;
22
+ }
23
+ if (filters.repository !== undefined && record.repository !== filters.repository) {
24
+ return false;
25
+ }
26
+ return true;
27
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Diary Entry filter + ordering semantics shared by every provider.
3
+ *
4
+ * The ceremonial diary is a participant's voice — intention, observation,
5
+ * reflection — written across the Five-Phase ceremonial methodology. The
6
+ * record schema lives with the providers so both the jsonl and neon backends
7
+ * read and write the same shape; the diary domain package
8
+ * (@medicine-wheel/ceremonial-diary) owns creation helpers, pattern detection,
9
+ * and markdown export.
10
+ */
11
+ import type { DiaryEntryFilters, DiaryEntryRecord } from './interface.js';
12
+ /** True when a record satisfies every provided filter (limit is applied separately). */
13
+ export declare function matchesDiaryEntryFilters(record: DiaryEntryRecord, filters: DiaryEntryFilters): boolean;
14
+ /**
15
+ * Filter, order (oldest → newest, mirroring the ceremonial journey), and apply
16
+ * the optional limit. Providers call this after loading their raw records so
17
+ * jsonl and neon return identical result sets.
18
+ */
19
+ export declare function filterAndOrderDiaryEntries(records: readonly DiaryEntryRecord[], filters?: DiaryEntryFilters): DiaryEntryRecord[];
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Diary Entry filter + ordering semantics shared by every provider.
3
+ *
4
+ * The ceremonial diary is a participant's voice — intention, observation,
5
+ * reflection — written across the Five-Phase ceremonial methodology. The
6
+ * record schema lives with the providers so both the jsonl and neon backends
7
+ * read and write the same shape; the diary domain package
8
+ * (@medicine-wheel/ceremonial-diary) owns creation helpers, pattern detection,
9
+ * and markdown export.
10
+ */
11
+ /** True when a record satisfies every provided filter (limit is applied separately). */
12
+ export function matchesDiaryEntryFilters(record, filters) {
13
+ if (filters.participant !== undefined && record.participant !== filters.participant) {
14
+ return false;
15
+ }
16
+ if (filters.phase !== undefined && record.phase !== filters.phase) {
17
+ return false;
18
+ }
19
+ if (filters.entryType !== undefined && record.entryType !== filters.entryType) {
20
+ return false;
21
+ }
22
+ if (filters.dateRange !== undefined) {
23
+ const entryTime = Date.parse(record.timestamp);
24
+ const startTime = Date.parse(filters.dateRange.start);
25
+ const endTime = Date.parse(filters.dateRange.end);
26
+ if (entryTime < startTime || entryTime > endTime) {
27
+ return false;
28
+ }
29
+ }
30
+ if (filters.tags !== undefined && filters.tags.length > 0) {
31
+ const entryTags = record.metadata?.tags ?? [];
32
+ const hasTag = filters.tags.some((tag) => entryTags.includes(tag));
33
+ if (!hasTag) {
34
+ return false;
35
+ }
36
+ }
37
+ return true;
38
+ }
39
+ /**
40
+ * Filter, order (oldest → newest, mirroring the ceremonial journey), and apply
41
+ * the optional limit. Providers call this after loading their raw records so
42
+ * jsonl and neon return identical result sets.
43
+ */
44
+ export function filterAndOrderDiaryEntries(records, filters = {}) {
45
+ const filtered = records
46
+ .filter((record) => matchesDiaryEntryFilters(record, filters))
47
+ .sort((left, right) => Date.parse(left.timestamp) - Date.parse(right.timestamp));
48
+ if (typeof filters.limit === 'number' && filters.limit >= 0) {
49
+ return filtered.slice(0, filters.limit);
50
+ }
51
+ return filtered;
52
+ }
package/dist/index.d.ts CHANGED
@@ -15,9 +15,12 @@
15
15
  * const node = await store.getNode('elder-1');
16
16
  * ```
17
17
  */
18
- export type { StorageProvider, RelationalNode, RelationalEdge, CeremonyLog, WeaveRecord, WeaveSyncState, InquiryWeaveFilters, PlanPerspectiveRecord, PlanPerspectiveEpisode, PlanPerspectiveFilters, ProviderType, NodePatch, EdgePatch, } from './interface.js';
18
+ export type { StorageProvider, RelationalNode, RelationalEdge, CeremonyLog, WeaveRecord, WeaveSyncState, InquiryWeaveFilters, PlanPerspectiveRecord, PlanPerspectiveEpisode, PlanPerspectiveFilters, CeremonialPhase, DiaryEntryType, DiaryEntryLocation, DiaryEntryMetadata, DiaryEntryRecord, DiaryEntryFilters, CeremonyEventKind, CeremonyEventParticipant, CeremonyEventRecord, CeremonyEventFilters, ProviderType, NodePatch, EdgePatch, } from './interface.js';
19
19
  export { NodeNotFoundError, EdgeNotFoundError, NodeHasRelationsError, } from './interface.js';
20
20
  export { mergePlanPerspectiveRecords, unionPlanPerspectiveEpisodes, matchesPlanPerspectiveFilters, } from './plan-perspectives.js';
21
+ export { matchesInquiryWeaveFilters, filterInquiryWeaves, } from './inquiry-weaves.js';
22
+ export { matchesDiaryEntryFilters, filterAndOrderDiaryEntries, } from './diary-records.js';
23
+ export { matchesCeremonyEventFilters } from './ceremony-events.js';
21
24
  export { createProvider, detectProvider } from './factory.js';
22
25
  export { JsonlProvider } from './jsonl.js';
23
26
  export { NeonProvider } from './neon.js';
package/dist/index.js CHANGED
@@ -19,6 +19,12 @@
19
19
  export { NodeNotFoundError, EdgeNotFoundError, NodeHasRelationsError, } from './interface.js';
20
20
  // Plan Perspective merge/filter semantics (shared across providers)
21
21
  export { mergePlanPerspectiveRecords, unionPlanPerspectiveEpisodes, matchesPlanPerspectiveFilters, } from './plan-perspectives.js';
22
+ // Inquiry Weave filter semantics (shared across providers)
23
+ export { matchesInquiryWeaveFilters, filterInquiryWeaves, } from './inquiry-weaves.js';
24
+ // Ceremonial Diary filter/ordering semantics (shared across providers)
25
+ export { matchesDiaryEntryFilters, filterAndOrderDiaryEntries, } from './diary-records.js';
26
+ // Ceremony Event filter semantics (shared across providers)
27
+ export { matchesCeremonyEventFilters } from './ceremony-events.js';
22
28
  // Factory
23
29
  export { createProvider, detectProvider } from './factory.js';
24
30
  // Providers (for direct instantiation)
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Inquiry Weave filter semantics — shared across providers.
3
+ *
4
+ * Both the JSONL and Postgres providers answer the same questions about a
5
+ * weave; keeping the predicate in one place stops the two from drifting apart
6
+ * on what a filter means.
7
+ */
8
+ import type { InquiryWeaveFilters, WeaveRecord } from './interface.js';
9
+ export declare function matchesInquiryWeaveFilters(record: WeaveRecord, filters: InquiryWeaveFilters): boolean;
10
+ /**
11
+ * Filter a batch of weave rows, dropping anything that did not survive the read.
12
+ *
13
+ * A Postgres payload that fails to parse arrives here as null, and every
14
+ * sibling record collection drops those before filtering. Without the guard a
15
+ * single unreadable row throws while reading `record.episode`, and the whole
16
+ * listing answers with an error instead of the weaves that are intact.
17
+ */
18
+ export declare function filterInquiryWeaves(records: readonly (WeaveRecord | null | undefined)[], filters?: InquiryWeaveFilters): WeaveRecord[];
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Inquiry Weave filter semantics — shared across providers.
3
+ *
4
+ * Both the JSONL and Postgres providers answer the same questions about a
5
+ * weave; keeping the predicate in one place stops the two from drifting apart
6
+ * on what a filter means.
7
+ */
8
+ export function matchesInquiryWeaveFilters(record, filters) {
9
+ if (filters.episode_path !== undefined && record.episode?.path !== filters.episode_path) {
10
+ return false;
11
+ }
12
+ if (filters.episode_number !== undefined && record.episode?.number !== filters.episode_number) {
13
+ return false;
14
+ }
15
+ if (filters.issue !== undefined && record.issue !== filters.issue) {
16
+ return false;
17
+ }
18
+ if (filters.artefact !== undefined && record.artefact?.id !== filters.artefact) {
19
+ return false;
20
+ }
21
+ return true;
22
+ }
23
+ /**
24
+ * Filter a batch of weave rows, dropping anything that did not survive the read.
25
+ *
26
+ * A Postgres payload that fails to parse arrives here as null, and every
27
+ * sibling record collection drops those before filtering. Without the guard a
28
+ * single unreadable row throws while reading `record.episode`, and the whole
29
+ * listing answers with an error instead of the weaves that are intact.
30
+ */
31
+ export function filterInquiryWeaves(records, filters = {}) {
32
+ return records.filter((record) => record !== null &&
33
+ record !== undefined &&
34
+ typeof record === 'object' &&
35
+ matchesInquiryWeaveFilters(record, filters));
36
+ }
@@ -137,6 +137,87 @@ export interface PlanPerspectiveFilters {
137
137
  session_id?: string;
138
138
  id?: string;
139
139
  }
140
+ export type CeremonialPhase = 'miigwechiwendam' | 'nindokendaan' | 'ningwaab' | 'nindoodam' | 'migwech';
141
+ export type DiaryEntryType = 'intention' | 'observation' | 'hypothesis' | 'data' | 'synthesis' | 'action' | 'reflection' | 'learning';
142
+ export interface DiaryEntryLocation {
143
+ lat: number;
144
+ lon: number;
145
+ name?: string;
146
+ }
147
+ export interface DiaryEntryMetadata {
148
+ location?: DiaryEntryLocation;
149
+ activity?: 'walking' | 'sitting' | 'coding' | 'reflecting' | string;
150
+ weather?: string;
151
+ emotionalTone?: string;
152
+ /** Related stories, sessions, or other entry keys. */
153
+ relatedKeys?: string[];
154
+ tags?: string[];
155
+ [key: string]: unknown;
156
+ }
157
+ export interface DiaryEntryRecord extends Record<string, unknown> {
158
+ id: string;
159
+ /** ISO 8601 timestamp. */
160
+ timestamp: string;
161
+ /** Author identity — the participant whose voice this entry carries. */
162
+ participant: string;
163
+ /** Which agent facilitated this entry, when applicable. */
164
+ agent?: string;
165
+ phase: CeremonialPhase;
166
+ entryType: DiaryEntryType;
167
+ /** Main diary text; supports Markdown. */
168
+ content: string;
169
+ metadata: DiaryEntryMetadata;
170
+ /**
171
+ * Optional relation into the chronicle. Medicine-wheel node-id convention is
172
+ * `chronicle:<episode-folder-name>` (a chronicle_episode node under parent
173
+ * `chronicle:miadi-chronicle`). Entries without a chronicle reference remain
174
+ * fully valid — the diary can speak whether or not it writes into an episode.
175
+ */
176
+ chronicle?: string;
177
+ }
178
+ export interface DiaryEntryFilters {
179
+ participant?: string;
180
+ phase?: CeremonialPhase;
181
+ entryType?: DiaryEntryType;
182
+ dateRange?: {
183
+ start: string;
184
+ end: string;
185
+ };
186
+ tags?: string[];
187
+ limit?: number;
188
+ }
189
+ export type CeremonyEventKind = 'issue' | 'pr' | 'merge' | 'commit';
190
+ export interface CeremonyEventParticipant {
191
+ name: string;
192
+ role: string;
193
+ perspective: 'indigenous' | 'western' | 'both';
194
+ }
195
+ export interface CeremonyEventRecord extends Record<string, unknown> {
196
+ id: string;
197
+ /** Origin of the event — currently 'github'. */
198
+ source: string;
199
+ kind: CeremonyEventKind;
200
+ phase: CeremonialPhase;
201
+ /** Four-Directions bridge derived from the ceremonial phase. */
202
+ direction?: DirectionName;
203
+ participants: CeremonyEventParticipant[];
204
+ relationshipImpacts: string[];
205
+ /** Spiral key preserved from Miadi semantics (non-Redis, deterministic). */
206
+ spiralKey: string;
207
+ /** ISO 8601 timestamp. */
208
+ timestamp: string;
209
+ repository?: string;
210
+ /** Issue/PR number or short commit sha. */
211
+ reference?: string | number;
212
+ metadata?: Record<string, unknown>;
213
+ }
214
+ export interface CeremonyEventFilters {
215
+ source?: string;
216
+ kind?: CeremonyEventKind;
217
+ phase?: CeremonialPhase;
218
+ direction?: DirectionName;
219
+ repository?: string;
220
+ }
140
221
  export interface StorageProvider {
141
222
  readonly name: string;
142
223
  connect(): Promise<void>;
@@ -171,5 +252,12 @@ export interface StorageProvider {
171
252
  registerPlanPerspective(record: PlanPerspectiveRecord): Promise<PlanPerspectiveRecord>;
172
253
  getPlanPerspective(id: string): Promise<PlanPerspectiveRecord | null>;
173
254
  listPlanPerspectives(filters?: PlanPerspectiveFilters): Promise<PlanPerspectiveRecord[]>;
255
+ registerDiaryEntry(record: DiaryEntryRecord): Promise<DiaryEntryRecord>;
256
+ getDiaryEntry(id: string): Promise<DiaryEntryRecord | null>;
257
+ listDiaryEntries(filters?: DiaryEntryFilters): Promise<DiaryEntryRecord[]>;
258
+ deleteDiaryEntry(id: string): Promise<void>;
259
+ registerCeremonyEvent(record: CeremonyEventRecord): Promise<CeremonyEventRecord>;
260
+ getCeremonyEvent(id: string): Promise<CeremonyEventRecord | null>;
261
+ listCeremonyEvents(filters?: CeremonyEventFilters): Promise<CeremonyEventRecord[]>;
174
262
  }
175
263
  export type ProviderType = 'jsonl' | 'neon' | 'redis' | 'auto';
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, PlanPerspectiveRecord, PlanPerspectiveFilters, NodePatch, EdgePatch } from './interface.js';
2
+ import type { StorageProvider, RelationalNode, RelationalEdge, CeremonyLog, WeaveRecord, InquiryWeaveFilters, PlanPerspectiveRecord, PlanPerspectiveFilters, DiaryEntryRecord, DiaryEntryFilters, CeremonyEventRecord, CeremonyEventFilters, NodePatch, EdgePatch } from './interface.js';
3
3
  export declare class JsonlProvider implements StorageProvider {
4
4
  readonly name = "jsonl";
5
5
  readonly dataDir: string;
@@ -8,6 +8,8 @@ export declare class JsonlProvider implements StorageProvider {
8
8
  private readonly ceremoniesFile;
9
9
  private readonly inquiryWeavesFile;
10
10
  private readonly planPerspectivesFile;
11
+ private readonly diaryEntriesFile;
12
+ private readonly ceremonyEventsFile;
11
13
  constructor(dataDir?: string);
12
14
  connect(): Promise<void>;
13
15
  disconnect(): Promise<void>;
@@ -37,11 +39,20 @@ export declare class JsonlProvider implements StorageProvider {
37
39
  registerPlanPerspective(record: PlanPerspectiveRecord): Promise<PlanPerspectiveRecord>;
38
40
  getPlanPerspective(id: string): Promise<PlanPerspectiveRecord | null>;
39
41
  listPlanPerspectives(filters?: PlanPerspectiveFilters): Promise<PlanPerspectiveRecord[]>;
42
+ registerDiaryEntry(record: DiaryEntryRecord): Promise<DiaryEntryRecord>;
43
+ getDiaryEntry(id: string): Promise<DiaryEntryRecord | null>;
44
+ listDiaryEntries(filters?: DiaryEntryFilters): Promise<DiaryEntryRecord[]>;
45
+ deleteDiaryEntry(id: string): Promise<void>;
46
+ registerCeremonyEvent(record: CeremonyEventRecord): Promise<CeremonyEventRecord>;
47
+ getCeremonyEvent(id: string): Promise<CeremonyEventRecord | null>;
48
+ listCeremonyEvents(filters?: CeremonyEventFilters): Promise<CeremonyEventRecord[]>;
40
49
  private readNodes;
41
50
  private readEdges;
42
51
  private readCeremonies;
43
52
  private readInquiryWeaves;
44
53
  private readPlanPerspectives;
54
+ private readDiaryEntries;
55
+ private readCeremonyEvents;
45
56
  private upsertById;
46
57
  private upsertEdge;
47
58
  private parseNode;
package/dist/jsonl.js CHANGED
@@ -3,6 +3,9 @@ import * as path from 'path';
3
3
  import { randomUUID } from 'crypto';
4
4
  import { NodeNotFoundError, EdgeNotFoundError, NodeHasRelationsError, } from './interface.js';
5
5
  import { mergePlanPerspectiveRecords, matchesPlanPerspectiveFilters, } from './plan-perspectives.js';
6
+ import { filterInquiryWeaves } from './inquiry-weaves.js';
7
+ import { filterAndOrderDiaryEntries } from './diary-records.js';
8
+ import { matchesCeremonyEventFilters } from './ceremony-events.js';
6
9
  export class JsonlProvider {
7
10
  name = 'jsonl';
8
11
  dataDir;
@@ -11,6 +14,8 @@ export class JsonlProvider {
11
14
  ceremoniesFile;
12
15
  inquiryWeavesFile;
13
16
  planPerspectivesFile;
17
+ diaryEntriesFile;
18
+ ceremonyEventsFile;
14
19
  constructor(dataDir) {
15
20
  this.dataDir = path.resolve(dataDir ?? resolveProjectDataDir());
16
21
  this.nodesFile = path.join(this.dataDir, 'nodes.jsonl');
@@ -18,6 +23,8 @@ export class JsonlProvider {
18
23
  this.ceremoniesFile = path.join(this.dataDir, 'ceremonies.jsonl');
19
24
  this.inquiryWeavesFile = path.join(this.dataDir, 'inquiry-weaves.jsonl');
20
25
  this.planPerspectivesFile = path.join(this.dataDir, 'plan-perspectives.jsonl');
26
+ this.diaryEntriesFile = path.join(this.dataDir, 'diary-entries.jsonl');
27
+ this.ceremonyEventsFile = path.join(this.dataDir, 'ceremony-events.jsonl');
21
28
  }
22
29
  async connect() {
23
30
  ensureDirectory(this.dataDir);
@@ -199,7 +206,7 @@ export class JsonlProvider {
199
206
  return this.readInquiryWeaves().find((record) => record.id === id) ?? null;
200
207
  }
201
208
  async listInquiryWeaves(filters = {}) {
202
- return this.readInquiryWeaves().filter((record) => matchesInquiryWeaveFilters(record, filters));
209
+ return filterInquiryWeaves(this.readInquiryWeaves(), filters);
203
210
  }
204
211
  async registerPlanPerspective(record) {
205
212
  let merged = record;
@@ -219,6 +226,34 @@ export class JsonlProvider {
219
226
  async listPlanPerspectives(filters = {}) {
220
227
  return this.readPlanPerspectives().filter((record) => matchesPlanPerspectiveFilters(record, filters));
221
228
  }
229
+ async registerDiaryEntry(record) {
230
+ await this.upsertById(this.diaryEntriesFile, record);
231
+ return record;
232
+ }
233
+ async getDiaryEntry(id) {
234
+ return this.readDiaryEntries().find((record) => record.id === id) ?? null;
235
+ }
236
+ async listDiaryEntries(filters = {}) {
237
+ return filterAndOrderDiaryEntries(this.readDiaryEntries(), filters);
238
+ }
239
+ async deleteDiaryEntry(id) {
240
+ await withWriteLock(this.diaryEntriesFile, () => {
241
+ const remaining = this.readDiaryEntries().filter((record) => record.id !== id);
242
+ writeJsonl(this.diaryEntriesFile, remaining);
243
+ });
244
+ }
245
+ async registerCeremonyEvent(record) {
246
+ await this.upsertById(this.ceremonyEventsFile, record);
247
+ return record;
248
+ }
249
+ async getCeremonyEvent(id) {
250
+ return this.readCeremonyEvents().find((record) => record.id === id) ?? null;
251
+ }
252
+ async listCeremonyEvents(filters = {}) {
253
+ return this.readCeremonyEvents()
254
+ .filter((record) => matchesCeremonyEventFilters(record, filters))
255
+ .sort(sortByNewest('timestamp'));
256
+ }
222
257
  readNodes() {
223
258
  return readJsonl(this.nodesFile);
224
259
  }
@@ -234,6 +269,12 @@ export class JsonlProvider {
234
269
  readPlanPerspectives() {
235
270
  return readJsonl(this.planPerspectivesFile);
236
271
  }
272
+ readDiaryEntries() {
273
+ return readJsonl(this.diaryEntriesFile);
274
+ }
275
+ readCeremonyEvents() {
276
+ return readJsonl(this.ceremonyEventsFile);
277
+ }
237
278
  async upsertById(filePath, item) {
238
279
  await withWriteLock(filePath, () => {
239
280
  const nextRecords = readJsonl(filePath).filter((candidate) => candidate.id !== item.id);
@@ -362,21 +403,6 @@ function edgeKey(edge) {
362
403
  function sortByNewest(field) {
363
404
  return (left, right) => Date.parse(right[field]) - Date.parse(left[field]);
364
405
  }
365
- function matchesInquiryWeaveFilters(record, filters) {
366
- if (filters.episode_path !== undefined && record.episode?.path !== filters.episode_path) {
367
- return false;
368
- }
369
- if (filters.episode_number !== undefined && record.episode?.number !== filters.episode_number) {
370
- return false;
371
- }
372
- if (filters.issue !== undefined && record.issue !== filters.issue) {
373
- return false;
374
- }
375
- if (filters.artefact !== undefined && record.artefact?.id !== filters.artefact) {
376
- return false;
377
- }
378
- return true;
379
- }
380
406
  function isProcessAlive(pid) {
381
407
  try {
382
408
  // Signal 0 checks process existence without sending a real signal.
@@ -388,6 +414,13 @@ function isProcessAlive(pid) {
388
414
  }
389
415
  }
390
416
  function isLockStale(lockPath) {
417
+ let mtimeMs;
418
+ try {
419
+ mtimeMs = fs.statSync(lockPath).mtimeMs;
420
+ }
421
+ catch {
422
+ return false; // Lock already gone — nothing to reap.
423
+ }
391
424
  try {
392
425
  const content = fs.readFileSync(lockPath, 'utf-8').trim();
393
426
  const parsed = JSON.parse(content);
@@ -402,7 +435,11 @@ function isLockStale(lockPath) {
402
435
  return lockAgeMs > 30_000;
403
436
  }
404
437
  catch {
405
- return false;
438
+ // A lock file we cannot read names no owner: a writer that died between
439
+ // creating the file and stamping it, or a foreign lock format. Judging it
440
+ // permanently held would leave the entity unwritable forever, so fall back
441
+ // to its age — the same grace period a stamped lock gets.
442
+ return Date.now() - mtimeMs > 30_000;
406
443
  }
407
444
  }
408
445
  async function acquireWriteLock(lockPath) {
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, PlanPerspectiveRecord, PlanPerspectiveFilters, NodePatch, EdgePatch } from './interface.js';
5
+ import type { StorageProvider, RelationalNode, RelationalEdge, CeremonyLog, WeaveRecord, InquiryWeaveFilters, PlanPerspectiveRecord, PlanPerspectiveFilters, DiaryEntryRecord, DiaryEntryFilters, CeremonyEventRecord, CeremonyEventFilters, NodePatch, EdgePatch } from './interface.js';
6
6
  export declare class NeonProvider implements StorageProvider {
7
7
  readonly name = "neon";
8
8
  private sql;
@@ -41,4 +41,13 @@ export declare class NeonProvider implements StorageProvider {
41
41
  getPlanPerspective(id: string): Promise<PlanPerspectiveRecord | null>;
42
42
  listPlanPerspectives(filters?: PlanPerspectiveFilters): Promise<PlanPerspectiveRecord[]>;
43
43
  private ensurePlanPerspectivesTable;
44
+ registerDiaryEntry(record: DiaryEntryRecord): Promise<DiaryEntryRecord>;
45
+ getDiaryEntry(id: string): Promise<DiaryEntryRecord | null>;
46
+ listDiaryEntries(filters?: DiaryEntryFilters): Promise<DiaryEntryRecord[]>;
47
+ deleteDiaryEntry(id: string): Promise<void>;
48
+ private ensureDiaryEntriesTable;
49
+ registerCeremonyEvent(record: CeremonyEventRecord): Promise<CeremonyEventRecord>;
50
+ getCeremonyEvent(id: string): Promise<CeremonyEventRecord | null>;
51
+ listCeremonyEvents(filters?: CeremonyEventFilters): Promise<CeremonyEventRecord[]>;
52
+ private ensureCeremonyEventsTable;
44
53
  }
package/dist/neon.js CHANGED
@@ -3,6 +3,9 @@
3
3
  */
4
4
  import { NodeNotFoundError, EdgeNotFoundError, NodeHasRelationsError, } from './interface.js';
5
5
  import { mergePlanPerspectiveRecords, matchesPlanPerspectiveFilters, } from './plan-perspectives.js';
6
+ import { filterInquiryWeaves } from './inquiry-weaves.js';
7
+ import { filterAndOrderDiaryEntries } from './diary-records.js';
8
+ import { matchesCeremonyEventFilters } from './ceremony-events.js';
6
9
  export class NeonProvider {
7
10
  name = 'neon';
8
11
  sql = null;
@@ -14,6 +17,8 @@ export class NeonProvider {
14
17
  this.sql = neon(url);
15
18
  await this.ensureInquiryWeavesTable();
16
19
  await this.ensurePlanPerspectivesTable();
20
+ await this.ensureDiaryEntriesTable();
21
+ await this.ensureCeremonyEventsTable();
17
22
  }
18
23
  async disconnect() {
19
24
  this.sql = null;
@@ -279,8 +284,7 @@ export class NeonProvider {
279
284
  return parseJsonValue(rows[0].payload, null);
280
285
  }
281
286
  async listInquiryWeaves(filters = {}) {
282
- const records = await this.getAllInquiryWeaves();
283
- return records.filter((record) => matchesInquiryWeaveFilters(record, filters));
287
+ return filterInquiryWeaves(await this.getAllInquiryWeaves(), filters);
284
288
  }
285
289
  async getAllInquiryWeaves() {
286
290
  const rows = await this.db `SELECT payload FROM inquiry_weaves ORDER BY updated_at DESC`;
@@ -370,6 +374,121 @@ export class NeonProvider {
370
374
  await this.db `CREATE INDEX IF NOT EXISTS idx_plan_perspectives_session_id ON plan_perspectives(session_id)`;
371
375
  await this.db `CREATE INDEX IF NOT EXISTS idx_plan_perspectives_episode_paths ON plan_perspectives USING GIN(episode_paths)`;
372
376
  }
377
+ // ── Ceremonial Diary Operations ──
378
+ async registerDiaryEntry(record) {
379
+ await this.db `
380
+ INSERT INTO diary_entries (id, payload, participant, phase, entry_type, chronicle, timestamp, updated_at)
381
+ VALUES (
382
+ ${record.id},
383
+ ${JSON.stringify(record)},
384
+ ${record.participant},
385
+ ${record.phase},
386
+ ${record.entryType},
387
+ ${record.chronicle ?? null},
388
+ ${record.timestamp},
389
+ NOW()
390
+ )
391
+ ON CONFLICT (id) DO UPDATE SET
392
+ payload = EXCLUDED.payload,
393
+ participant = EXCLUDED.participant,
394
+ phase = EXCLUDED.phase,
395
+ entry_type = EXCLUDED.entry_type,
396
+ chronicle = EXCLUDED.chronicle,
397
+ timestamp = EXCLUDED.timestamp,
398
+ updated_at = NOW()
399
+ `;
400
+ return record;
401
+ }
402
+ async getDiaryEntry(id) {
403
+ const rows = await this.db `SELECT payload FROM diary_entries WHERE id = ${id}`;
404
+ if (rows.length === 0)
405
+ return null;
406
+ return parseJsonValue(rows[0].payload, null);
407
+ }
408
+ async listDiaryEntries(filters = {}) {
409
+ const rows = await this.db `SELECT payload FROM diary_entries`;
410
+ const records = rows
411
+ .map((row) => parseJsonValue(row.payload, null))
412
+ .filter((record) => record !== null);
413
+ return filterAndOrderDiaryEntries(records, filters);
414
+ }
415
+ async deleteDiaryEntry(id) {
416
+ await this.db `DELETE FROM diary_entries WHERE id = ${id}`;
417
+ }
418
+ async ensureDiaryEntriesTable() {
419
+ await this.db `
420
+ CREATE TABLE IF NOT EXISTS diary_entries (
421
+ id TEXT PRIMARY KEY,
422
+ payload JSONB NOT NULL,
423
+ participant TEXT NOT NULL,
424
+ phase TEXT NOT NULL,
425
+ entry_type TEXT NOT NULL,
426
+ chronicle TEXT,
427
+ timestamp TIMESTAMPTZ NOT NULL,
428
+ updated_at TIMESTAMPTZ DEFAULT NOW()
429
+ )
430
+ `;
431
+ await this.db `CREATE INDEX IF NOT EXISTS idx_diary_entries_participant ON diary_entries(participant)`;
432
+ await this.db `CREATE INDEX IF NOT EXISTS idx_diary_entries_phase ON diary_entries(phase)`;
433
+ await this.db `CREATE INDEX IF NOT EXISTS idx_diary_entries_chronicle ON diary_entries(chronicle)`;
434
+ }
435
+ // ── Ceremony Event Operations ──
436
+ async registerCeremonyEvent(record) {
437
+ await this.db `
438
+ INSERT INTO ceremony_events (id, payload, source, kind, phase, direction, repository, timestamp, updated_at)
439
+ VALUES (
440
+ ${record.id},
441
+ ${JSON.stringify(record)},
442
+ ${record.source},
443
+ ${record.kind},
444
+ ${record.phase},
445
+ ${record.direction ?? null},
446
+ ${record.repository ?? null},
447
+ ${record.timestamp},
448
+ NOW()
449
+ )
450
+ ON CONFLICT (id) DO UPDATE SET
451
+ payload = EXCLUDED.payload,
452
+ source = EXCLUDED.source,
453
+ kind = EXCLUDED.kind,
454
+ phase = EXCLUDED.phase,
455
+ direction = EXCLUDED.direction,
456
+ repository = EXCLUDED.repository,
457
+ timestamp = EXCLUDED.timestamp,
458
+ updated_at = NOW()
459
+ `;
460
+ return record;
461
+ }
462
+ async getCeremonyEvent(id) {
463
+ const rows = await this.db `SELECT payload FROM ceremony_events WHERE id = ${id}`;
464
+ if (rows.length === 0)
465
+ return null;
466
+ return parseJsonValue(rows[0].payload, null);
467
+ }
468
+ async listCeremonyEvents(filters = {}) {
469
+ const rows = await this.db `SELECT payload FROM ceremony_events ORDER BY timestamp DESC`;
470
+ return rows
471
+ .map((row) => parseJsonValue(row.payload, null))
472
+ .filter((record) => record !== null && matchesCeremonyEventFilters(record, filters));
473
+ }
474
+ async ensureCeremonyEventsTable() {
475
+ await this.db `
476
+ CREATE TABLE IF NOT EXISTS ceremony_events (
477
+ id TEXT PRIMARY KEY,
478
+ payload JSONB NOT NULL,
479
+ source TEXT NOT NULL,
480
+ kind TEXT NOT NULL,
481
+ phase TEXT NOT NULL,
482
+ direction TEXT,
483
+ repository TEXT,
484
+ timestamp TIMESTAMPTZ NOT NULL,
485
+ updated_at TIMESTAMPTZ DEFAULT NOW()
486
+ )
487
+ `;
488
+ await this.db `CREATE INDEX IF NOT EXISTS idx_ceremony_events_kind ON ceremony_events(kind)`;
489
+ await this.db `CREATE INDEX IF NOT EXISTS idx_ceremony_events_phase ON ceremony_events(phase)`;
490
+ await this.db `CREATE INDEX IF NOT EXISTS idx_ceremony_events_repository ON ceremony_events(repository)`;
491
+ }
373
492
  }
374
493
  function getConnectionString() {
375
494
  return process.env.DATABASE_URL ?? process.env.POSTGRES_URL ?? process.env.NEON_DATABASE_URL;
@@ -394,18 +513,3 @@ function parseJsonValue(value, fallback) {
394
513
  }
395
514
  return value;
396
515
  }
397
- function matchesInquiryWeaveFilters(record, filters) {
398
- if (filters.episode_path !== undefined && record.episode?.path !== filters.episode_path) {
399
- return false;
400
- }
401
- if (filters.episode_number !== undefined && record.episode?.number !== filters.episode_number) {
402
- return false;
403
- }
404
- if (filters.issue !== undefined && record.issue !== filters.issue) {
405
- return false;
406
- }
407
- if (filters.artefact !== undefined && record.artefact?.id !== filters.artefact) {
408
- return false;
409
- }
410
- return true;
411
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@medicine-wheel/storage-provider",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
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.5.0"
19
+ "@medicine-wheel/ontology-core": "^0.5.2"
20
20
  },
21
21
  "devDependencies": {
22
22
  "typescript": "^5.7.0"