@medicine-wheel/app 0.14.3 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -34,6 +34,7 @@ storage alongside its composable libraries.
34
34
  ├── @medicine-wheel/data-store-postgres ← PostgreSQL / Neon provider scaffold
35
35
  ├── @medicine-wheel/storage-provider ← Canonical persistence contract (JSONL ⇄ Neon)
36
36
  ├── @medicine-wheel/infra ← Hosts, tenants, services, port bindings as facets
37
+ ├── @medicine-wheel/honcho ← The wheel's projection into Honcho, memory that reasons
37
38
  ├── @medicine-wheel/perception-layer ← Witness recordings as typed perceptual events
38
39
  ├── @medicine-wheel/narrative-cluster ← Events → clusters → beats → Film Edit Brief
39
40
  ├── @medicine-wheel/ceremonial-diary ← A participant's voice across the Five Phases
@@ -156,6 +157,11 @@ Typed infrastructure facets — hosts, tenants, services and port bindings — k
156
157
 
157
158
  - **Dependencies:** `@medicine-wheel/ontology-core`, `zod`
158
159
 
160
+ ### [@medicine-wheel/honcho](src/honcho)
161
+ The wheel's projection into [Honcho](https://honcho.dev), memory that reasons. The wheel stays canonical; Honcho holds what the history has come to mean about each peer. A beat becomes a message from its speaker in the session of its ceremony, a speaker's evolving representation is recalled before they speak again, and a derived conclusion returns as a `knowledge` node carrying `metadata.kind: "memory_projection"` with its status and provenance. Zero dependencies, `/v3` only, configured by `HONCHO_URL`. Nothing is filtered here.
162
+
163
+ - **Dependencies:** `@medicine-wheel/ontology-core`
164
+
159
165
  ### [@medicine-wheel/perception-layer](src/perception-layer)
160
166
  Eyes and ears of agent-supported film production — witness a belt-device recording as typed perceptual events and seed a production knowledge graph.
161
167
 
@@ -2,6 +2,8 @@ import { NextResponse } from "next/server";
2
2
  import { parseLimit } from "@/lib/api-paging";
3
3
  import { createProvider, detectProvider } from "@medicine-wheel/storage-provider";
4
4
  import { ceremonyBelongsToEpisode, ceremonyEpisodePath } from "@/lib/ceremony-response";
5
+ import { projectCeremony } from "@medicine-wheel/honcho";
6
+ import { projectAfterWrite } from "@/lib/honcho-projection";
5
7
 
6
8
  /** Episode directory names are `YYYY-MM-DD-episode-NNN-slug`; nothing else may bind. */
7
9
  const EPISODE_PATH = /^\d{4}-\d{2}-\d{2}-episode-\d{3,}-[a-z0-9-]+$/;
@@ -139,6 +141,9 @@ export async function POST(request: Request) {
139
141
  };
140
142
 
141
143
  await store.logCeremony(ceremony);
144
+ // The river: the stored ceremony leaves for Honcho in the background when
145
+ // HONCHO_URL is set. Never awaited.
146
+ projectAfterWrite(() => projectCeremony(ceremony), `ceremony ${ceremony.id}`);
142
147
  return NextResponse.json({ success: true, ceremony, provider: detectProvider() }, { status: 201 });
