@medicine-wheel/app 0.15.8 → 0.16.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/app/api/memory/about/route.ts +27 -0
- package/app/api/memory/ask/route.ts +31 -0
- package/app/api/memory/conclusions/[id]/route.ts +34 -0
- package/app/api/memory/conclusions/route.ts +44 -0
- package/app/api/memory/resend/route.ts +24 -0
- package/app/api/memory/route.ts +15 -0
- package/app/api/memory/search/route.ts +21 -0
- package/lib/memory.ts +170 -0
- package/package.json +27 -25
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { readLimit, readScope, wheelMemory } from '@/lib/memory';
|
|
3
|
+
|
|
4
|
+
export const dynamic = 'force-dynamic';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* What the memory holds about one person, within a scope.
|
|
8
|
+
* Body: { person (a wheel node id), scope, question?, limit? }. jgwill/medicine-wheel#149
|
|
9
|
+
*/
|
|
10
|
+
export async function POST(request: Request) {
|
|
11
|
+
const body = await request.json().catch(() => null);
|
|
12
|
+
const person = typeof body?.person === 'string' ? body.person.trim() : '';
|
|
13
|
+
if (!person) return NextResponse.json({ error: 'person is required (a wheel node id)' }, { status: 400 });
|
|
14
|
+
const read = readScope(body?.scope);
|
|
15
|
+
if ('error' in read) return NextResponse.json({ error: read.error }, { status: 400 });
|
|
16
|
+
try {
|
|
17
|
+
const answer = await wheelMemory().about({
|
|
18
|
+
person,
|
|
19
|
+
scope: read.scope,
|
|
20
|
+
...(typeof body.question === 'string' && body.question.trim() ? { question: body.question.trim() } : {}),
|
|
21
|
+
limit: readLimit(body.limit),
|
|
22
|
+
});
|
|
23
|
+
return NextResponse.json(answer, { headers: { 'cache-control': 'no-store' } });
|
|
24
|
+
} catch (error) {
|
|
25
|
+
return NextResponse.json({ error: error instanceof Error ? error.message : String(error) }, { status: 500 });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { readLimit, readScope, wheelMemory } from '@/lib/memory';
|
|
3
|
+
|
|
4
|
+
export const dynamic = 'force-dynamic';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Ask the wheel's memory a question within a scope.
|
|
8
|
+
* Body: { question, scope, peer?, reasoning_level?, limit? }.
|
|
9
|
+
* The answer draws only on the ceremonies the scope reaches, and every source
|
|
10
|
+
* names the wheel record it rests on. jgwill/medicine-wheel#149
|
|
11
|
+
*/
|
|
12
|
+
export async function POST(request: Request) {
|
|
13
|
+
const body = await request.json().catch(() => null);
|
|
14
|
+
const question = typeof body?.question === 'string' ? body.question.trim() : '';
|
|
15
|
+
if (!question) return NextResponse.json({ error: 'question is required' }, { status: 400 });
|
|
16
|
+
if (question.length > 10000) return NextResponse.json({ error: 'question holds at most 10000 characters' }, { status: 400 });
|
|
17
|
+
const read = readScope(body?.scope);
|
|
18
|
+
if ('error' in read) return NextResponse.json({ error: read.error }, { status: 400 });
|
|
19
|
+
try {
|
|
20
|
+
const answer = await wheelMemory().ask({
|
|
21
|
+
question,
|
|
22
|
+
scope: read.scope,
|
|
23
|
+
...(typeof body.peer === 'string' && body.peer ? { peer: body.peer } : {}),
|
|
24
|
+
...(typeof body.reasoning_level === 'string' ? { reasoning_level: body.reasoning_level } : {}),
|
|
25
|
+
limit: readLimit(body.limit),
|
|
26
|
+
});
|
|
27
|
+
return NextResponse.json(answer, { headers: { 'cache-control': 'no-store' } });
|
|
28
|
+
} catch (error) {
|
|
29
|
+
return NextResponse.json({ error: error instanceof Error ? error.message : String(error) }, { status: 500 });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { MEMORY_PROJECTION_KIND } from '@medicine-wheel/honcho';
|
|
3
|
+
import { createProvider } from '@medicine-wheel/storage-provider';
|
|
4
|
+
|
|
5
|
+
export const dynamic = 'force-dynamic';
|
|
6
|
+
type RouteContext = { params: Promise<{ id: string }> };
|
|
7
|
+
|
|
8
|
+
const STATUSES = ['inferred', 'confirmed', 'rejected'];
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Confirm or reject a conclusion, recording who did it and when beside the
|
|
12
|
+
* status: { status, by }. `by` names a person (a wheel node id or a name).
|
|
13
|
+
* jgwill/medicine-wheel#149 (W8)
|
|
14
|
+
*/
|
|
15
|
+
export async function PATCH(request: Request, context: RouteContext) {
|
|
16
|
+
const { id } = await context.params;
|
|
17
|
+
const body = await request.json().catch(() => null);
|
|
18
|
+
if (!STATUSES.includes(body?.status)) return NextResponse.json({ error: `status must be one of ${STATUSES.join(', ')}` }, { status: 400 });
|
|
19
|
+
const by = typeof body?.by === 'string' ? body.by.trim() : '';
|
|
20
|
+
if (!by) return NextResponse.json({ error: 'by is required: who confirms or rejects it' }, { status: 400 });
|
|
21
|
+
try {
|
|
22
|
+
const store = await createProvider();
|
|
23
|
+
const node = await store.getNode(id);
|
|
24
|
+
if (!node || node.metadata?.kind !== MEMORY_PROJECTION_KIND) {
|
|
25
|
+
return NextResponse.json({ error: `No conclusion ${id} on this wheel` }, { status: 404 });
|
|
26
|
+
}
|
|
27
|
+
const updated = await store.updateNode(id, {
|
|
28
|
+
metadata: { ...node.metadata, status: body.status, judged_by: by, judged_at: new Date().toISOString() },
|
|
29
|
+
});
|
|
30
|
+
return NextResponse.json({ node: updated }, { headers: { 'cache-control': 'no-store' } });
|
|
31
|
+
} catch (error) {
|
|
32
|
+
return NextResponse.json({ error: error instanceof Error ? error.message : String(error) }, { status: 500 });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { honchoIdFor, memoryProjectionNode, type MemoryProjectionKind, type MemoryProjectionStatus } from '@medicine-wheel/honcho';
|
|
3
|
+
import { createProvider } from '@medicine-wheel/storage-provider';
|
|
4
|
+
|
|
5
|
+
export const dynamic = 'force-dynamic';
|
|
6
|
+
|
|
7
|
+
const KINDS: MemoryProjectionKind[] = ['pattern', 'summary', 'open_question', 'preference'];
|
|
8
|
+
const STATUSES: MemoryProjectionStatus[] = ['inferred', 'confirmed', 'rejected'];
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Return a conclusion the memory derived to the wheel, as a `knowledge` node
|
|
12
|
+
* with `metadata.kind: "memory_projection"`: the person it is about, what it
|
|
13
|
+
* rests on, its status. A derived pattern is never stored as a bare fact.
|
|
14
|
+
* Body: { about (a wheel node id), content, kind, status?, source_event_ids?, derived_by?, provider? }.
|
|
15
|
+
* jgwill/medicine-wheel#149
|
|
16
|
+
*/
|
|
17
|
+
export async function POST(request: Request) {
|
|
18
|
+
const body = await request.json().catch(() => null);
|
|
19
|
+
const about = typeof body?.about === 'string' ? body.about.trim() : '';
|
|
20
|
+
const content = typeof body?.content === 'string' ? body.content.trim() : '';
|
|
21
|
+
if (!about) return NextResponse.json({ error: 'about is required (the wheel node id the conclusion is about)' }, { status: 400 });
|
|
22
|
+
if (!content) return NextResponse.json({ error: 'content is required' }, { status: 400 });
|
|
23
|
+
if (!KINDS.includes(body.kind)) return NextResponse.json({ error: `kind must be one of ${KINDS.join(', ')}` }, { status: 400 });
|
|
24
|
+
const status: MemoryProjectionStatus = body.status === undefined ? 'inferred' : body.status;
|
|
25
|
+
if (!STATUSES.includes(status)) return NextResponse.json({ error: `status must be one of ${STATUSES.join(', ')}` }, { status: 400 });
|
|
26
|
+
try {
|
|
27
|
+
const node = memoryProjectionNode({
|
|
28
|
+
source: typeof body.provider === 'string' && body.provider ? body.provider : 'honcho',
|
|
29
|
+
peerId: honchoIdFor(about),
|
|
30
|
+
content,
|
|
31
|
+
kind: body.kind,
|
|
32
|
+
status,
|
|
33
|
+
sourceEventIds: Array.isArray(body.source_event_ids) ? body.source_event_ids.map(String) : [],
|
|
34
|
+
generatedAt: new Date().toISOString(),
|
|
35
|
+
...(typeof body.derived_by === 'string' && body.derived_by ? { derivedBy: body.derived_by } : {}),
|
|
36
|
+
});
|
|
37
|
+
node.metadata.about = about;
|
|
38
|
+
const store = await createProvider();
|
|
39
|
+
await store.createNode(node as never);
|
|
40
|
+
return NextResponse.json({ node }, { status: 201, headers: { 'cache-control': 'no-store' } });
|
|
41
|
+
} catch (error) {
|
|
42
|
+
return NextResponse.json({ error: error instanceof Error ? error.message : String(error) }, { status: 500 });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { holdsRecord, honchoProjectionStatus, isPendingRef, markPending, PENDING_KINDS, retryPending } from '@/lib/honcho-projection';
|
|
3
|
+
|
|
4
|
+
export const dynamic = 'force-dynamic';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Send one wheel record to the memory provider again: `{ kind, id }` with kind
|
|
8
|
+
* `beat`, `ceremony` or `diary`. It joins the pending ledger and one delivery
|
|
9
|
+
* pass runs before the answer. The same door as `/api/honcho/pending`, under a
|
|
10
|
+
* name that does not depend on the provider. jgwill/medicine-wheel#149
|
|
11
|
+
*/
|
|
12
|
+
export async function POST(request: Request) {
|
|
13
|
+
const body = await request.json().catch(() => null);
|
|
14
|
+
if (!isPendingRef(body)) {
|
|
15
|
+
return NextResponse.json({ error: 'Provide { kind, id }', kinds: PENDING_KINDS }, { status: 400 });
|
|
16
|
+
}
|
|
17
|
+
if (!honchoProjectionStatus().enabled) {
|
|
18
|
+
return NextResponse.json({ error: 'No memory provider is configured on this wheel; nothing to send to' }, { status: 409 });
|
|
19
|
+
}
|
|
20
|
+
if (!(await holdsRecord(body))) return NextResponse.json({ error: `The wheel holds no ${body.kind} ${body.id}` }, { status: 404 });
|
|
21
|
+
markPending({ kind: body.kind, id: body.id }, 'resent by hand');
|
|
22
|
+
const result = await retryPending();
|
|
23
|
+
return NextResponse.json({ resent: { kind: body.kind, id: body.id }, ...result }, { headers: { 'cache-control': 'no-store' } });
|
|
24
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { wheelMemory } from '@/lib/memory';
|
|
3
|
+
import { honchoProjectionStatus } from '@/lib/honcho-projection';
|
|
4
|
+
|
|
5
|
+
export const dynamic = 'force-dynamic';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The wheel's memory: which providers answer, whether each can keep its
|
|
9
|
+
* reasoning inside a reach, and what the river still owes Honcho.
|
|
10
|
+
* jgwill/medicine-wheel#149
|
|
11
|
+
*/
|
|
12
|
+
export async function GET() {
|
|
13
|
+
const { providers } = await wheelMemory().status();
|
|
14
|
+
return NextResponse.json({ providers, river: honchoProjectionStatus() }, { headers: { 'cache-control': 'no-store' } });
|
|
15
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { readLimit, readScope, wheelMemory } from '@/lib/memory';
|
|
3
|
+
|
|
4
|
+
export const dynamic = 'force-dynamic';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The records within a scope closest to a query, with no reasoning.
|
|
8
|
+
* Body: { query, scope, limit? }. jgwill/medicine-wheel#149
|
|
9
|
+
*/
|
|
10
|
+
export async function POST(request: Request) {
|
|
11
|
+
const body = await request.json().catch(() => null);
|
|
12
|
+
const query = typeof body?.query === 'string' ? body.query.trim() : '';
|
|
13
|
+
if (!query) return NextResponse.json({ error: 'query is required' }, { status: 400 });
|
|
14
|
+
const read = readScope(body?.scope);
|
|
15
|
+
if ('error' in read) return NextResponse.json({ error: read.error }, { status: 400 });
|
|
16
|
+
try {
|
|
17
|
+
return NextResponse.json(await wheelMemory().search({ query, scope: read.scope, limit: readLimit(body.limit) }), { headers: { 'cache-control': 'no-store' } });
|
|
18
|
+
} catch (error) {
|
|
19
|
+
return NextResponse.json({ error: error instanceof Error ? error.message : String(error) }, { status: 500 });
|
|
20
|
+
}
|
|
21
|
+
}
|
package/lib/memory.ts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wheel's memory, on the server: the one place a memory question is asked.
|
|
3
|
+
*
|
|
4
|
+
* A caller names a scope; `@medicine-wheel/memory` turns it into the
|
|
5
|
+
* ceremonies it reaches from this store, and every provider is confined to
|
|
6
|
+
* that reach. Only this server holds a provider's settings (`HONCHO_URL`,
|
|
7
|
+
* `HONCHO_WORKSPACE_ID`, `HONCHO_API_KEY`), so an agent needs `MW_API_URL`
|
|
8
|
+
* and nothing else. With no provider, or with one down, the wheel answers from
|
|
9
|
+
* its own records. jgwill/medicine-wheel#149
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
createMemory,
|
|
14
|
+
type CeremonyRef,
|
|
15
|
+
type Memory,
|
|
16
|
+
type MemoryRecord,
|
|
17
|
+
type MemoryScope,
|
|
18
|
+
type MemoryWheel,
|
|
19
|
+
type SeatRef,
|
|
20
|
+
} from '@medicine-wheel/memory';
|
|
21
|
+
import { WHEEL_PEER, createHonchoClient, honchoFromEnv, honchoMemoryProvider } from '@medicine-wheel/honcho';
|
|
22
|
+
import { createProvider } from '@medicine-wheel/storage-provider';
|
|
23
|
+
import { getAllBeats } from '@/lib/store';
|
|
24
|
+
import { ceremonyEpisodePath } from '@/lib/ceremony-response';
|
|
25
|
+
|
|
26
|
+
const MEMBER_OF = 'member_of';
|
|
27
|
+
|
|
28
|
+
/** The store, as the memory reads it. Read on every question: a turn spoken a second ago is in reach. */
|
|
29
|
+
export function wheelForMemory(): MemoryWheel {
|
|
30
|
+
return {
|
|
31
|
+
async ceremonies(): Promise<CeremonyRef[]> {
|
|
32
|
+
const store = await createProvider();
|
|
33
|
+
return (await store.getAllCeremonies(Number.MAX_SAFE_INTEGER)).map((c) => ({
|
|
34
|
+
id: c.id,
|
|
35
|
+
subject_id: c.subject_id ?? null,
|
|
36
|
+
circle_id: c.circle_id ?? null,
|
|
37
|
+
episode_path: ceremonyEpisodePath(c) ?? null,
|
|
38
|
+
closes: c.closes ?? null,
|
|
39
|
+
participants: c.participants ?? [],
|
|
40
|
+
}));
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
async seats(): Promise<SeatRef[]> {
|
|
44
|
+
const store = await createProvider();
|
|
45
|
+
return (await store.getAllEdges(Number.MAX_SAFE_INTEGER))
|
|
46
|
+
.filter((e) => e.relationship_type === MEMBER_OF)
|
|
47
|
+
.map((e) => ({ person_id: e.from_id, circle_id: e.to_id }));
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
async records(reach): Promise<MemoryRecord[]> {
|
|
51
|
+
const ceremonies = new Set(reach.ceremonies);
|
|
52
|
+
const chronicles = new Set(reach.episodes.flatMap((p) => [`chronicle:${p}`, p]));
|
|
53
|
+
const store = await createProvider();
|
|
54
|
+
const out: MemoryRecord[] = [];
|
|
55
|
+
|
|
56
|
+
for (const c of await store.getAllCeremonies(Number.MAX_SAFE_INTEGER)) {
|
|
57
|
+
if (!ceremonies.has(c.id)) continue;
|
|
58
|
+
out.push({
|
|
59
|
+
wheel_kind: 'ceremony',
|
|
60
|
+
wheel_id: c.id,
|
|
61
|
+
ceremony_id: c.id,
|
|
62
|
+
speaker: WHEEL_PEER,
|
|
63
|
+
text: [
|
|
64
|
+
`Ceremony: ${c.type} (${c.direction})`,
|
|
65
|
+
c.participants?.length ? `Participants: ${c.participants.join(', ')}` : '',
|
|
66
|
+
c.intentions?.length ? `Intentions: ${c.intentions.join('; ')}` : '',
|
|
67
|
+
c.research_context && !c.research_context.trim().startsWith('{') ? `Context: ${c.research_context}` : '',
|
|
68
|
+
].filter(Boolean).join('\n'),
|
|
69
|
+
at: c.timestamp,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (const b of getAllBeats()) {
|
|
74
|
+
const ceremony = (b.ceremonies ?? []).find((id) => ceremonies.has(id));
|
|
75
|
+
if (!ceremony) continue;
|
|
76
|
+
out.push({
|
|
77
|
+
wheel_kind: 'beat',
|
|
78
|
+
wheel_id: b.id,
|
|
79
|
+
ceremony_id: ceremony,
|
|
80
|
+
...(b.speaker ? { speaker: b.speaker } : {}),
|
|
81
|
+
text: [`[${b.direction}] ${b.title}`, b.description, b.prose && b.prose !== b.description ? b.prose : '', (b.learnings ?? []).join('\n')]
|
|
82
|
+
.filter(Boolean)
|
|
83
|
+
.join('\n'),
|
|
84
|
+
at: b.timestamp,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (const d of await store.listDiaryEntries({})) {
|
|
89
|
+
const ceremony = typeof d.metadata?.ceremony_id === 'string' ? d.metadata.ceremony_id : undefined;
|
|
90
|
+
const inReach = (ceremony && ceremonies.has(ceremony)) || (!ceremony && d.chronicle && chronicles.has(d.chronicle));
|
|
91
|
+
if (!inReach) continue;
|
|
92
|
+
out.push({
|
|
93
|
+
wheel_kind: 'diary',
|
|
94
|
+
wheel_id: d.id,
|
|
95
|
+
...(ceremony ? { ceremony_id: ceremony } : {}),
|
|
96
|
+
speaker: d.participant,
|
|
97
|
+
text: `[${d.phase} · ${d.entryType}] ${d.content}`,
|
|
98
|
+
at: d.timestamp,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
async names(ids) {
|
|
105
|
+
const store = await createProvider();
|
|
106
|
+
const out: Record<string, string> = {};
|
|
107
|
+
await Promise.all(
|
|
108
|
+
ids.map(async (id) => {
|
|
109
|
+
if (id === WHEEL_PEER) {
|
|
110
|
+
out[id] = 'the wheel';
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const node = await store.getNode(id).catch(() => null);
|
|
114
|
+
if (node?.name) out[id] = node.name;
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
117
|
+
return out;
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let current: { key: string; memory: Memory } | null = null;
|
|
123
|
+
|
|
124
|
+
/** The memory for this process, rebuilt when the provider's settings change. */
|
|
125
|
+
export function wheelMemory(): Memory {
|
|
126
|
+
const cfg = honchoFromEnv();
|
|
127
|
+
const key = cfg ? `${cfg.baseUrl}|${cfg.workspace}|${cfg.apiKey ?? ''}` : 'none';
|
|
128
|
+
if (!current || current.key !== key) {
|
|
129
|
+
// The dialectic reasons for seconds; a question over many ceremonies can take tens.
|
|
130
|
+
const providers = cfg ? [honchoMemoryProvider(createHonchoClient({ ...cfg, timeoutMs: 90_000 }))] : [];
|
|
131
|
+
current = { key, memory: createMemory({ wheel: wheelForMemory(), providers }) };
|
|
132
|
+
}
|
|
133
|
+
return current.memory;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const SCOPE_LISTS = ['ceremonies', 'subject_id', 'circle_id', 'episode_path'] as const;
|
|
137
|
+
|
|
138
|
+
/** A scope from a request body, or the reason it is not one. */
|
|
139
|
+
export function readScope(value: unknown): { scope: MemoryScope } | { error: string } {
|
|
140
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
141
|
+
return { error: 'scope must be an object: { ceremonies?, subject_id?, circle_id?, episode_path?, participant?, exclude_ceremonies? }' };
|
|
142
|
+
}
|
|
143
|
+
const raw = value as Record<string, unknown>;
|
|
144
|
+
const scope: MemoryScope = {};
|
|
145
|
+
for (const key of SCOPE_LISTS) {
|
|
146
|
+
const v = raw[key];
|
|
147
|
+
if (v === undefined || v === null) continue;
|
|
148
|
+
if (typeof v === 'string') scope[key] = v;
|
|
149
|
+
else if (Array.isArray(v) && v.every((x) => typeof x === 'string')) scope[key] = v as string[];
|
|
150
|
+
else return { error: `scope.${key} must be a string or a list of strings` };
|
|
151
|
+
}
|
|
152
|
+
if (raw.participant !== undefined && raw.participant !== null) {
|
|
153
|
+
if (typeof raw.participant !== 'string') return { error: 'scope.participant must be a string' };
|
|
154
|
+
scope.participant = raw.participant;
|
|
155
|
+
}
|
|
156
|
+
if (raw.exclude_ceremonies !== undefined && raw.exclude_ceremonies !== null) {
|
|
157
|
+
if (!Array.isArray(raw.exclude_ceremonies) || !raw.exclude_ceremonies.every((x) => typeof x === 'string')) {
|
|
158
|
+
return { error: 'scope.exclude_ceremonies must be a list of strings' };
|
|
159
|
+
}
|
|
160
|
+
scope.exclude_ceremonies = raw.exclude_ceremonies as string[];
|
|
161
|
+
}
|
|
162
|
+
const unknown = Object.keys(raw).filter((k) => ![...SCOPE_LISTS, 'participant', 'exclude_ceremonies'].includes(k));
|
|
163
|
+
if (unknown.length) return { error: `scope does not know ${unknown.join(', ')}` };
|
|
164
|
+
return { scope };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** A positive whole number up to `max`, or undefined. */
|
|
168
|
+
export function readLimit(value: unknown, max = 50): number | undefined {
|
|
169
|
+
return typeof value === 'number' && Number.isInteger(value) && value > 0 ? Math.min(value, max) : undefined;
|
|
170
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@medicine-wheel/app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.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/session-reader",
|
|
33
33
|
"src/ceremony-protocol",
|
|
34
34
|
"src/storage-provider",
|
|
35
|
+
"src/memory",
|
|
35
36
|
"src/client",
|
|
36
37
|
"src/honcho",
|
|
37
38
|
"src/data-store",
|
|
@@ -90,30 +91,31 @@
|
|
|
90
91
|
"release:major": "npm run version:major && npm run publish:all && npm run release:commit"
|
|
91
92
|
},
|
|
92
93
|
"dependencies": {
|
|
93
|
-
"@medicine-wheel/ceremonial-diary": "^0.
|
|
94
|
-
"@medicine-wheel/ceremony-protocol": "^0.
|
|
95
|
-
"@medicine-wheel/community-review": "^0.
|
|
96
|
-
"@medicine-wheel/consent-lifecycle": "^0.
|
|
97
|
-
"@medicine-wheel/creative-orientation": "^0.
|
|
98
|
-
"@medicine-wheel/data-store": "^0.
|
|
99
|
-
"@medicine-wheel/data-store-postgres": "^0.
|
|
100
|
-
"@medicine-wheel/fire-keeper": "^0.
|
|
101
|
-
"@medicine-wheel/github-ceremony": "^0.
|
|
102
|
-
"@medicine-wheel/graph-viz": "^0.
|
|
103
|
-
"@medicine-wheel/honcho": "^0.
|
|
104
|
-
"@medicine-wheel/
|
|
105
|
-
"@medicine-wheel/
|
|
106
|
-
"@medicine-wheel/
|
|
107
|
-
"@medicine-wheel/narrative-
|
|
108
|
-
"@medicine-wheel/
|
|
109
|
-
"@medicine-wheel/
|
|
110
|
-
"@medicine-wheel/
|
|
111
|
-
"@medicine-wheel/
|
|
112
|
-
"@medicine-wheel/relational-
|
|
113
|
-
"@medicine-wheel/
|
|
114
|
-
"@medicine-wheel/
|
|
115
|
-
"@medicine-wheel/
|
|
116
|
-
"@medicine-wheel/
|
|
94
|
+
"@medicine-wheel/ceremonial-diary": "^0.16.0",
|
|
95
|
+
"@medicine-wheel/ceremony-protocol": "^0.16.0",
|
|
96
|
+
"@medicine-wheel/community-review": "^0.16.0",
|
|
97
|
+
"@medicine-wheel/consent-lifecycle": "^0.16.0",
|
|
98
|
+
"@medicine-wheel/creative-orientation": "^0.16.0",
|
|
99
|
+
"@medicine-wheel/data-store": "^0.16.0",
|
|
100
|
+
"@medicine-wheel/data-store-postgres": "^0.16.0",
|
|
101
|
+
"@medicine-wheel/fire-keeper": "^0.16.0",
|
|
102
|
+
"@medicine-wheel/github-ceremony": "^0.16.0",
|
|
103
|
+
"@medicine-wheel/graph-viz": "^0.16.0",
|
|
104
|
+
"@medicine-wheel/honcho": "^0.16.0",
|
|
105
|
+
"@medicine-wheel/memory": "^0.16.0",
|
|
106
|
+
"@medicine-wheel/importance-unit": "^0.16.0",
|
|
107
|
+
"@medicine-wheel/mcp": "^4.16.0",
|
|
108
|
+
"@medicine-wheel/narrative-cluster": "^0.16.0",
|
|
109
|
+
"@medicine-wheel/narrative-engine": "^0.16.0",
|
|
110
|
+
"@medicine-wheel/ontology-core": "^0.16.0",
|
|
111
|
+
"@medicine-wheel/perception-layer": "^0.16.0",
|
|
112
|
+
"@medicine-wheel/prompt-decomposition": "^0.16.0",
|
|
113
|
+
"@medicine-wheel/relational-index": "^0.16.0",
|
|
114
|
+
"@medicine-wheel/relational-query": "^0.16.0",
|
|
115
|
+
"@medicine-wheel/session-reader": "^0.16.0",
|
|
116
|
+
"@medicine-wheel/storage-provider": "^0.16.0",
|
|
117
|
+
"@medicine-wheel/transformation-tracker": "^0.16.0",
|
|
118
|
+
"@medicine-wheel/ui-components": "^0.16.0",
|
|
117
119
|
"@neondatabase/serverless": "^0.10.0",
|
|
118
120
|
"@xyflow/react": "^12.3.0",
|
|
119
121
|
"clsx": "^2.1.1",
|