@medicine-wheel/events 0.0.0-stage → 0.18.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
@@ -1,3 +1,122 @@
1
- # Temporary Holding Version
1
+ # @medicine-wheel/events
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The events a Medicine Wheel writes when something happens on it: a cycle
4
+ opened, a beat recorded, a ceremony closed, a circle created, a chronicle
5
+ episode registered, a relation woven. One catalog, the envelope the events
6
+ travel in, the files that describe them for any consumer, the log they wait in,
7
+ and the seam a broker plugs into.
8
+
9
+ The wheel keeps each record's latest state in its store. This package keeps the
10
+ moments: every write the wheel commits leaves one line in `events.jsonl`
11
+ beside the store. Nothing leaves the machine until a publisher is configured.
12
+ Episode 548 of the Miadi Chronicle is choosing the broker
13
+ (jgwill/medicine-wheel#164, miadisabelle/kherix-hermeneia#70).
14
+
15
+ ## For a consumer
16
+
17
+ | file | what it is |
18
+ |---|---|
19
+ | `schema/wheel-events.schema.json` | One JSON Schema (draft-07) that validates any wheel event: the envelope, plus each event's type, subject kind and payload. |
20
+ | `schema/asyncapi.json` | An AsyncAPI 3.0 document: one channel per event, addressed by its topic (`medicine-wheel.cycle.opened`). AsyncAPI is to events what OpenAPI is to HTTP; AsyncAPI Studio renders it and generators read it. It names no server, because no broker is chosen. |
21
+
22
+ ```js
23
+ const schema = require('@medicine-wheel/events/schema/wheel-events.schema.json');
24
+ const { WHEEL_EVENT_CATALOG, filterEvents } = require('@medicine-wheel/events');
25
+ ```
26
+
27
+ A payload carries references and labels — ids, a direction, a type — never a
28
+ person's words. A cycle's question, a beat's prose and a diary entry's text
29
+ stay in the wheel. To read one, fetch the record by `subject.id` from the wheel
30
+ that wrote the event (`source.instance`).
31
+
32
+ ## The envelope
33
+
34
+ jgwill/Miadi's `MiadiEventEnvelope` 0.1.0
35
+ (`rispecs/events/00-miadi-event-spine.rispec.md`):
36
+
37
+ ```json
38
+ {
39
+ "id": "mwev:1792019860666:4c509d0b",
40
+ "schema_version": "0.1.0",
41
+ "event_type": "medicine-wheel.cycle.opened",
42
+ "source": { "kind": "miadi", "system": "medicine-wheel", "instance": "ilex:8040" },
43
+ "subject": { "kind": "cycle", "id": "cycle-1792019860642-hzqvkk" },
44
+ "occurred_at": "2026-10-14T23:17:40.666Z",
45
+ "observed_at": "2026-10-14T23:17:40.666Z",
46
+ "correlation_id": "cycle-1792019860642-hzqvkk",
47
+ "idempotency_key": "mwev:1792019860666:4c509d0b",
48
+ "privacy": { "classification": "internal", "content_included": false },
49
+ "payload": { "current_direction": "east" }
50
+ }
51
+ ```
52
+
53
+ - `idempotency_key` equals `id`. A resend of the same line is the same
54
+ occurrence; a receiver that keeps keys recognises it.
55
+ - `correlation_id` groups: a cycle and the beats recorded in it, a ceremony and
56
+ its closing, a conclusion and the person it is about.
57
+ - An amendment names what moved in `payload.changed`. A write that changes
58
+ nothing writes no event.
59
+
60
+ ## The events
61
+
62
+ | family | events |
63
+ |---|---|
64
+ | cycle | `cycle.opened`, `cycle.amended`, `cycle.archived` |
65
+ | beat | `beat.recorded`, `beat.amended`, `beat.witnessed` |
66
+ | ceremony | `ceremony.opened`, `ceremony.closed`, `ceremony.amended`, `ceremony.removed` |
67
+ | diary | `diary.written`, `diary.amended`, `diary.removed` |
68
+ | circle | `circle.created`, `circle.amended`, `circle.removed` |
69
+ | chronicle episode | `chronicle.episode.registered`, `chronicle.episode.amended`, `chronicle.episode.removed` |
70
+ | memory conclusion | `memory.conclusion.recorded`, `memory.conclusion.judged`, `memory.conclusion.amended`, `memory.conclusion.removed` |
71
+ | node | `node.created`, `node.amended`, `node.removed` |
72
+ | relation | `relation.made`, `relation.amended`, `relation.removed` |
73
+
74
+ `WHEEL_EVENT_CATALOG` says, for each: what happened, which write produces it,
75
+ what its correlation groups, and every payload field. `PayloadOf<'beat.recorded'>`
76
+ is its payload type.
77
+
78
+ ## For a producer
79
+
80
+ ```ts
81
+ import { buildEvent, cycleWritten } from '@medicine-wheel/events';
82
+ import { appendEvent, eventLogPath } from '@medicine-wheel/events/log';
83
+
84
+ const input = cycleWritten(before, after, Object.keys(patch)); // null when nothing moved
85
+ if (input) appendEvent(eventLogPath(dataDir), buildEvent(input, { instance: process.env.MW_INSTANCE }));
86
+ ```
87
+
88
+ The deciders (`cycleWritten`, `beatWritten`, `beatWitnessed`, `ceremonyWritten`,
89
+ `diaryWritten`, `nodeWritten`, `conclusionJudged`, `relationWritten` and the
90
+ `*Removed` ones) are shared by the wheel's HTTP routes and the MCP server's
91
+ local store, so the same act leaves the same event whichever door it used.
92
+
93
+ ## Publishing
94
+
95
+ ```ts
96
+ import { drainLog, httpPublisher, rewind, publisherStatus } from '@medicine-wheel/events/publish';
97
+
98
+ await drainLog(logFile, httpPublisher({ url, token }));
99
+ ```
100
+
101
+ Episode 548 draws an event's path as Named → Sent → Arrived | Failed →
102
+ Replayed. An event in the log is Named. `drainLog` sends each event after the
103
+ publisher's cursor, in order, and writes the outcome to `events-receipts.jsonl`:
104
+
105
+ - `arrived`: a 2xx answer.
106
+ - `failed`: unreachable, timed out, 408, 429 or 5xx. The pass stops, and the
107
+ next one starts from the same event.
108
+ - `refused`: any other answer. It is recorded and passed, so one bad event does
109
+ not hold back the rest.
110
+
111
+ `rewind` is Replay: it moves the cursor back, and the same lines go out again
112
+ with the same idempotency keys. Each publisher keeps its own cursor
113
+ (`events-cursor.<name>.json`).
114
+
115
+ `httpPublisher` POSTs the envelope as JSON with `Idempotency-Key` and
116
+ `X-Event-Type` headers. Each of these can receive it: a webhook endpoint, a
117
+ Hookdeck source, a Cloudflare Worker in front of a queue, or the Miadi
118
+ hooks-gateway once it accepts this envelope. A broker client would be another
119
+ `Publisher` beside it. The events and their names stay the same.
120
+
121
+ In the wheel, set `MW_EVENTS_PUBLISH_URL` (and `MW_EVENTS_PUBLISH_TOKEN`) to turn
122
+ publishing on. `GET /api/health` then reports how many events are still owed.