143
148
  } catch (error: unknown) {
144
149
  const message = error instanceof Error ? error.message : String(error);
@@ -3,6 +3,8 @@ import { parseLimit } from "@/lib/api-paging";
3
3
  import { createProvider, detectProvider } from "@medicine-wheel/storage-provider";
4
4
  import type { DiaryEntryFilters, DiaryEntryRecord } from "@medicine-wheel/storage-provider";
5
5
  import { createDiaryEntry } from "@medicine-wheel/ceremonial-diary";
6
+ import { projectDiaryEntry } from "@medicine-wheel/honcho";
7
+ import { projectAfterWrite } from "@/lib/honcho-projection";
6
8
 
7
9
  /**
8
10
  * The ceremonial diary door (0.14.0).
@@ -122,6 +124,9 @@ export async function POST(request: Request) {
122
124
  ...(typeof body.agent === "string" && body.agent ? { agent: body.agent } : {}),
123
125
  ...(typeof body.chronicle === "string" && body.chronicle ? { chronicle: body.chronicle } : {}),
124
126
  });
127
+ // The river: the participant's voice leaves for Honcho in the background
128
+ // when HONCHO_URL is set. Never awaited.
129
+ projectAfterWrite(() => projectDiaryEntry(entry), `diary entry ${entry.id}`);
125
130
  return NextResponse.json({ success: true, entry, provider: detectProvider() }, { status: 201 });
126
131
  } catch (error: unknown) {
127
132
  const message = error instanceof Error ? error.message : String(error);
@@ -1,5 +1,6 @@
1
1
  import { NextResponse } from 'next/server';
2
2
  import { createProvider, detectProvider } from '@medicine-wheel/storage-provider';
3
+ import { honchoProjectionStatus } from '@/lib/honcho-projection';
3
4
 
4
5
  export async function GET() {
5
6
  const providerType = detectProvider();
@@ -26,6 +27,9 @@ export async function GET() {
26
27
  nodes,
27
28
  ceremonies,
28
29
  },
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.
32
+ honcho: honchoProjectionStatus(),
29
33
  env: {
30
34
  MW_STORAGE_PROVIDER: process.env.MW_STORAGE_PROVIDER || 'not set',
31
35
  DATABASE_URL: process.env.DATABASE_URL ? 'configured' : 'not configured',
@@ -1,5 +1,7 @@
1
1
  import { NextResponse } from "next/server";
2
2
  import { getAllBeats, createBeat } from "@/lib/store";
3
+ import { projectBeat } from "@medicine-wheel/honcho";
4
+ import { projectAfterWrite } from "@/lib/honcho-projection";
3
5
 
4
6
  export async function GET() {
5
7
  try {
@@ -33,6 +35,9 @@ export async function POST(request: Request) {
33
35
  speaker: body.speaker,
34
36
  witnesses: Array.isArray(body.witnesses) ? body.witnesses : undefined,
35
37
  });
38
+ // The river: a stored beat leaves for Honcho in the background when
39
+ // HONCHO_URL is set. Never awaited — the wheel answers on its own clock.
40
+ projectAfterWrite(() => projectBeat(beat), `beat ${beat.id}`);
36
41
  // Warnings ride on the created beat rather than replacing it, so clients
37
42
  // that read the beat back by id keep working while advisory findings stop
38
43
  // being computed-and-discarded.
@@ -8,6 +8,10 @@ set -e
8
8
  # MW_DATA_DIR — path to JSONL store directory (default: /data/store)
9
9
  # MW_STORAGE_PROVIDER — storage backend (default: jsonl)
10
10
  # PORT — HTTP port (default: 8040)
11
+ # HONCHO_URL — when set, every stored beat, ceremony and diary entry
12
+ # is projected into Honcho (memory that reasons);
13
+ # HONCHO_WORKSPACE_ID (default medicine-wheel),
14
+ # HONCHO_API_KEY (unset while Honcho's auth is off)
11
15
 
12
16
  STORE_DIR="${MW_DATA_DIR:-/data/store}"
13
17
  PORT="${PORT:-8040}"
@@ -17,6 +21,11 @@ mkdir -p "$STORE_DIR"
17
21
  echo "🌿 Medicine Wheel"
18
22
  echo "📁 Store: $STORE_DIR"
19
23
  echo "🌐 http://localhost:$PORT"
24
+ if [ -n "${HONCHO_URL:-}" ]; then
25
+ echo "🧠 Honcho: $HONCHO_URL (workspace ${HONCHO_WORKSPACE_ID:-medicine-wheel})"
26
+ else
27
+ echo "🧠 Honcho: off (set HONCHO_URL to project the wheel into memory)"
28
+ fi
20
29
  echo ""
21
30
 
22
31
  export MW_DATA_DIR="$STORE_DIR"
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Automatic projection — the wheel pushes to Honcho on write.
3
+ *
4
+ * `@medicine-wheel/honcho` is the door; this is the river. Every write route
5
+ * that lands a beat, a ceremony or a diary entry calls {@link projectAfterWrite}
6
+ * once the record is stored. With `HONCHO_URL` set the record leaves for Honcho
7
+ * in the background; unset, nothing happens and nothing is logged. Either way
8
+ * the wheel's own answer to its caller is unchanged: a projection never delays
9
+ * a response and never turns a stored record into an error.
10
+ *
11
+ * A failure is reported once on stderr with the record's id, because a silent
12
+ * memory is worse than a noisy one. `awaitProjections()` exists for tests and
13
+ * for anything that must know the river has run dry before it exits.
14
+ */
15
+
16
+ import {
17
+ createHonchoClient,
18
+ honchoFromEnv,
19
+ project,
20
+ type HonchoClient,
21
+ type HonchoConfig,
22
+ type Projection,
23
+ } from '@medicine-wheel/honcho';
24
+
25
+ const inFlight = new Set<Promise<unknown>>();
26
+
27
+ let cached: { key: string; client: HonchoClient } | null = null;
28
+
29
+ function clientFor(cfg: HonchoConfig): HonchoClient {
30
+ const key = `${cfg.baseUrl}|${cfg.workspace}|${cfg.apiKey ?? ''}`;
31
+ if (cached?.key !== key) cached = { key, client: createHonchoClient(cfg) };
32
+ return cached.client;
33
+ }
34
+
35
+ /** What `/api/health` reports: whether the river runs, and where to. */
36
+ export function honchoProjectionStatus(): { enabled: boolean; url?: string; workspace?: string } {
37
+ const cfg = honchoFromEnv();
38
+ return cfg ? { enabled: true, url: cfg.baseUrl, workspace: cfg.workspace } : { enabled: false };
39
+ }
40
+
41
+ /**
42
+ * 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.
45
+ *
46
+ * The projection is built by `shape`, called *inside* the error boundary. It
47
+ * used to be built by the caller, in the route's own expression — so a record
48
+ * the projection could not shape (a `learnings` that arrived as a string, say)
49
+ * threw synchronously and turned an already-stored write into a 500 for the
50
+ * writer. The wheel's answer to its caller must not depend on the river, and
51
+ * that includes the part that reads the record.
52
+ */
53
+ export function projectAfterWrite(shape: () => Projection, label: string): void {
54
+ const cfg = honchoFromEnv();
55
+ 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);
67
+ }
68
+
69
+ /** Resolves once every projection started so far has settled. */
70
+ export async function awaitProjections(): Promise<void> {
71
+ while (inFlight.size) await Promise.allSettled([...inFlight]);
72
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@medicine-wheel/app",
3
- "version": "0.14.3",
3
+ "version": "0.15.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",
@@ -32,6 +32,7 @@
32
32
  "src/ceremony-protocol",
33
33
  "src/storage-provider",
34
34
  "src/client",
35
+ "src/honcho",
35
36
  "src/data-store",
36
37
  "src/data-store-postgres",
37
38
  "src/graph-viz",
@@ -88,29 +89,30 @@
88
89
  "release:major": "npm run version:major && npm run publish:all && npm run release:commit"
89
90
  },
