@medicine-wheel/app 0.18.0 → 0.19.1
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 +11 -1
- package/app/api/edges/route.ts +7 -4
- package/app/api/events/git/route.ts +23 -0
- package/dist/cli/mw.js +9 -1
- package/instrumentation.ts +6 -1
- package/lib/event-log.ts +96 -4
- package/package.json +28 -28
package/README.md
CHANGED
|
@@ -363,7 +363,7 @@ weaves, plan perspectives, charts and MMOTs are not recorded yet. Each line is
|
|
|
363
363
|
a `MiadiEventEnvelope` (jgwill/Miadi
|
|
364
364
|
`rispecs/events/00-miadi-event-spine.rispec.md`) namespaced `medicine-wheel.*`:
|
|
365
365
|
`cycle.opened`, `beat.recorded`, `ceremony.closed`, `circle.created`,
|
|
366
|
-
`chronicle.episode.registered`, `relation.made`, … —
|
|
366
|
+
`chronicle.episode.registered`, `chronicle.lineage.made`, `relation.made`, … — 32 in all, catalogued in
|
|
367
367
|
[`@medicine-wheel/events`](src/events/README.md).
|
|
368
368
|
|
|
369
369
|
- A payload names records and carries labels. A cycle's question, a beat's
|
|
@@ -381,6 +381,16 @@ mw events schema [--asyncapi] # the JSON Schema, or th
|
|
|
381
381
|
Set `MW_INSTANCE` (e.g. `ilex:8040`) to name this wheel in each event's
|
|
382
382
|
`source.instance`.
|
|
383
383
|
|
|
384
|
+
**Carried by git.** Set `MW_EVENTS_GIT_SYNC=push` and the wheel commits
|
|
385
|
+
`events.jsonl` (that file only) in its data repository and pushes it. It never
|
|
386
|
+
pushes someone else's commit. A follower elsewhere runs
|
|
387
|
+
`mw-events follow <clone> --exec <command>` and handles each event it has not
|
|
388
|
+
seen yet. See the [package README](src/events/README.md#carried-by-git).
|
|
389
|
+
|
|
390
|
+
Two chronicle episodes related through the Miadi lineage door (`POST /api/edges`
|
|
391
|
+
between two `chronicle_episode` nodes) produce `chronicle.lineage.made`, which
|
|
392
|
+
carries the manifest's sentence that says why.
|
|
393
|
+
|
|
384
394
|
**Publishing.** Nothing leaves the machine until `MW_EVENTS_PUBLISH_URL` is set.
|
|
385
395
|
Once it is, the wheel POSTs each event, in log order, after every write and
|
|
386
396
|
every minute (`MW_EVENTS_PUBLISH_INTERVAL_MS`).
|
package/app/api/edges/route.ts
CHANGED
|
@@ -178,8 +178,9 @@ export async function POST(request: Request) {
|
|
|
178
178
|
|
|
179
179
|
await store.createEdge(edge);
|
|
180
180
|
// The event log (#164): a relation made, or — over an existing pair —
|
|
181
|
-
// amended in what moved.
|
|
182
|
-
|
|
181
|
+
// amended in what moved. Between two chronicle episodes it is lineage,
|
|
182
|
+
// and carries the sentence that says why.
|
|
183
|
+
recordEvent(relationWritten(previous, edge, { from: fromNode, to: toNode }));
|
|
183
184
|
return NextResponse.json(
|
|
184
185
|
{ success: true, edge, replaced, provider: detectProvider() },
|
|
185
186
|
{ status: 201 },
|
|
@@ -216,7 +217,8 @@ export async function PATCH(request: Request) {
|
|
|
216
217
|
const store = await createProvider();
|
|
217
218
|
const previous = await store.getEdge(pair.from, pair.to);
|
|
218
219
|
const edge = await store.updateEdge(pair.from, pair.to, parsed.data);
|
|
219
|
-
|
|
220
|
+
const [fromNode, toNode] = await Promise.all([store.getNode(pair.from), store.getNode(pair.to)]);
|
|
221
|
+
recordEvent(relationWritten(previous ?? edge, edge, { from: fromNode, to: toNode }));
|
|
220
222
|
return NextResponse.json({ success: true, edge, provider: detectProvider() });
|
|
221
223
|
} catch (error: unknown) {
|
|
222
224
|
if (error instanceof EdgeNotFoundError) {
|
|
@@ -234,8 +236,9 @@ export async function DELETE(request: Request) {
|
|
|
234
236
|
|
|
235
237
|
const store = await createProvider();
|
|
236
238
|
const previous = await store.getEdge(pair.from, pair.to);
|
|
239
|
+
const [fromNode, toNode] = await Promise.all([store.getNode(pair.from), store.getNode(pair.to)]);
|
|
237
240
|
await store.deleteEdge(pair.from, pair.to);
|
|
238
|
-
recordEvent(relationRemoved(previous ?? { from_id: pair.from, to_id: pair.to }));
|
|
241
|
+
recordEvent(relationRemoved(previous ?? { from_id: pair.from, to_id: pair.to }, { from: fromNode, to: toNode }));
|
|
239
242
|
return NextResponse.json({
|
|
240
243
|
success: true,
|
|
241
244
|
deleted: { from_id: pair.from, to_id: pair.to },
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { eventGitStatus, gitSyncMode, syncEventsToGit } from "@/lib/event-log";
|
|
3
|
+
|
|
4
|
+
const OFF = {
|
|
5
|
+
error: "The git carriage is off — events.jsonl is not committed by the wheel.",
|
|
6
|
+
hint: "Set MW_EVENTS_GIT_SYNC=push (or commit) and restart the wheel.",
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The event log carried by git (#164). GET: the mode and the last pass —
|
|
11
|
+
* what was committed, whether it was pushed, how many commits the remote
|
|
12
|
+
* still lacks, the last error. POST: commit (and push) now.
|
|
13
|
+
*/
|
|
14
|
+
export async function GET() {
|
|
15
|
+
if (gitSyncMode() === "off") return NextResponse.json(OFF, { status: 409 });
|
|
16
|
+
return NextResponse.json(eventGitStatus());
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function POST() {
|
|
20
|
+
if (gitSyncMode() === "off") return NextResponse.json(OFF, { status: 409 });
|
|
21
|
+
const result = await syncEventsToGit();
|
|
22
|
+
return NextResponse.json(result, { status: result?.error ? 502 : 200 });
|
|
23
|
+
}
|
package/dist/cli/mw.js
CHANGED
|
@@ -308,6 +308,7 @@ ${C.bold}🌿 mw — Medicine Wheel CLI${C.reset}
|
|
|
308
308
|
mw events status What the publisher still owes
|
|
309
309
|
mw events publish Send what waits now
|
|
310
310
|
mw events replay <event-id|all> Send again from that event
|
|
311
|
+
mw events sync Commit (and push) events.jsonl now
|
|
311
312
|
mw events schema [--asyncapi] Print the JSON Schema (or AsyncAPI)
|
|
312
313
|
|
|
313
314
|
SYSTEM
|
|
@@ -842,11 +843,18 @@ async function cmdEvents(positional, flags) {
|
|
|
842
843
|
process.exitCode = 1;
|
|
843
844
|
break;
|
|
844
845
|
}
|
|
846
|
+
case 'sync': {
|
|
847
|
+
const { status, data } = await eventsApi('POST', '/api/events/git');
|
|
848
|
+
if (status !== 200)
|
|
849
|
+
return failed(status, data);
|
|
850
|
+
ppValue(data);
|
|
851
|
+
break;
|
|
852
|
+
}
|
|
845
853
|
case 'schema':
|
|
846
854
|
console.log(JSON.stringify(flags['asyncapi'] ? (0, events_1.asyncApiDocument)() : (0, events_1.wheelEventsJsonSchema)(), null, 2));
|
|
847
855
|
break;
|
|
848
856
|
default:
|
|
849
|
-
unknownSub('events', sub, ['list', 'status', 'publish', 'replay', 'schema']);
|
|
857
|
+
unknownSub('events', sub, ['list', 'status', 'publish', 'replay', 'sync', 'schema']);
|
|
850
858
|
}
|
|
851
859
|
}
|
|
852
860
|
// ── Main dispatch ─────────────────────────────────────────────────
|
package/instrumentation.ts
CHANGED
|
@@ -8,11 +8,16 @@
|
|
|
8
8
|
* Starts the event publisher's clock (#164): events in `events.jsonl` that the
|
|
9
9
|
* configured publisher has not taken are sent on start and every minute after.
|
|
10
10
|
* With `MW_EVENTS_PUBLISH_URL` unset this does nothing.
|
|
11
|
+
*
|
|
12
|
+
* Starts the event log's git carriage (#164): with `MW_EVENTS_GIT_SYNC` set,
|
|
13
|
+
* `events.jsonl` is committed in its data repository (and pushed) on start
|
|
14
|
+
* and every ten minutes, besides soon after each recorded event.
|
|
11
15
|
*/
|
|
12
16
|
export async function register() {
|
|
13
17
|
if (process.env.NEXT_RUNTIME !== "nodejs") return;
|
|
14
18
|
const { startPendingRetries } = await import("@/lib/honcho-projection");
|
|
15
19
|
startPendingRetries();
|
|
16
|
-
const { startEventPublishing } = await import("@/lib/event-log");
|
|
20
|
+
const { startEventPublishing, startEventGitSync } = await import("@/lib/event-log");
|
|
17
21
|
startEventPublishing();
|
|
22
|
+
startEventGitSync();
|
|
18
23
|
}
|
package/lib/event-log.ts
CHANGED
|
@@ -19,9 +19,16 @@
|
|
|
19
19
|
* `MW_EVENTS_PUBLISH_INTERVAL_MS` (default one minute), from this wheel's
|
|
20
20
|
* cursor, with receipts beside the log. Unset, nothing leaves the machine.
|
|
21
21
|
*
|
|
22
|
+
* Carried by git, before any broker: with `MW_EVENTS_GIT_SYNC=push` (or
|
|
23
|
+
* `commit`), the wheel commits `events.jsonl` — that file only — in the data
|
|
24
|
+
* repository it lives in, about half a minute after events are recorded
|
|
25
|
+
* (`MW_EVENTS_GIT_DELAY_MS`), on start, and every ten minutes
|
|
26
|
+
* (`MW_EVENTS_GIT_INTERVAL_MS`), then pushes. A follower elsewhere pulls a
|
|
27
|
+
* clone and acts on what it has not seen (`mw-events follow`).
|
|
28
|
+
*
|
|
22
29
|
* Never the caller's error: a line that cannot be written is reported once on
|
|
23
|
-
* stderr with the record's id, and a publisher that cannot be
|
|
24
|
-
* receipt, not a failed write.
|
|
30
|
+
* stderr with the record's id, and a publisher or a remote that cannot be
|
|
31
|
+
* reached is a receipt or a status, not a failed write.
|
|
25
32
|
*/
|
|
26
33
|
|
|
27
34
|
import { buildEvent, filterEvents, type EventFilter, type WheelEvent, type WheelEventInput } from '@medicine-wheel/events';
|
|
@@ -37,6 +44,7 @@ import {
|
|
|
37
44
|
type PublisherStatus,
|
|
38
45
|
type Receipt,
|
|
39
46
|
} from '@medicine-wheel/events/publish';
|
|
47
|
+
import { commitEventLog, type CommitResult } from '@medicine-wheel/events/git';
|
|
40
48
|
import { resolveProjectDataDir } from '@/lib/jsonl-store';
|
|
41
49
|
|
|
42
50
|
/** Where the log lives: `MW_EVENTS_FILE`, else beside the wheel's JSONL store. */
|
|
@@ -68,6 +76,7 @@ export function recordEvent(input: WheelEventInput | null): WheelEvent | undefin
|
|
|
68
76
|
return undefined;
|
|
69
77
|
}
|
|
70
78
|
kickPublisher();
|
|
79
|
+
kickGitSync();
|
|
71
80
|
return event;
|
|
72
81
|
}
|
|
73
82
|
|
|
@@ -170,8 +179,90 @@ export function eventPublisherReport(limit = 20): { publisher: PublisherStatus;
|
|
|
170
179
|
return { publisher: publisherStatus(file, publisher.name), receipts: readReceipts(file, publisher.name).slice(-limit).reverse() };
|
|
171
180
|
}
|
|
172
181
|
|
|
173
|
-
|
|
174
|
-
|
|
182
|
+
// ── carried by git ──────────────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
export type GitSyncMode = 'off' | 'commit' | 'push';
|
|
185
|
+
|
|
186
|
+
/** `MW_EVENTS_GIT_SYNC`: `push` (or `1`, `true`), `commit`, else off. */
|
|
187
|
+
export function gitSyncMode(): GitSyncMode {
|
|
188
|
+
const value = process.env.MW_EVENTS_GIT_SYNC?.trim().toLowerCase();
|
|
189
|
+
if (value === 'push' || value === '1' || value === 'true') return 'push';
|
|
190
|
+
if (value === 'commit') return 'commit';
|
|
191
|
+
return 'off';
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const GIT = Symbol.for('@medicine-wheel/app.event-git');
|
|
195
|
+
type GitState = {
|
|
196
|
+
running?: Promise<CommitResult | null>;
|
|
197
|
+
again?: boolean;
|
|
198
|
+
timer?: ReturnType<typeof setTimeout>;
|
|
199
|
+
interval?: ReturnType<typeof setInterval>;
|
|
200
|
+
failing?: boolean;
|
|
201
|
+
last?: CommitResult & { at: string };
|
|
202
|
+
};
|
|
203
|
+
const gitState: GitState = ((globalThis as Record<symbol, GitState>)[GIT] ??= {});
|
|
204
|
+
|
|
205
|
+
async function gitPass(): Promise<CommitResult | null> {
|
|
206
|
+
const mode = gitSyncMode();
|
|
207
|
+
if (mode === 'off') return null;
|
|
208
|
+
const result = await commitEventLog(eventLogFile(), { push: mode === 'push', instance: eventInstance() });
|
|
209
|
+
gitState.last = { ...result, at: new Date().toISOString() };
|
|
210
|
+
if (result.error && !gitState.failing) {
|
|
211
|
+
console.error(`[events] git carriage stopped: ${result.error} — the log keeps growing and the next pass tries again`);
|
|
212
|
+
} else if (!result.error && gitState.failing) {
|
|
213
|
+
console.log('[events] git carriage resumed');
|
|
214
|
+
}
|
|
215
|
+
if (result.committed) {
|
|
216
|
+
console.log(`[events] committed ${result.events} event(s) in ${result.commit?.slice(0, 8)}${result.pushed ? ', pushed' : ''}`);
|
|
217
|
+
}
|
|
218
|
+
gitState.failing = Boolean(result.error);
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Commit (and push) the log now. A pass already running is joined, and runs once more after it. */
|
|
223
|
+
export function syncEventsToGit(): Promise<CommitResult | null> {
|
|
224
|
+
if (gitState.running) {
|
|
225
|
+
gitState.again = true;
|
|
226
|
+
return gitState.running;
|
|
227
|
+
}
|
|
228
|
+
const run = gitPass().then(async (result) => {
|
|
229
|
+
while (gitState.again) {
|
|
230
|
+
gitState.again = false;
|
|
231
|
+
result = (await gitPass()) ?? result;
|
|
232
|
+
}
|
|
233
|
+
return result;
|
|
234
|
+
}).finally(() => {
|
|
235
|
+
if (gitState.running === run) gitState.running = undefined;
|
|
236
|
+
});
|
|
237
|
+
gitState.running = run;
|
|
238
|
+
return run;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** After a recorded event: commit soon, so a burst of writes travels as one commit. */
|
|
242
|
+
export function kickGitSync(): void {
|
|
243
|
+
if (gitSyncMode() === 'off' || gitState.timer) return;
|
|
244
|
+
gitState.timer = setTimeout(() => {
|
|
245
|
+
gitState.timer = undefined;
|
|
246
|
+
void syncEventsToGit();
|
|
247
|
+
}, Number(process.env.MW_EVENTS_GIT_DELAY_MS) || 30_000);
|
|
248
|
+
gitState.timer.unref?.();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Start the carriage clock from `instrumentation.ts`: a pass now, then every ten minutes. */
|
|
252
|
+
export function startEventGitSync(intervalMs = Number(process.env.MW_EVENTS_GIT_INTERVAL_MS) || 10 * 60_000): void {
|
|
253
|
+
if (gitSyncMode() === 'off' || gitState.interval) return;
|
|
254
|
+
gitState.interval = setInterval(() => { void syncEventsToGit(); }, intervalMs);
|
|
255
|
+
gitState.interval.unref?.();
|
|
256
|
+
void syncEventsToGit();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** The carriage's mode and its last pass. */
|
|
260
|
+
export function eventGitStatus(): { mode: GitSyncMode; last?: GitState['last'] } {
|
|
261
|
+
return { mode: gitSyncMode(), ...(gitState.last ? { last: gitState.last } : {}) };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** What `/api/health` reports: how many events, the last one, what the publisher owes, how the git carriage stands. */
|
|
265
|
+
export function eventLogStatus(): { count: number; last_at?: string; publisher?: PublisherStatus; git?: ReturnType<typeof eventGitStatus> } | { error: string } {
|
|
175
266
|
try {
|
|
176
267
|
const events = readEvents();
|
|
177
268
|
const last = events[events.length - 1];
|
|
@@ -180,6 +271,7 @@ export function eventLogStatus(): { count: number; last_at?: string; publisher?:
|
|
|
180
271
|
count: events.length,
|
|
181
272
|
...(last ? { last_at: last.observed_at } : {}),
|
|
182
273
|
...(publisher ? { publisher: publisherStatus(eventLogFile(), publisher.name) } : {}),
|
|
274
|
+
...(gitSyncMode() !== 'off' ? { git: eventGitStatus() } : {}),
|
|
183
275
|
};
|
|
184
276
|
} catch (error) {
|
|
185
277
|
return { error: (error as Error)?.message ?? String(error) };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@medicine-wheel/app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.1",
|
|
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",
|
|
@@ -92,33 +92,33 @@
|
|
|
92
92
|
"release:major": "npm run version:major && npm run publish:all && npm run release:commit"
|
|
93
93
|
},
|
|
94
94
|
"dependencies": {
|
|
95
|
-
"@medicine-wheel/ceremonial-diary": "^0.
|
|
96
|
-
"@medicine-wheel/ceremony-protocol": "^0.
|
|
97
|
-
"@medicine-wheel/community-identity": "^0.
|
|
98
|
-
"@medicine-wheel/community-review": "^0.
|
|
99
|
-
"@medicine-wheel/consent-lifecycle": "^0.
|
|
100
|
-
"@medicine-wheel/creative-orientation": "^0.
|
|
101
|
-
"@medicine-wheel/data-store": "^0.
|
|
102
|
-
"@medicine-wheel/data-store-postgres": "^0.
|
|
103
|
-
"@medicine-wheel/events": "^0.
|
|
104
|
-
"@medicine-wheel/fire-keeper": "^0.
|
|
105
|
-
"@medicine-wheel/github-ceremony": "^0.
|
|
106
|
-
"@medicine-wheel/graph-viz": "^0.
|
|
107
|
-
"@medicine-wheel/honcho": "^0.
|
|
108
|
-
"@medicine-wheel/importance-unit": "^0.
|
|
109
|
-
"@medicine-wheel/mcp": "^4.
|
|
110
|
-
"@medicine-wheel/memory": "^0.
|
|
111
|
-
"@medicine-wheel/narrative-cluster": "^0.
|
|
112
|
-
"@medicine-wheel/narrative-engine": "^0.
|
|
113
|
-
"@medicine-wheel/ontology-core": "^0.
|
|
114
|
-
"@medicine-wheel/perception-layer": "^0.
|
|
115
|
-
"@medicine-wheel/prompt-decomposition": "^0.
|
|
116
|
-
"@medicine-wheel/relational-index": "^0.
|
|
117
|
-
"@medicine-wheel/relational-query": "^0.
|
|
118
|
-
"@medicine-wheel/session-reader": "^0.
|
|
119
|
-
"@medicine-wheel/storage-provider": "^0.
|
|
120
|
-
"@medicine-wheel/transformation-tracker": "^0.
|
|
121
|
-
"@medicine-wheel/ui-components": "^0.
|
|
95
|
+
"@medicine-wheel/ceremonial-diary": "^0.19.1",
|
|
96
|
+
"@medicine-wheel/ceremony-protocol": "^0.19.1",
|
|
97
|
+
"@medicine-wheel/community-identity": "^0.19.1",
|
|
98
|
+
"@medicine-wheel/community-review": "^0.19.1",
|
|
99
|
+
"@medicine-wheel/consent-lifecycle": "^0.19.1",
|
|
100
|
+
"@medicine-wheel/creative-orientation": "^0.19.1",
|
|
101
|
+
"@medicine-wheel/data-store": "^0.19.1",
|
|
102
|
+
"@medicine-wheel/data-store-postgres": "^0.19.1",
|
|
103
|
+
"@medicine-wheel/events": "^0.19.1",
|
|
104
|
+
"@medicine-wheel/fire-keeper": "^0.19.1",
|
|
105
|
+
"@medicine-wheel/github-ceremony": "^0.19.1",
|
|
106
|
+
"@medicine-wheel/graph-viz": "^0.19.1",
|
|
107
|
+
"@medicine-wheel/honcho": "^0.19.1",
|
|
108
|
+
"@medicine-wheel/importance-unit": "^0.19.1",
|
|
109
|
+
"@medicine-wheel/mcp": "^4.19.1",
|
|
110
|
+
"@medicine-wheel/memory": "^0.19.1",
|
|
111
|
+
"@medicine-wheel/narrative-cluster": "^0.19.1",
|
|
112
|
+
"@medicine-wheel/narrative-engine": "^0.19.1",
|
|
113
|
+
"@medicine-wheel/ontology-core": "^0.19.1",
|
|
114
|
+
"@medicine-wheel/perception-layer": "^0.19.1",
|
|
115
|
+
"@medicine-wheel/prompt-decomposition": "^0.19.1",
|
|
116
|
+
"@medicine-wheel/relational-index": "^0.19.1",
|
|
117
|
+
"@medicine-wheel/relational-query": "^0.19.1",
|
|
118
|
+
"@medicine-wheel/session-reader": "^0.19.1",
|
|
119
|
+
"@medicine-wheel/storage-provider": "^0.19.1",
|
|
120
|
+
"@medicine-wheel/transformation-tracker": "^0.19.1",
|
|
121
|
+
"@medicine-wheel/ui-components": "^0.19.1",
|
|
122
122
|
"@miadi/episode-vessel": "^0.2.1",
|
|
123
123
|
"@neondatabase/serverless": "^0.10.0",
|
|
124
124
|
"@xyflow/react": "^12.3.0",
|