@medicine-wheel/app 0.15.6 → 0.15.7

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/README.md CHANGED
@@ -348,7 +348,10 @@ HONCHO_WORKSPACE_ID=medicine-wheel # default
348
348
  With `HONCHO_URL` set, every stored beat, ceremony and diary entry projects into
349
349
  Honcho on write, in the background. `GET /api/health` reports `honcho.enabled`.
350
350
  A projection never delays the wheel's answer, and a Honcho that is down is a
351
- line on stderr, not an error to the writer. A running server holds its old
351
+ line on stderr, not an error to the writer. The record is not lost: its
352
+ reference waits in `honcho-pending.jsonl` beside the store and is sent again on
353
+ start, every five minutes, and after any projection that gets through —
354
+ `honcho.pending` counts what waits. A running server holds its old
352
355
  build and its old environment — restart it for either to take effect.
353
356
 
354
357
  ## License
@@ -162,7 +162,7 @@ export async function POST(request: Request) {
162
162
  await store.logCeremony(ceremony);
163
163
  // The river: the stored ceremony leaves for Honcho in the background when
164
164
  // HONCHO_URL is set. Never awaited.
165
- projectAfterWrite(() => projectCeremony(ceremony), `ceremony ${ceremony.id}`);
165
+ projectAfterWrite({ kind: "ceremony", id: ceremony.id }, () => projectCeremony(ceremony));
166
166
  return NextResponse.json({ success: true, ceremony, provider: detectProvider() }, { status: 201 });
167
167
  } catch (error: unknown) {
168
168
  const message = error instanceof Error ? error.message : String(error);
@@ -126,7 +126,7 @@ export async function POST(request: Request) {
126
126
  });
127
127
  // The river: the participant's voice leaves for Honcho in the background
128
128
  // when HONCHO_URL is set. Never awaited.
129
- projectAfterWrite(() => projectDiaryEntry(entry), `diary entry ${entry.id}`);
129
+ projectAfterWrite({ kind: "diary", id: entry.id }, () => projectDiaryEntry(entry));
130
130
  return NextResponse.json({ success: true, entry, provider: detectProvider() }, { status: 201 });