90
91
  "dependencies": {
91
- "@medicine-wheel/ceremonial-diary": "^0.14.3",
92
- "@medicine-wheel/ceremony-protocol": "^0.14.3",
93
- "@medicine-wheel/community-review": "^0.14.3",
94
- "@medicine-wheel/consent-lifecycle": "^0.14.3",
95
- "@medicine-wheel/creative-orientation": "^0.14.3",
96
- "@medicine-wheel/data-store": "^0.14.3",
97
- "@medicine-wheel/data-store-postgres": "^0.14.3",
98
- "@medicine-wheel/fire-keeper": "^0.14.3",
99
- "@medicine-wheel/github-ceremony": "^0.14.3",
100
- "@medicine-wheel/graph-viz": "^0.14.3",
101
- "@medicine-wheel/importance-unit": "^0.14.3",
102
- "@medicine-wheel/mcp": "^4.14.3",
103
- "@medicine-wheel/narrative-cluster": "^0.14.3",
104
- "@medicine-wheel/narrative-engine": "^0.14.3",
105
- "@medicine-wheel/ontology-core": "^0.14.3",
106
- "@medicine-wheel/perception-layer": "^0.14.3",
107
- "@medicine-wheel/prompt-decomposition": "^0.14.3",
108
- "@medicine-wheel/relational-index": "^0.14.3",
109
- "@medicine-wheel/relational-query": "^0.14.3",
110
- "@medicine-wheel/session-reader": "^0.14.3",
111
- "@medicine-wheel/storage-provider": "^0.14.3",
112
- "@medicine-wheel/transformation-tracker": "^0.14.3",
113
- "@medicine-wheel/ui-components": "^0.14.3",
92
+ "@medicine-wheel/ceremonial-diary": "^0.15.0",
93
+ "@medicine-wheel/ceremony-protocol": "^0.15.0",
94
+ "@medicine-wheel/community-review": "^0.15.0",
95
+ "@medicine-wheel/consent-lifecycle": "^0.15.0",
96
+ "@medicine-wheel/creative-orientation": "^0.15.0",
97
+ "@medicine-wheel/data-store": "^0.15.0",
98
+ "@medicine-wheel/data-store-postgres": "^0.15.0",
99
+ "@medicine-wheel/fire-keeper": "^0.15.0",
100
+ "@medicine-wheel/github-ceremony": "^0.15.0",
101
+ "@medicine-wheel/graph-viz": "^0.15.0",
102
+ "@medicine-wheel/honcho": "^0.15.0",
103
+ "@medicine-wheel/importance-unit": "^0.15.0",
104
+ "@medicine-wheel/mcp": "^4.15.0",
105
+ "@medicine-wheel/narrative-cluster": "^0.15.0",
106
+ "@medicine-wheel/narrative-engine": "^0.15.0",
107
+ "@medicine-wheel/ontology-core": "^0.15.0",
108
+ "@medicine-wheel/perception-layer": "^0.15.0",
109
+ "@medicine-wheel/prompt-decomposition": "^0.15.0",
110
+ "@medicine-wheel/relational-index": "^0.15.0",
111
+ "@medicine-wheel/relational-query": "^0.15.0",
112
+ "@medicine-wheel/session-reader": "^0.15.0",
113
+ "@medicine-wheel/storage-provider": "^0.15.0",
114
+ "@medicine-wheel/transformation-tracker": "^0.15.0",
115
+ "@medicine-wheel/ui-components": "^0.15.0",
114
116
  "@neondatabase/serverless": "^0.10.0",
115
117
  "@xyflow/react": "^12.3.0",
116
118
  "clsx": "^2.1.1",