@medicine-wheel/app 0.13.0 → 0.14.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.
@@ -1,7 +1,10 @@
1
1
  import { NextResponse } from "next/server";
2
2
  import { parseLimit } from "@/lib/api-paging";
3
3
  import { createProvider, detectProvider } from "@medicine-wheel/storage-provider";
4
- import { ceremonyBelongsToEpisode } from "@/lib/ceremony-response";
4
+ import { ceremonyBelongsToEpisode, ceremonyEpisodePath } from "@/lib/ceremony-response";
5
+
6
+ /** Episode directory names are `YYYY-MM-DD-episode-NNN-slug`; nothing else may bind. */
7
+ const EPISODE_PATH = /^\d{4}-\d{2}-\d{2}-episode-\d{3,}-[a-z0-9-]+$/;
5
8
 
6
9
  export async function GET(request: Request) {
7
10
  try {
@@ -9,6 +12,8 @@ export async function GET(request: Request) {
9
12
  const direction = searchParams.get("direction");
10
13
  const type = searchParams.get("type");
11
14
  const episodePath = searchParams.get("episode_path");
15
+ const circleId = searchParams.get("circle_id");
16
+ const closes = searchParams.get("closes");
12
17
 
13
18
  const limit = parseLimit(searchParams.get("limit"));
14
19
  if (limit instanceof NextResponse) return limit;
@@ -41,6 +46,15 @@ export async function GET(request: Request) {
41
46
  );
42
47
  }
43
48
 
49
+ if (circleId) {
50
+ ceremonies = ceremonies.filter((c) => c.circle_id === circleId);
51
+ }
52
+
53
+ // `?closes=<opening id>` answers "is this ceremony closed, and by which record".
54
+ if (closes) {
55
+ ceremonies = ceremonies.filter((c) => c.closes === closes);
56
+ }
57
+
44
58
  const matched = ceremonies.length;
45
59
  if (limit !== null && ceremonies.length > limit) {
46
60
  ceremonies = ceremonies.slice(0, limit);
@@ -53,7 +67,7 @@ export async function GET(request: Request) {
53
67
  // `total` is the whole store, `matched` what the filters selected. When
54
68
  // count < matched the caller holds a page and can now see that it does.
55
69
  total,
56
- ...(direction || type || episodePath ? { matched } : {}),
70
+ ...(direction || type || episodePath || circleId || closes ? { matched } : {}),
57
71
  truncated: ceremonies.length < matched,
58
72
  });
59
73
  } catch (error: unknown) {
@@ -66,7 +80,47 @@ export async function POST(request: Request) {
66
80
  try {
67
81
  const store = await createProvider();
68
82
  const body = await request.json();
69
-
83
+
84
+ // Typed episode binding (0.14.0). A caller may still pass the legacy JSON
85
+ // string in `research_context`; it is lifted into the typed fields so every
86
+ // reader sees one shape. When both are given the typed fields win.
87
+ const legacy = ceremonyEpisodePath(body);
88
+ const episodePath: string | undefined =
89
+ typeof body.episode_path === "string" && body.episode_path ? body.episode_path : legacy;
90
+ if (episodePath !== undefined && !EPISODE_PATH.test(episodePath)) {
91
+ return NextResponse.json(
92
+ { error: `Invalid episode_path: ${episodePath}`, hint: "YYYY-MM-DD-episode-NNN-slug" },
93
+ { status: 400 },
94
+ );
95
+ }
96
+ const fromName = episodePath ? Number(episodePath.match(/-episode-(\d+)-/)?.[1]) : NaN;
97
+ const episodeNumber: number | undefined =
98
+ typeof body.episode_number === "number" ? body.episode_number : Number.isFinite(fromName) ? fromName : undefined;
99
+
100
+ if (body.closes !== undefined) {
101
+ if (typeof body.closes !== "string" || !body.closes) {
102
+ return NextResponse.json({ error: "closes must be a ceremony id" }, { status: 400 });
103
+ }
104
+ if (!(await store.getCeremony(body.closes))) {
105
+ return NextResponse.json(
106
+ { error: `Cannot close a ceremony that does not exist: ${body.closes}` },
107
+ { status: 404 },
108
+ );
109
+ }
110
+ }
111
+ if (body.circle_id !== undefined) {
112
+ if (typeof body.circle_id !== "string" || !body.circle_id) {
113
+ return NextResponse.json({ error: "circle_id must be a node id" }, { status: 400 });
114
+ }
115
+ const circle = await store.getNode(body.circle_id);
116
+ if (!circle) {
117
+ return NextResponse.json(
118
+ { error: `Cannot hold a ceremony in a circle that does not exist: ${body.circle_id}`, hint: "Create the circle node first on /nodes." },
119
+ { status: 404 },
120
+ );
121
+ }
122
+ }
123
+
70
124
  const ceremony = {
71
125
  id: body.id || crypto.randomUUID(),
72
126
  type: body.type,
@@ -74,10 +128,16 @@ export async function POST(request: Request) {
74
128
  participants: body.participants ?? [],
75
129
  medicines_used: body.medicines_used ?? [],
76
130
  intentions: body.intentions ?? [],
77
- timestamp: new Date().toISOString(),
131
+ timestamp: typeof body.timestamp === "string" && body.timestamp ? body.timestamp : new Date().toISOString(),
78
132
  research_context: body.research_context,
133
+ ...(Array.isArray(body.relations_honored) ? { relations_honored: body.relations_honored } : {}),
134
+ ...(episodePath ? { episode_path: episodePath } : {}),
135
+ ...(episodeNumber !== undefined ? { episode_number: episodeNumber } : {}),
136
+ ...(typeof body.source === "string" && body.source ? { source: body.source } : {}),
137
+ ...(typeof body.closes === "string" ? { closes: body.closes } : {}),
138
+ ...(typeof body.circle_id === "string" ? { circle_id: body.circle_id } : {}),
79
139
  };
80
-
140
+
81
141
  await store.logCeremony(ceremony);
82
142
  return NextResponse.json({ success: true, ceremony, provider: detectProvider() }, { status: 201 });
83
143
  } catch (error: unknown) {
@@ -0,0 +1,31 @@
1
+ import { NextResponse } from "next/server";
2
+ import { createProvider, detectProvider } from "@medicine-wheel/storage-provider";
3
+
4
+ type RouteContext = { params: Promise<{ id: string }> };
5
+
6
+ export async function GET(_request: Request, context: RouteContext) {
7
+ const { id } = await context.params;
8
+ try {
9
+ const store = await createProvider();
10
+ const entry = await store.getDiaryEntry(id);
11
+ if (!entry) return NextResponse.json({ error: `No diary entry with id ${id}` }, { status: 404 });
12
+ return NextResponse.json({ entry, provider: detectProvider() });
13
+ } catch (error: unknown) {
14
+ const message = error instanceof Error ? error.message : String(error);
15
+ return NextResponse.json({ error: message }, { status: 500 });
16
+ }
17
+ }
18
+
19
+ export async function DELETE(_request: Request, context: RouteContext) {
20
+ const { id } = await context.params;
21
+ try {
22
+ const store = await createProvider();
23
+ const entry = await store.getDiaryEntry(id);
24
+ if (!entry) return NextResponse.json({ error: `No diary entry with id ${id}` }, { status: 404 });
25
+ await store.deleteDiaryEntry(id);
26
+ return NextResponse.json({ success: true, deleted: { id }, provider: detectProvider() });
27
+ } catch (error: unknown) {
28
+ const message = error instanceof Error ? error.message : String(error);
29
+ return NextResponse.json({ error: message }, { status: 500 });
30
+ }
31
+ }
@@ -0,0 +1,130 @@
1
+ import { NextResponse } from "next/server";
2
+ import { parseLimit } from "@/lib/api-paging";
3
+ import { createProvider, detectProvider } from "@medicine-wheel/storage-provider";
4
+ import type { DiaryEntryFilters, DiaryEntryRecord } from "@medicine-wheel/storage-provider";
5
+ import { createDiaryEntry } from "@medicine-wheel/ceremonial-diary";
6
+
7
+ /**
8
+ * The ceremonial diary door (0.14.0).
9
+ *
10
+ * The storage provider has carried diary records since 0.6 (`registerDiaryEntry`,
11
+ * `listDiaryEntries`); no route exposed them, so every consumer that wanted a
12
+ * diary kept its own jsonl beside the wheel. This door reads and writes the
13
+ * same collection the MCP and the provider share.
14
+ *
15
+ * A diary entry is a participant's voice inside a ceremony. It may name the
16
+ * ceremony it belongs to (`ceremony_id`, carried in metadata) and the chronicle
17
+ * episode it writes into (`chronicle`, `chronicle:<episode-folder>`); both are
18
+ * optional — the diary can speak whether or not it writes into an episode.
19
+ */
20
+
21
+ const PHASES = ["miigwechiwendam", "nindokendaan", "ningwaab", "nindoodam", "migwech"] as const;
22
+ const ENTRY_TYPES = ["intention", "observation", "hypothesis", "data", "synthesis", "action", "reflection", "learning"] as const;
23
+
24
+ type Phase = DiaryEntryRecord["phase"];
25
+ type EntryType = DiaryEntryRecord["entryType"];
26
+
27
+ function ceremonyOf(entry: DiaryEntryRecord): string | undefined {
28
+ const value = entry.metadata?.ceremony_id;
29
+ return typeof value === "string" && value ? value : undefined;
30
+ }
31
+
32
+ export async function GET(request: Request) {
33
+ try {
34
+ const { searchParams } = new URL(request.url);
35
+ const limit = parseLimit(searchParams.get("limit"));
36
+ if (limit instanceof NextResponse) return limit;
37
+
38
+ const participant = searchParams.get("participant") ?? undefined;
39
+ const phase = searchParams.get("phase") ?? undefined;
40
+ const entryType = searchParams.get("entryType") ?? searchParams.get("entry_type") ?? undefined;
41
+ const chronicle = searchParams.get("chronicle") ?? undefined;
42
+ const ceremonyId = searchParams.get("ceremony_id") ?? undefined;
43
+ const tags = searchParams.getAll("tag");
44
+
45
+ if (phase && !PHASES.includes(phase as Phase)) {
46
+ return NextResponse.json({ error: `Invalid phase: ${phase}`, hint: PHASES.join(", ") }, { status: 400 });
47
+ }
48
+ if (entryType && !ENTRY_TYPES.includes(entryType as EntryType)) {
49
+ return NextResponse.json({ error: `Invalid entryType: ${entryType}`, hint: ENTRY_TYPES.join(", ") }, { status: 400 });
50
+ }
51
+
52
+ const filters: DiaryEntryFilters = {
53
+ ...(participant ? { participant } : {}),
54
+ ...(phase ? { phase: phase as Phase } : {}),
55
+ ...(entryType ? { entryType: entryType as EntryType } : {}),
56
+ ...(tags.length ? { tags } : {}),
57
+ };
58
+
59
+ const store = await createProvider();
60
+ const all = await store.listDiaryEntries(filters);
61
+ let entries = all;
62
+ if (chronicle) entries = entries.filter((e) => e.chronicle === chronicle);
63
+ if (ceremonyId) entries = entries.filter((e) => ceremonyOf(e) === ceremonyId);
64
+ const matched = entries.length;
65
+ if (limit !== null && entries.length > limit) entries = entries.slice(0, limit);
66
+
67
+ return NextResponse.json({
68
+ entries,
69
+ provider: detectProvider(),
70
+ count: entries.length,
71
+ total: all.length,
72
+ ...(chronicle || ceremonyId ? { matched } : {}),
73
+ truncated: entries.length < matched,
74
+ });
75
+ } catch (error: unknown) {
76
+ const message = error instanceof Error ? error.message : String(error);
77
+ return NextResponse.json({ error: message }, { status: 500 });
78
+ }
79
+ }
80
+
81
+ export async function POST(request: Request) {
82
+ try {
83
+ let body: Record<string, unknown>;
84
+ try {
85
+ body = (await request.json()) as Record<string, unknown>;
86
+ } catch {
87
+ return NextResponse.json({ error: "Body must be JSON" }, { status: 400 });
88
+ }
89
+ const { participant, phase, entryType, content } = body;
90
+ if (typeof participant !== "string" || !participant.trim()) {
91
+ return NextResponse.json({ error: "participant is required" }, { status: 400 });
92
+ }
93
+ if (typeof phase !== "string" || !PHASES.includes(phase as Phase)) {
94
+ return NextResponse.json({ error: `phase must be one of: ${PHASES.join(", ")}` }, { status: 400 });
95
+ }
96
+ if (typeof entryType !== "string" || !ENTRY_TYPES.includes(entryType as EntryType)) {
97
+ return NextResponse.json({ error: `entryType must be one of: ${ENTRY_TYPES.join(", ")}` }, { status: 400 });
98
+ }
99
+ if (typeof content !== "string" || !content.trim()) {
100
+ return NextResponse.json({ error: "content is required" }, { status: 400 });
101
+ }
102
+ const metadata: Record<string, unknown> =
103
+ body.metadata && typeof body.metadata === "object" ? { ...(body.metadata as Record<string, unknown>) } : {};
104
+ const store = await createProvider();
105
+ if (typeof body.ceremony_id === "string" && body.ceremony_id) {
106
+ if (!(await store.getCeremony(body.ceremony_id))) {
107
+ return NextResponse.json(
108
+ { error: `Cannot write into a ceremony that does not exist: ${body.ceremony_id}` },
109
+ { status: 404 },
110
+ );
111
+ }
112
+ metadata.ceremony_id = body.ceremony_id;
113
+ }
114
+ const entry = await createDiaryEntry(store, {
115
+ ...(typeof body.id === "string" && body.id ? { id: body.id } : {}),
116
+ ...(typeof body.timestamp === "string" && body.timestamp ? { timestamp: body.timestamp } : {}),
117
+ participant: participant.trim(),
118
+ phase: phase as Phase,
119
+ entryType: entryType as EntryType,
120
+ content,
121
+ metadata,
122
+ ...(typeof body.agent === "string" && body.agent ? { agent: body.agent } : {}),
123
+ ...(typeof body.chronicle === "string" && body.chronicle ? { chronicle: body.chronicle } : {}),
124
+ });
125
+ return NextResponse.json({ success: true, entry, provider: detectProvider() }, { status: 201 });
126
+ } catch (error: unknown) {
127
+ const message = error instanceof Error ? error.message : String(error);
128
+ return NextResponse.json({ error: message }, { status: 500 });
129
+ }
130
+ }
@@ -0,0 +1,55 @@
1
+ import { NextResponse } from "next/server";
2
+ import { z } from "zod";
3
+ import { getBeat, witnessBeat } from "@/lib/store";
4
+
5
+ type RouteContext = { params: Promise<{ id: string }> };
6
+
7
+ /**
8
+ * One beat, by id — and the witnessing door.
9
+ *
10
+ * In a talking circle a beat is one person's turn. Those who held the circle
11
+ * while it was spoken are its witnesses (`witnesses[]`, 0.14.0). PATCH adds
12
+ * witnesses (a set: repeating a name changes nothing) and may name the
13
+ * `speaker` when the beat was authored without one. Nothing else on a beat is
14
+ * editable here: what was said is what was said.
15
+ */
16
+ const BeatPatchSchema = z
17
+ .object({
18
+ witnesses: z.array(z.string().trim().min(1)).optional(),
19
+ speaker: z.string().trim().min(1).optional(),
20
+ })
21
+ .strict()
22
+ .refine((patch) => patch.witnesses !== undefined || patch.speaker !== undefined, {
23
+ message: "Provide witnesses to add, a speaker, or both.",
24
+ });
25
+
26
+ function notFound(id: string) {
27
+ return NextResponse.json({ error: `No beat with id ${id}` }, { status: 404 });
28
+ }
29
+
30
+ export async function GET(_request: Request, context: RouteContext) {
31
+ const { id } = await context.params;
32
+ const beat = getBeat(id);
33
+ if (!beat) return notFound(id);
34
+ return NextResponse.json(beat);
35
+ }
36
+
37
+ export async function PATCH(request: Request, context: RouteContext) {
38
+ const { id } = await context.params;
39
+ let body: unknown;
40
+ try {
41
+ body = await request.json();
42
+ } catch {
43
+ return NextResponse.json({ error: "Body must be JSON" }, { status: 400 });
44
+ }
45
+ const parsed = BeatPatchSchema.safeParse(body);
46
+ if (!parsed.success) {
47
+ return NextResponse.json(
48
+ { error: "Invalid patch", issues: parsed.error.issues },
49
+ { status: 400 },
50
+ );
51
+ }
52
+ const beat = witnessBeat(id, parsed.data);
53
+ if (!beat) return notFound(id);
54
+ return NextResponse.json(beat);
55
+ }
@@ -30,6 +30,8 @@ export async function POST(request: Request) {
30
30
  parent_beat_id: body.parent_beat_id,
31
31
  sub_beats: body.sub_beats,
32
32
  origin: body.origin,
33
+ speaker: body.speaker,
34
+ witnesses: Array.isArray(body.witnesses) ? body.witnesses : undefined,
33
35
  });
34
36
  // Warnings ride on the created beat rather than replacing it, so clients
35
37
  // that read the beat back by id keep working while advisory findings stop
@@ -15,9 +15,9 @@ import { toast } from "sonner";
15
15
  // Storage returns description at the top level; legacy nodes carried it in metadata.
16
16
  type NodeRecord = RelationalNode & { description?: string };
17
17
 
18
- const NODE_TYPES: NodeType[] = ["human", "land", "spirit", "ancestor", "future", "knowledge"];
18
+ const NODE_TYPES: NodeType[] = ["human", "land", "spirit", "ancestor", "future", "knowledge", "circle"];
19
19
  const DIRECTIONS: DirectionName[] = ["east", "south", "west", "north"];
20
- const NODE_TYPE_ICONS: Record<NodeType, string> = { human: "👤", land: "🌍", spirit: "✨", ancestor: "🪶", future: "🌱", knowledge: "📚" };
20
+ const NODE_TYPE_ICONS: Record<NodeType, string> = { human: "👤", land: "🌍", spirit: "✨", ancestor: "🪶", future: "🌱", knowledge: "📚", circle: "⭕" };
21
21
  const DIRECTION_ICONS: Record<DirectionName, string> = { east: "🌅", south: "🌞", west: "🌄", north: "❄️" };
22
22
 
23
23
  function describeNode(node: NodeRecord): string {
@@ -47,11 +47,22 @@ export function normalizeCeremonyLog(value: unknown): CeremonyLog | null {
47
47
  ...(typeof ceremony.research_context === "string"
48
48
  ? { research_context: ceremony.research_context }
49
49
  : {}),
50
+ ...(Array.isArray(ceremony.relations_honored)
51
+ ? { relations_honored: asStringArray(ceremony.relations_honored) }
52
+ : {}),
53
+ ...(typeof ceremony.episode_path === "string" ? { episode_path: ceremony.episode_path } : {}),
54
+ ...(typeof ceremony.episode_number === "number" ? { episode_number: ceremony.episode_number } : {}),
55
+ ...(typeof ceremony.source === "string" ? { source: ceremony.source } : {}),
56
+ ...(typeof ceremony.closes === "string" ? { closes: ceremony.closes } : {}),
57
+ ...(typeof ceremony.circle_id === "string" ? { circle_id: ceremony.circle_id } : {}),
50
58
  };
51
59
  }
52
60
 
53
61
  export function ceremonyEpisodePath(value: unknown): string | undefined {
54
62
  if (!value || typeof value !== "object") return undefined;
63
+ // Typed binding first (0.14.0), then the legacy JSON string in research_context.
64
+ const typed = (value as Record<string, unknown>).episode_path;
65
+ if (typeof typed === "string" && typed.length > 0) return typed;
55
66
  const researchContext = (value as Record<string, unknown>).research_context;
56
67
  if (typeof researchContext !== "string") return undefined;
57
68
 
@@ -71,6 +71,9 @@ interface StoredBeat {
71
71
  parent_beat_id?: string;
72
72
  sub_beats?: string[];
73
73
  origin?: { producer: string; source_ref?: string; method?: string };
74
+ /** Talking circle (0.14.0): who spoke, who witnessed. */
75
+ speaker?: string;
76
+ witnesses?: string[];
74
77
  }
75
78
 
76
79
  interface StoredCycle {
package/lib/store.ts CHANGED
@@ -175,6 +175,8 @@ export function createBeat(
175
175
  cycle_id: data.cycle_id,
176
176
  parent_beat_id: data.parent_beat_id,
177
177
  origin: data.origin ?? existing?.origin ?? { producer: 'rest' },
178
+ ...(data.speaker !== undefined ? { speaker: data.speaker } : {}),
179
+ ...(data.witnesses !== undefined ? { witnesses: data.witnesses } : {}),
178
180
  };
179
181
 
180
182
  // `strictAct` turns an act that contradicts the direction into an error
@@ -332,3 +334,34 @@ export function seedDemoData() {
332
334
 
333
335
  // Auto-seed on import
334
336
  seedDemoData();
337
+
338
+ // ── Talking-circle doors on beats (0.14.0) ──
339
+
340
+ /** One beat by id, normalized like the collection reads. */
341
+ export function getBeat(id: string): NarrativeBeat | undefined {
342
+ const raw = store.getBeat(id);
343
+ if (!raw) return undefined;
344
+ return extractBeats([raw])[0];
345
+ }
346
+
347
+ /**
348
+ * Add witnesses to a beat (a set: a name already present is not repeated) and,
349
+ * when given, name its speaker. Returns the stored beat, or undefined when
350
+ * there is no such beat. What was said is never rewritten here.
351
+ */
352
+ export function witnessBeat(
353
+ id: string,
354
+ patch: { witnesses?: string[]; speaker?: string },
355
+ ): NarrativeBeat | undefined {
356
+ const raw = store.getBeat(id) as (NarrativeBeat & Record<string, unknown>) | undefined;
357
+ if (!raw) return undefined;
358
+ const witnesses = new Set<string>(Array.isArray(raw.witnesses) ? raw.witnesses : []);
359
+ for (const w of patch.witnesses ?? []) witnesses.add(w);
360
+ const next = {
361
+ ...raw,
362
+ ...(patch.speaker !== undefined ? { speaker: patch.speaker } : {}),
363
+ ...(witnesses.size > 0 ? { witnesses: [...witnesses] } : {}),
364
+ };
365
+ store.createBeat(next as any);
366
+ return extractBeats([next])[0];
367
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@medicine-wheel/app",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
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",
@@ -31,6 +31,7 @@
31
31
  "src/session-reader",
32
32
  "src/ceremony-protocol",
33
33
  "src/storage-provider",
34
+ "src/client",
34
35
  "src/data-store",
35
36
  "src/data-store-postgres",
36
37
  "src/graph-viz",
@@ -47,6 +48,7 @@
47
48
  "src/creative-problem-solving",
48
49
  "src/community-review",
49
50
  "src/consent-lifecycle",
51
+ "src/community-identity",
50
52
  "src/fire-keeper",
51
53
  "src/transformation-tracker",
52
54
  "src/ceremonial-diary",
@@ -86,29 +88,29 @@
86
88
  "release:major": "npm run version:major && npm run publish:all && npm run release:commit"
87
89
  },
88
90
  "dependencies": {
89
- "@medicine-wheel/ceremonial-diary": "^0.13.0",
90
- "@medicine-wheel/ceremony-protocol": "^0.13.0",
91
- "@medicine-wheel/community-review": "^0.13.0",
92
- "@medicine-wheel/consent-lifecycle": "^0.13.0",
93
- "@medicine-wheel/creative-orientation": "^0.13.0",
94
- "@medicine-wheel/data-store": "^0.13.0",
95
- "@medicine-wheel/data-store-postgres": "^0.13.0",
96
- "@medicine-wheel/fire-keeper": "^0.13.0",
97
- "@medicine-wheel/github-ceremony": "^0.13.0",
98
- "@medicine-wheel/graph-viz": "^0.13.0",
99
- "@medicine-wheel/importance-unit": "^0.13.0",
100
- "@medicine-wheel/mcp": "^4.13.0",
101
- "@medicine-wheel/narrative-cluster": "^0.13.0",
102
- "@medicine-wheel/narrative-engine": "^0.13.0",
103
- "@medicine-wheel/ontology-core": "^0.13.0",
104
- "@medicine-wheel/perception-layer": "^0.13.0",
105
- "@medicine-wheel/prompt-decomposition": "^0.13.0",
106
- "@medicine-wheel/relational-index": "^0.13.0",
107
- "@medicine-wheel/relational-query": "^0.13.0",
108
- "@medicine-wheel/session-reader": "^0.13.0",
109
- "@medicine-wheel/storage-provider": "^0.13.0",
110
- "@medicine-wheel/transformation-tracker": "^0.13.0",
111
- "@medicine-wheel/ui-components": "^0.13.0",
91
+ "@medicine-wheel/ceremonial-diary": "^0.14.0",
92
+ "@medicine-wheel/ceremony-protocol": "^0.14.0",
93
+ "@medicine-wheel/community-review": "^0.14.0",
94
+ "@medicine-wheel/consent-lifecycle": "^0.14.0",
95
+ "@medicine-wheel/creative-orientation": "^0.14.0",
96
+ "@medicine-wheel/data-store": "^0.14.0",
97
+ "@medicine-wheel/data-store-postgres": "^0.14.0",
98
+ "@medicine-wheel/fire-keeper": "^0.14.0",
99
+ "@medicine-wheel/github-ceremony": "^0.14.0",
100
+ "@medicine-wheel/graph-viz": "^0.14.0",
101
+ "@medicine-wheel/importance-unit": "^0.14.0",
102
+ "@medicine-wheel/mcp": "^4.14.0",
103
+ "@medicine-wheel/narrative-cluster": "^0.14.0",
104
+ "@medicine-wheel/narrative-engine": "^0.14.0",
105
+ "@medicine-wheel/ontology-core": "^0.14.0",
106
+ "@medicine-wheel/perception-layer": "^0.14.0",
107
+ "@medicine-wheel/prompt-decomposition": "^0.14.0",
108
+ "@medicine-wheel/relational-index": "^0.14.0",
109
+ "@medicine-wheel/relational-query": "^0.14.0",
110
+ "@medicine-wheel/session-reader": "^0.14.0",
111
+ "@medicine-wheel/storage-provider": "^0.14.0",
112
+ "@medicine-wheel/transformation-tracker": "^0.14.0",
113
+ "@medicine-wheel/ui-components": "^0.14.0",
112
114
  "@neondatabase/serverless": "^0.10.0",
113
115
  "@xyflow/react": "^12.3.0",
114
116
  "clsx": "^2.1.1",