131
131
  } catch (error: unknown) {
132
132
  const message = error instanceof Error ? error.message : String(error);
@@ -28,7 +28,8 @@ export async function GET() {
28
28
  ceremonies,
29
29
  },
30
30
  // The river to Honcho: enabled when HONCHO_URL is set. A wheel that
31
- // reports enabled projects every stored beat, ceremony and diary entry.
31
+ // reports enabled projects every stored beat, ceremony and diary entry;
32
+ // `pending` counts those still waiting for Honcho to take them (#147).
32
33
  honcho: honchoProjectionStatus(),
33
34
  env: {
34
35
  MW_STORAGE_PROVIDER: process.env.MW_STORAGE_PROVIDER || 'not set',
@@ -0,0 +1,67 @@
1
+ import { NextResponse } from "next/server";
2
+ import {
3
+ holdsRecord,
4
+ honchoProjectionStatus,
5
+ isPendingRef,
6
+ listPending,
7
+ markPending,
8
+ PENDING_KINDS,
9
+ retryPending,
10
+ type PendingRef,
11
+ } from "@/lib/honcho-projection";
12
+
13
+ /**
14
+ * The river's pending ledger (#147): records the wheel holds and Honcho has
15
+ * not received yet.
16
+ *
17
+ * GET — what waits, oldest first, with the river's status.
18
+ * POST — put records on the ledger by reference: `{ kind, id }`, or
19
+ * `{ refs: [{ kind, id }, …] }`. For records lost before the ledger
20
+ * existed, whose ids the server log still names. Each must be a record
21
+ * the wheel holds. A retry pass starts at once and the answer does not
22
+ * wait for it; `{ refs: [] }` only starts the pass.
23
+ */
24
+
25
+ export async function GET() {
26
+ try {
27
+ return NextResponse.json({ honcho: honchoProjectionStatus(), pending: listPending() });
28
+ } catch (error: unknown) {
29
+ return NextResponse.json({ error: error instanceof Error ? error.message : String(error) }, { status: 500 });
30
+ }
31
+ }
32
+
33
+ export async function POST(request: Request) {
34
+ try {
35
+ const status = honchoProjectionStatus();
36
+ if (!status.enabled) {
37
+ return NextResponse.json(
38
+ { error: "The river is off (HONCHO_URL is unset); nothing would carry these records." },
39
+ { status: 409 },
40
+ );
41
+ }
42
+ const body = await request.json().catch(() => null);
43
+ const raw: unknown[] | null = Array.isArray(body?.refs) ? body.refs : isPendingRef(body) ? [body] : null;
44
+ if (!raw) {
45
+ return NextResponse.json(
46
+ { error: "Provide { kind, id } or { refs: [{ kind, id }, …] }", kinds: PENDING_KINDS },
47
+ { status: 400 },
48
+ );
49
+ }
50
+ const refs: PendingRef[] = [];
51
+ for (const r of raw) {
52
+ if (!isPendingRef(r)) {
53
+ return NextResponse.json({ error: `Not a record reference: ${JSON.stringify(r)}`, kinds: PENDING_KINDS }, { status: 400 });
54
+ }
55
+ if (!(await holdsRecord(r))) {
56
+ return NextResponse.json({ error: `The wheel holds no ${r.kind} ${r.id}` }, { status: 404 });
57
+ }
58
+ refs.push({ kind: r.kind, id: r.id });
59
+ }
60
+ let pending = listPending().length;
61
+ for (const ref of refs) pending = markPending(ref, "queued by hand");
62
+ void retryPending();
63
+ return NextResponse.json({ queued: refs.length, pending }, { status: 202 });
64
+ } catch (error: unknown) {
65
+ return NextResponse.json({ error: error instanceof Error ? error.message : String(error) }, { status: 500 });
66
+ }
67
+ }
@@ -37,7 +37,7 @@ export async function POST(request: Request) {
37
37
  });
38
38
  // The river: a stored beat leaves for Honcho in the background when
39
39
  // HONCHO_URL is set. Never awaited — the wheel answers on its own clock.
40
- projectAfterWrite(() => projectBeat(beat), `beat ${beat.id}`);
40
+ projectAfterWrite({ kind: "beat", id: beat.id }, () => projectBeat(beat));
41
41
  // Warnings ride on the created beat rather than replacing it, so clients
42
42
  // that read the beat back by id keep working while advisory findings stop
43
43
  // being computed-and-discarded.
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Runs once when the server starts (Next.js instrumentation hook).
3
+ *
4
+ * Starts the Honcho river's retry clock (#147): records Honcho could not
5
+ * receive while it was away wait on the pending ledger, and are sent again on
6
+ * start and every few minutes after. With `HONCHO_URL` unset this does nothing.
7
+ */
8
+ export async function register() {
9
+ if (process.env.NEXT_RUNTIME !== "nodejs") return;
10
+ const { startPendingRetries } = await import("@/lib/honcho-projection");
11
+ startPendingRetries();
12
+ }
@@ -11,16 +11,32 @@
11
11
  * A failure is reported once on stderr with the record's id, because a silent
12
12
  * memory is worse than a noisy one. `awaitProjections()` exists for tests and
13
13
  * for anything that must know the river has run dry before it exits.
14
+ *
15
+ * A record Honcho could not receive is not lost (#147). Until then a Honcho
16
+ * that was offline left a hole in its memory of every talking circle held
17
+ * while it was away — the wheel kept the turns, and nothing ever sent them.
18
+ * Now the record's reference, `{ kind, id }`, waits in a pending ledger beside
19
+ * the wheel's store (`honcho-pending.jsonl`, or `HONCHO_PENDING_FILE`), and
20
+ * {@link retryPending} sends it again: when the server starts, every few
21
+ * minutes, and right after any projection that gets through. Only the
22
+ * reference waits. The words stay in the wheel, which remains canonical and is
23
+ * read again when the record finally leaves.
14
24
  */
15
25
 
26
+ import * as fs from 'node:fs';
27
+ import * as path from 'node:path';
16
28
  import {
17
29
  createHonchoClient,
18
30
  honchoFromEnv,
19
31
  project,
32
+ projectBeat,
33
+ projectCeremony,
34
+ projectDiaryEntry,
20
35
  type HonchoClient,
21
36
  type HonchoConfig,
22
37
  type Projection,
23
38
  } from '@medicine-wheel/honcho';
39
+ import { resolveProjectDataDir } from '@/lib/jsonl-store';
24
40
 
25
41
  const inFlight = new Set<Promise<unknown>>();
26
42
 
@@ -32,41 +48,323 @@ function clientFor(cfg: HonchoConfig): HonchoClient {
32
48
  return cached.client;
33
49
  }
34
50
 
35
- /** What `/api/health` reports: whether the river runs, and where to. */
36
- export function honchoProjectionStatus(): { enabled: boolean; url?: string; workspace?: string } {
51
+ function track(run: Promise<unknown>): void {
52
+ inFlight.add(run);
53
+ run.finally(() => { inFlight.delete(run); }).catch(() => {});
54
+ }
55
+
56
+ // ── what can wait ───────────────────────────────────────────────────────────
57
+
58
+ export const PENDING_KINDS = ['beat', 'ceremony', 'diary'] as const;
59
+ export type PendingKind = (typeof PENDING_KINDS)[number];
60
+
61
+ /** A record the river owes Honcho, by reference. */
62
+ export interface PendingRef {
63
+ kind: PendingKind;
64
+ id: string;
65
+ }
66
+
67
+ export interface PendingEntry extends PendingRef {
68
+ /** When the record first failed to arrive. */
69
+ since: string;
70
+ /** The failure that put it here. */
71
+ error?: string;
72
+ }
73
+
74
+ const KIND_LABEL: Record<PendingKind, string> = { beat: 'beat', ceremony: 'ceremony', diary: 'diary entry' };
75
+
76
+ const labelOf = (ref: PendingRef) => `${KIND_LABEL[ref.kind]} ${ref.id}`;
77
+ const same = (a: PendingRef, b: PendingRef) => a.kind === b.kind && a.id === b.id;
78
+
79
+ export function isPendingRef(value: unknown): value is PendingRef {
80
+ const v = value as Partial<PendingRef> | null;
81
+ return !!v && typeof v === 'object'
82
+ && typeof v.kind === 'string' && (PENDING_KINDS as readonly string[]).includes(v.kind)
83
+ && typeof v.id === 'string' && v.id.length > 0;
84
+ }
85
+
86
+ // ── the pending ledger ──────────────────────────────────────────────────────
87
+ //
88
+ // Every read and write below is synchronous on purpose. Next compiles
89
+ // `instrumentation.ts` and the routes into separate bundles, each with its own
90
+ // copy of this module; the file is what they share. A synchronous
91
+ // read-modify-write cannot interleave with another inside one Node process, so
92
+ // a record queued by a route while the timer's pass is sending another is
93
+ // never overwritten away.
94
+
95
+ /** Where the ledger lives: `HONCHO_PENDING_FILE`, else beside the wheel's JSONL store. */
96
+ export function pendingFile(): string {
97
+ return process.env.HONCHO_PENDING_FILE || path.join(resolveProjectDataDir(), 'honcho-pending.jsonl');
98
+ }
99
+
100
+ function readLedger(): PendingEntry[] {
101
+ let text: string;
102
+ try {
103
+ text = fs.readFileSync(pendingFile(), 'utf-8');
104
+ } catch (error) {
105
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return [];
106
+ throw error;
107
+ }
108
+ const entries: PendingEntry[] = [];
109
+ for (const line of text.split('\n')) {
110
+ if (!line.trim()) continue;
111
+ try {
112
+ const entry = JSON.parse(line);
113
+ if (isPendingRef(entry) && !entries.some((e) => same(e, entry))) entries.push(entry as PendingEntry);
114
+ } catch {
115
+ // A torn line is skipped, not fatal: the rest of the ledger still sends.
116
+ }
117
+ }
118
+ return entries;
119
+ }
120
+
121
+ /** An empty ledger is no file, so a data repository shows it only while something waits. */
122
+ function writeLedger(entries: PendingEntry[]): void {
123
+ const file = pendingFile();
124
+ if (!entries.length) {
125
+ fs.rmSync(file, { force: true });
126
+ return;
127
+ }
128
+ fs.mkdirSync(path.dirname(file), { recursive: true });
129
+ const tmp = `${file}.${process.pid}.tmp`;
130
+ fs.writeFileSync(tmp, entries.map((e) => JSON.stringify(e)).join('\n') + '\n');
131
+ fs.renameSync(tmp, file);
132
+ }
133
+
134
+ /** What waits, oldest first. */
135
+ export function listPending(): PendingEntry[] {
136
+ return readLedger();
137
+ }
138
+
139
+ /** Put a record on the ledger (once — a reference already waiting keeps its place). Returns how many wait. */
140
+ export function markPending(ref: PendingRef, error?: string): number {
141
+ const entries = readLedger();
142
+ if (!entries.some((e) => same(e, ref))) {
143
+ entries.push({ kind: ref.kind, id: ref.id, since: new Date().toISOString(), ...(error ? { error } : {}) });
144
+ writeLedger(entries);
145
+ }
146
+ return entries.length;
147
+ }
148
+
149
+ function clearPending(ref: PendingRef): void {
150
+ const entries = readLedger();
151
+ const rest = entries.filter((e) => !same(e, ref));
152
+ if (rest.length !== entries.length) writeLedger(rest);
153
+ }
154
+
155
+ // ── reading a record back ───────────────────────────────────────────────────
156
+
157
+ /**
158
+ * The record a reference names, shaped for Honcho — read from the wheel now,
159
+ * so what leaves is what the wheel holds, including any witness added since.
160
+ * `null` when the wheel no longer holds it. Throws when the store cannot be
161
+ * read, which is a reason to wait, not to forget.
162
+ */
163
+ async function shaperFor(ref: PendingRef): Promise<(() => Projection) | null> {
164
+ if (ref.kind === 'beat') {
165
+ const { getBeat } = await import('@/lib/store');
166
+ const beat = getBeat(ref.id);
167
+ return beat ? () => projectBeat(beat) : null;
168
+ }
169
+ const { createProvider } = await import('@medicine-wheel/storage-provider');
170
+ const store = await createProvider();
171
+ if (ref.kind === 'ceremony') {
172
+ const ceremony = await store.getCeremony(ref.id);
173
+ return ceremony ? () => projectCeremony(ceremony) : null;
174
+ }
175
+ const entry = await store.getDiaryEntry(ref.id);
176
+ return entry ? () => projectDiaryEntry(entry) : null;
177
+ }
178
+
179
+ /** Whether the wheel holds the record a reference names. */
180
+ export async function holdsRecord(ref: PendingRef): Promise<boolean> {
181
+ return (await shaperFor(ref)) !== null;
182
+ }
183
+
184
+ // ── failures ────────────────────────────────────────────────────────────────
185
+
186
+ /**
187
+ * Whether sending again can change the outcome: Honcho unreachable or timed
188
+ * out (the client reports 502), failing on its own side (5xx), or asking us
189
+ * to slow down. A 4xx refusal is Honcho's answer to this record as shaped;
190
+ * it would give the same answer every five minutes, forever.
191
+ */
192
+ function mayArriveLater(error: unknown): boolean {
193
+ const status = (error as { status?: unknown })?.status;
194
+ return typeof status !== 'number' || status >= 500 || status === 408 || status === 429;
195
+ }
196
+
197
+ function report(cfg: HonchoConfig, label: string, error: unknown, tail = ''): void {
198
+ const err = error as { message?: string; body?: string };
199
+ console.error(
200
+ `[honcho] projection of ${label} into ${cfg.workspace}@${cfg.baseUrl} failed: ${err?.message ?? String(error)}` +
201
+ (err?.body ? ` — ${err.body}` : '') +
202
+ tail,
203
+ );
204
+ }
205
+
206
+ /** What `/api/health` reports: whether the river runs, where to, and how much waits for it. */
207
+ export function honchoProjectionStatus(): { enabled: boolean; url?: string; workspace?: string; pending?: number } {
37
208
  const cfg = honchoFromEnv();
38
- return cfg ? { enabled: true, url: cfg.baseUrl, workspace: cfg.workspace } : { enabled: false };
209
+ if (!cfg) return { enabled: false };
210
+ let pending: number | undefined;
211
+ try {
212
+ pending = readLedger().length;
213
+ } catch {
214
+ pending = undefined;
215
+ }
216
+ return { enabled: true, url: cfg.baseUrl, workspace: cfg.workspace, ...(pending !== undefined ? { pending } : {}) };
39
217
  }
40
218
 
219
+ // ── the river ───────────────────────────────────────────────────────────────
220
+
41
221
  /**
42
222
  * Fire-and-forget. Returns immediately; the projection runs in the background.
43
- * `label` names the record in the one line written when Honcho refuses or is
44
- * unreachable.
223
+ * `ref` names the record — in the one line written when Honcho refuses or is
224
+ * unreachable, and on the ledger when it may arrive later.
45
225
  *
46
226
  * The projection is built by `shape`, called *inside* the error boundary. It
47
227
  * used to be built by the caller, in the route's own expression — so a record
48
228
  * the projection could not shape (a `learnings` that arrived as a string, say)
49
229
  * threw synchronously and turned an already-stored write into a 500 for the
50
230
  * writer. The wheel's answer to its caller must not depend on the river, and
51
- * that includes the part that reads the record.
231
+ * that includes the part that reads the record. A record that cannot be
232
+ * shaped is reported and not queued: shaping it again fails the same way.
52
233
  */
53
- export function projectAfterWrite(shape: () => Projection, label: string): void {
234
+ export function projectAfterWrite(ref: PendingRef, shape: () => Projection): void {
54
235
  const cfg = honchoFromEnv();
55
236
  if (!cfg) return;
56
- const run = Promise.resolve()
57
- .then(() => project(clientFor(cfg), shape()))
58
- .catch((error: unknown) => {
59
- const err = error as { message?: string; status?: number; body?: string };
60
- console.error(
61
- `[honcho] projection of ${label} into ${cfg.workspace}@${cfg.baseUrl} failed: ${err?.message ?? String(error)}` +
62
- (err?.body ? ` — ${err.body}` : ''),
63
- );
64
- })
65
- .finally(() => { inFlight.delete(run); });
66
- inFlight.add(run);
237
+ const label = labelOf(ref);
238
+ track(
239
+ Promise.resolve().then(async () => {
240
+ let projection: Projection;
241
+ try {
242
+ projection = shape();
243
+ } catch (error) {
244
+ report(cfg, label, error);
245
+ return;
246
+ }
247
+ try {
248
+ await project(clientFor(cfg), projection);
249
+ } catch (error) {
250
+ if (!mayArriveLater(error)) {
251
+ report(cfg, label, error);
252
+ return;
253
+ }
254
+ let tail: string;
255
+ try {
256
+ tail = ` — waiting to be sent again (${markPending(ref, (error as Error)?.message)} pending)`;
257
+ } catch (ledgerError) {
258
+ tail = ` — and could not be kept for later: ${(ledgerError as Error)?.message ?? String(ledgerError)}`;
259
+ }
260
+ report(cfg, label, error, tail);
261
+ return;
262
+ }
263
+ // Honcho answered, so whatever waited behind it can go now.
264
+ try {
265
+ if (readLedger().length) void retryPending();
266
+ } catch {
267
+ // The ledger is the timer's to reach; this was only a head start.
268
+ }
269
+ }),
270
+ );
271
+ }
272
+
273
+ export interface RetryResult {
274
+ /** Delivered on this pass and cleared from the ledger. */
275
+ sent: number;
276
+ /** Cleared without delivery: the wheel no longer holds it, or Honcho refused it. */
277
+ dropped: number;
278
+ /** Still waiting after this pass. */
279
+ pending: number;
280
+ }
281
+
282
+ // One pass at a time across every copy of this module (see the ledger note):
283
+ // two passes over the same ledger would send the same record twice.
284
+ const RETRY_STATE = Symbol.for('@medicine-wheel/app.honcho-retry');
285
+ type RetryState = { running?: Promise<RetryResult>; timer?: ReturnType<typeof setInterval> };
286
+ const retryState: RetryState = ((globalThis as Record<symbol, RetryState>)[RETRY_STATE] ??= {});
287
+
288
+ async function drain(): Promise<RetryResult> {
289
+ const result: RetryResult = { sent: 0, dropped: 0, pending: 0 };
290
+ const cfg = honchoFromEnv();
291
+ try {
292
+ if (cfg) {
293
+ for (const entry of readLedger()) {
294
+ const label = labelOf(entry);
295
+ // A store that cannot be read ends the pass with everything still waiting.
296
+ const shape = await shaperFor(entry);
297
+ if (!shape) {
298
+ console.error(`[honcho] ${label} was waiting for Honcho and the wheel no longer holds it; dropped from the ledger`);
299
+ clearPending(entry);
300
+ result.dropped++;
301
+ continue;
302
+ }
303
+ let projection: Projection;
304
+ try {
305
+ projection = shape();
306
+ } catch (error) {
307
+ report(cfg, label, error, ' — dropped from the ledger');
308
+ clearPending(entry);
309
+ result.dropped++;
310
+ continue;
311
+ }
312
+ try {
313
+ await project(clientFor(cfg), projection);
314
+ } catch (error) {
315
+ // Still away: this record and every one behind it keep waiting,
316
+ // and a Honcho that is down is not asked once per record.
317
+ if (mayArriveLater(error)) break;
318
+ report(cfg, label, error, ' — dropped from the ledger');
319
+ clearPending(entry);
320
+ result.dropped++;
321
+ continue;
322
+ }
323
+ clearPending(entry);
324
+ result.sent++;
325
+ }
326
+ }
327
+ result.pending = readLedger().length;
328
+ } catch (error) {
329
+ console.error(`[honcho] retrying the pending ledger (${pendingFile()}) failed: ${(error as Error)?.message ?? String(error)}`);
330
+ }
331
+ if (result.sent) {
332
+ console.log(`[honcho] ${result.sent} waiting projection(s) delivered; ${result.pending} still pending`);
333
+ }
334
+ return result;
335
+ }
336
+
337
+ /**
338
+ * Send what waits, oldest first, and clear each record Honcho takes. Stops at
339
+ * the first sign Honcho is still away. Never rejects. A pass already running
340
+ * is joined, not repeated.
341
+ */
342
+ export function retryPending(): Promise<RetryResult> {
343
+ if (retryState.running) return retryState.running;
344
+ const run = drain().finally(() => {
345
+ if (retryState.running === run) retryState.running = undefined;
346
+ });
347
+ retryState.running = run;
348
+ track(run);
349
+ return run;
350
+ }
351
+
352
+ /**
353
+ * Start the retry clock: one pass now, then one every `intervalMs`
354
+ * (`HONCHO_RETRY_INTERVAL_MS`, default five minutes). Called once from
355
+ * `instrumentation.ts` when the server starts; a second call is a no-op, and
356
+ * with `HONCHO_URL` unset there is no river to wait for.
357
+ */
358
+ export function startPendingRetries(
359
+ intervalMs = Number(process.env.HONCHO_RETRY_INTERVAL_MS) || 5 * 60_000,
360
+ ): void {
361
+ if (retryState.timer || !honchoFromEnv()) return;
362
+ retryState.timer = setInterval(() => { void retryPending(); }, intervalMs);
363
+ retryState.timer.unref?.();
364
+ void retryPending();
67
365
  }
68
366
 
69
- /** Resolves once every projection started so far has settled. */
367
+ /** Resolves once every projection and retry pass started so far has settled. */
70
368
  export async function awaitProjections(): Promise<void> {
71
369
  while (inFlight.size) await Promise.allSettled([...inFlight]);
72
370
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@medicine-wheel/app",
3
- "version": "0.15.6",
3
+ "version": "0.15.7",
4
4
  "description": "Medicine Wheel — Interactive visual layer for Indigenous relational research with Four Directions, ceremonies, and narrative arcs",
5
5
  "bin": {
6
6
  "mw": "dist/cli/mw.js",
@@ -14,6 +14,7 @@
14
14
  "public/**/*",
15
15
  "dist/cli/**/*",
16
16
  "next.config.mjs",
17
+ "instrumentation.ts",
17
18
  "next-env.d.ts",
18
19
  "postcss.config.mjs",
19
20
  "tsconfig.json",
@@ -89,30 +90,30 @@
89
90
  "release:major": "npm run version:major && npm run publish:all && npm run release:commit"
90
91
  },
91
92
  "dependencies": {
92
- "@medicine-wheel/ceremonial-diary": "^0.15.6",
93
- "@medicine-wheel/ceremony-protocol": "^0.15.6",
94
- "@medicine-wheel/community-review": "^0.15.6",
95
- "@medicine-wheel/consent-lifecycle": "^0.15.6",
96
- "@medicine-wheel/creative-orientation": "^0.15.6",
97
- "@medicine-wheel/data-store": "^0.15.6",
98
- "@medicine-wheel/data-store-postgres": "^0.15.6",
99
- "@medicine-wheel/fire-keeper": "^0.15.6",
100
- "@medicine-wheel/github-ceremony": "^0.15.6",
101
- "@medicine-wheel/graph-viz": "^0.15.6",
102
- "@medicine-wheel/honcho": "^0.15.6",
103
- "@medicine-wheel/importance-unit": "^0.15.6",
104
- "@medicine-wheel/mcp": "^4.15.6",
105
- "@medicine-wheel/narrative-cluster": "^0.15.6",
106
- "@medicine-wheel/narrative-engine": "^0.15.6",
107
- "@medicine-wheel/ontology-core": "^0.15.6",
108
- "@medicine-wheel/perception-layer": "^0.15.6",
109
- "@medicine-wheel/prompt-decomposition": "^0.15.6",
110
- "@medicine-wheel/relational-index": "^0.15.6",
111
- "@medicine-wheel/relational-query": "^0.15.6",
112
- "@medicine-wheel/session-reader": "^0.15.6",
113
- "@medicine-wheel/storage-provider": "^0.15.6",
114
- "@medicine-wheel/transformation-tracker": "^0.15.6",
115
- "@medicine-wheel/ui-components": "^0.15.6",
93
+ "@medicine-wheel/ceremonial-diary": "^0.15.7",
94
+ "@medicine-wheel/ceremony-protocol": "^0.15.7",
95
+ "@medicine-wheel/community-review": "^0.15.7",
96
+ "@medicine-wheel/consent-lifecycle": "^0.15.7",
97
+ "@medicine-wheel/creative-orientation": "^0.15.7",
98
+ "@medicine-wheel/data-store": "^0.15.7",
99
+ "@medicine-wheel/data-store-postgres": "^0.15.7",
100
+ "@medicine-wheel/fire-keeper": "^0.15.7",
101
+ "@medicine-wheel/github-ceremony": "^0.15.7",
102
+ "@medicine-wheel/graph-viz": "^0.15.7",
103
+ "@medicine-wheel/honcho": "^0.15.7",
104
+ "@medicine-wheel/importance-unit": "^0.15.7",
105
+ "@medicine-wheel/mcp": "^4.15.7",
106
+ "@medicine-wheel/narrative-cluster": "^0.15.7",
107
+ "@medicine-wheel/narrative-engine": "^0.15.7",
108
+ "@medicine-wheel/ontology-core": "^0.15.7",
109
+ "@medicine-wheel/perception-layer": "^0.15.7",
110
+ "@medicine-wheel/prompt-decomposition": "^0.15.7",
111
+ "@medicine-wheel/relational-index": "^0.15.7",
112
+ "@medicine-wheel/relational-query": "^0.15.7",
113
+ "@medicine-wheel/session-reader": "^0.15.7",
114
+ "@medicine-wheel/storage-provider": "^0.15.7",
115
+ "@medicine-wheel/transformation-tracker": "^0.15.7",
116
+ "@medicine-wheel/ui-components": "^0.15.7",
116
117
  "@neondatabase/serverless": "^0.10.0",
117
118
  "@xyflow/react": "^12.3.0",
118
119
  "clsx": "^2.1.1",