@medicine-wheel/events 0.0.0-stage → 0.19.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 +181 -2
- package/dist/catalog.d.ts +846 -0
- package/dist/catalog.d.ts.map +1 -0
- package/dist/catalog.js +311 -0
- package/dist/catalog.js.map +1 -0
- package/dist/cli.d.ts +24 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +167 -0
- package/dist/cli.js.map +1 -0
- package/dist/decide.d.ts +116 -0
- package/dist/decide.d.ts.map +1 -0
- package/dist/decide.js +293 -0
- package/dist/decide.js.map +1 -0
- package/dist/envelope.d.ts +81 -0
- package/dist/envelope.d.ts.map +1 -0
- package/dist/envelope.js +80 -0
- package/dist/envelope.js.map +1 -0
- package/dist/git.d.ts +75 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +197 -0
- package/dist/git.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +60 -0
- package/dist/index.js.map +1 -0
- package/dist/log.d.ts +18 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +107 -0
- package/dist/log.js.map +1 -0
- package/dist/publish.d.ts +117 -0
- package/dist/publish.d.ts.map +1 -0
- package/dist/publish.js +242 -0
- package/dist/publish.js.map +1 -0
- package/dist/schema.d.ts +31 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +175 -0
- package/dist/schema.js.map +1 -0
- package/package.json +72 -4
- package/schema/asyncapi.json +2842 -0
- package/schema/wheel-events.schema.json +1968 -0
package/README.md
CHANGED
|
@@ -1,3 +1,182 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @medicine-wheel/events
|
|
2
2
|
|
|
3
|
-
|
|
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. A cycle's
|
|
28
|
+
question, a beat's prose and a diary entry's text stay in the wheel. To read
|
|
29
|
+
one, fetch the record by `subject.id` from the wheel that wrote the event
|
|
30
|
+
(`source.instance`).
|
|
31
|
+
|
|
32
|
+
There is one exception. When two chronicle episodes are related, the
|
|
33
|
+
`chronicle.lineage.*` events carry the sentence that says why
|
|
34
|
+
(`payload.relation`). That sentence is already committed to the episode's
|
|
35
|
+
`episode.yaml`, so the event repeats it rather than releasing it. These
|
|
36
|
+
events say so with `privacy: { classification: "chronicle", content_included: true }`.
|
|
37
|
+
|
|
38
|
+
## The envelope
|
|
39
|
+
|
|
40
|
+
jgwill/Miadi's `MiadiEventEnvelope` 0.1.0
|
|
41
|
+
(`rispecs/events/00-miadi-event-spine.rispec.md`):
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"id": "mwev:1792019860666:4c509d0b",
|
|
46
|
+
"schema_version": "0.1.0",
|
|
47
|
+
"event_type": "medicine-wheel.cycle.opened",
|
|
48
|
+
"source": { "kind": "miadi", "system": "medicine-wheel", "instance": "ilex:8040" },
|
|
49
|
+
"subject": { "kind": "cycle", "id": "cycle-1792019860642-hzqvkk" },
|
|
50
|
+
"occurred_at": "2026-10-14T23:17:40.666Z",
|
|
51
|
+
"observed_at": "2026-10-14T23:17:40.666Z",
|
|
52
|
+
"correlation_id": "cycle-1792019860642-hzqvkk",
|
|
53
|
+
"idempotency_key": "mwev:1792019860666:4c509d0b",
|
|
54
|
+
"privacy": { "classification": "internal", "content_included": false },
|
|
55
|
+
"payload": { "current_direction": "east" }
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
- `idempotency_key` equals `id`. A resend of the same line is the same
|
|
60
|
+
occurrence; a receiver that keeps keys recognises it.
|
|
61
|
+
- `correlation_id` groups: a cycle and the beats recorded in it, a ceremony and
|
|
62
|
+
its closing, a conclusion and the person it is about.
|
|
63
|
+
- An amendment names what moved in `payload.changed`. A write that changes
|
|
64
|
+
nothing writes no event.
|
|
65
|
+
|
|
66
|
+
## The events
|
|
67
|
+
|
|
68
|
+
| family | events |
|
|
69
|
+
|---|---|
|
|
70
|
+
| cycle | `cycle.opened`, `cycle.amended`, `cycle.archived` |
|
|
71
|
+
| beat | `beat.recorded`, `beat.amended`, `beat.witnessed` |
|
|
72
|
+
| ceremony | `ceremony.opened`, `ceremony.closed`, `ceremony.amended`, `ceremony.removed` |
|
|
73
|
+
| diary | `diary.written`, `diary.amended`, `diary.removed` |
|
|
74
|
+
| circle | `circle.created`, `circle.amended`, `circle.removed` |
|
|
75
|
+
| chronicle episode | `chronicle.episode.registered`, `chronicle.episode.amended`, `chronicle.episode.removed` |
|
|
76
|
+
| memory conclusion | `memory.conclusion.recorded`, `memory.conclusion.judged`, `memory.conclusion.amended`, `memory.conclusion.removed` |
|
|
77
|
+
| node | `node.created`, `node.amended`, `node.removed` |
|
|
78
|
+
| chronicle lineage | `chronicle.lineage.made`, `chronicle.lineage.amended`, `chronicle.lineage.removed` — a relation between two chronicle episodes, with the why |
|
|
79
|
+
| relation | `relation.made`, `relation.amended`, `relation.removed` |
|
|
80
|
+
|
|
81
|
+
`WHEEL_EVENT_CATALOG` says, for each: what happened, which write produces it,
|
|
82
|
+
what its correlation groups, and every payload field. `PayloadOf<'beat.recorded'>`
|
|
83
|
+
is its payload type.
|
|
84
|
+
|
|
85
|
+
## For a producer
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
import { buildEvent, cycleWritten } from '@medicine-wheel/events';
|
|
89
|
+
import { appendEvent, eventLogPath } from '@medicine-wheel/events/log';
|
|
90
|
+
|
|
91
|
+
const input = cycleWritten(before, after, Object.keys(patch)); // null when nothing moved
|
|
92
|
+
if (input) appendEvent(eventLogPath(dataDir), buildEvent(input, { instance: process.env.MW_INSTANCE }));
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The deciders (`cycleWritten`, `beatWritten`, `beatWitnessed`, `ceremonyWritten`,
|
|
96
|
+
`diaryWritten`, `nodeWritten`, `conclusionJudged`, `relationWritten` and the
|
|
97
|
+
`*Removed` ones) are shared by the wheel's HTTP routes and the MCP server's
|
|
98
|
+
local store, so the same act leaves the same event whichever door it used.
|
|
99
|
+
|
|
100
|
+
## Carried by git
|
|
101
|
+
|
|
102
|
+
Before any broker exists, the log can travel through the data repository it
|
|
103
|
+
already lives in. This is not a publisher: nothing here sends one event and
|
|
104
|
+
waits for a receipt.
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import { commitEventLog, followEventLog } from '@medicine-wheel/events/git';
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**On the wheel's side,** `commitEventLog` commits `events.jsonl` and nothing
|
|
111
|
+
else. Anything else that is changed or staged stays exactly as it was. It then
|
|
112
|
+
pushes. It pushes only commits that touch the log alone. When the branch also
|
|
113
|
+
holds someone's own data commit, it holds the push and says why. A log the
|
|
114
|
+
repository ignores is reported as an error, not committed by force.
|
|
115
|
+
|
|
116
|
+
In the wheel, set `MW_EVENTS_GIT_SYNC=push` (or `commit`). The wheel then
|
|
117
|
+
commits:
|
|
118
|
+
|
|
119
|
+
- about 30 seconds after events are recorded, so a burst travels as one commit;
|
|
120
|
+
- on start;
|
|
121
|
+
- every 10 minutes.
|
|
122
|
+
|
|
123
|
+
`mw events sync` commits now. `GET /api/events/git` shows the last pass.
|
|
124
|
+
|
|
125
|
+
**Anywhere else,** clone the data repository, then follow it:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
npx -p @medicine-wheel/events mw-events follow ~/mw-data --exec ./on-event.sh
|
|
129
|
+
npx -p @medicine-wheel/events mw-events follow ~/mw-data --every 60 # pull every minute, print new events
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`follow` pulls, then hands each event this follower has not handled yet to
|
|
133
|
+
`--exec`, oldest first:
|
|
134
|
+
|
|
135
|
+
- The event's JSON goes on stdin.
|
|
136
|
+
- `MW_EVENT_ID`, `MW_EVENT_TYPE`, `MW_EVENT_SUBJECT_KIND`, `MW_EVENT_SUBJECT_ID`,
|
|
137
|
+
`MW_EVENT_CORRELATION` and `MW_EVENT_OBSERVED_AT` go in the environment.
|
|
138
|
+
- Exit 0 and the follower moves past that event. Anything else and it stops,
|
|
139
|
+
and the same event comes first next time.
|
|
140
|
+
|
|
141
|
+
The cursor lives in `--state-dir`, which defaults to
|
|
142
|
+
`~/.local/state/medicine-wheel-events`, never in the clone. Use `--name` to give
|
|
143
|
+
two followers of one clone separate places.
|
|
144
|
+
|
|
145
|
+
## Publishing
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
import { drainLog, httpPublisher, rewind, publisherStatus } from '@medicine-wheel/events/publish';
|
|
149
|
+
|
|
150
|
+
await drainLog(logFile, httpPublisher({ url, token }));
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Episode 548 draws an event's path as Named → Sent → Arrived | Failed →
|
|
154
|
+
Replayed. An event in the log is Named. `drainLog` sends each event after the
|
|
155
|
+
publisher's cursor, in order, and writes the outcome to `events-receipts.jsonl`:
|
|
156
|
+
|
|
157
|
+
- `arrived`: a 2xx answer.
|
|
158
|
+
- `failed`: unreachable, timed out, 408, 429 or 5xx. The pass stops, and the
|
|
159
|
+
next one starts from the same event.
|
|
160
|
+
- `refused`: any other answer. It is recorded and passed, so one bad event does
|
|
161
|
+
not hold back the rest.
|
|
162
|
+
|
|
163
|
+
`rewind` is Replay: it moves the cursor back, and the same lines go out again
|
|
164
|
+
with the same idempotency keys. Each publisher keeps its own cursor
|
|
165
|
+
(`events-cursor.<name>.json`).
|
|
166
|
+
|
|
167
|
+
`httpPublisher` POSTs the envelope as JSON with `Idempotency-Key` and
|
|
168
|
+
`X-Event-Type` headers. Each of these can receive it: a webhook endpoint, a
|
|
169
|
+
Hookdeck source, a Cloudflare Worker in front of a queue, or the Miadi
|
|
170
|
+
hooks-gateway once it accepts this envelope. A broker client would be another
|
|
171
|
+
`Publisher` beside it. The events and their names stay the same.
|
|
172
|
+
|
|
173
|
+
In the wheel, set `MW_EVENTS_PUBLISH_URL` (and `MW_EVENTS_PUBLISH_TOKEN`) to turn
|
|
174
|
+
publishing on. `GET /api/health` then reports how many events are still owed.
|
|
175
|
+
|
|
176
|
+
## Foundations
|
|
177
|
+
|
|
178
|
+
[docs/foundations/event-driven-architecture.md](../../docs/foundations/event-driven-architecture.md)
|
|
179
|
+
covers the literature behind this package and maps each idea to the code:
|
|
180
|
+
notification versus event-carried state, the log, the outbox, idempotent
|
|
181
|
+
receivers, dead letters and AsyncAPI. It also covers why the logic lives in the
|
|
182
|
+
wheel and Miadi consumes it.
|