@quo-systems/quo 0.1.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.
@@ -0,0 +1,339 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The estate. Every other chapter is a scene: two or three beings, one move,
3
+ // one answer. This one is a graph under churn.
4
+ //
5
+ // An organisation does not run one relation. It runs a few hundred, scattered
6
+ // over wards on scattered harbors, and they are not born together: partners
7
+ // are invited, employees knock, standings are taken, people are kicked, wards
8
+ // move to harbors of their own. Nothing is wrong at the second move. What
9
+ // goes wrong goes wrong at the hundredth, when a kick and a take and a
10
+ // migration have landed on top of each other, and no scene written by hand
11
+ // will ever be that sequence.
12
+ //
13
+ // So this chapter does not write the sequence. It keeps a model of the graph,
14
+ // plays legal moves against it from three fixed seeds, and after every move
15
+ // asks three things:
16
+ //
17
+ // the ledger is a graph every arc the wards hold closes on both ends
18
+ // the ledger is what it can do every arc in the model is asked, and answers
19
+ // exactly what being that arc means it answers
20
+ // the graph is not the topology the same seed, played under one ward, under
21
+ // a ward per being, and under two harbors,
22
+ // ends in the same graph
23
+ //
24
+ // The third is the one that matters most here. A being never learns where the
25
+ // other one is, so a script that is topology-invariant at forty moves is the
26
+ // promise of Quo held at a scale a scene cannot reach -- and it is what makes
27
+ // a migration honest: if nobody can tell the topology apart, moving a ward
28
+ // between harbors cannot be felt.
29
+ //
30
+ // A relation is two arcs and never one edge. The occupant arc is the host's,
31
+ // born at invite; the standing arc is the guest's, born at take, and only
32
+ // after an answer. `SPEC.md`: take binds B's side only, the knock bound hers.
33
+ // A model that kept one symmetric edge would be a wrong model, and would
34
+ // agree happily with a ward that had drifted.
35
+ import { assert } from './assert.ts';
36
+ import { isSilence, isUnreached } from '../being/silence.ts';
37
+ import type { Invitation } from '../being/types.ts';
38
+ import { Member } from './beings.ts';
39
+ import type { Census, Handle, Runner, World } from './index.ts';
40
+
41
+ const HEX64 = /^[0-9a-f]{64}$/;
42
+
43
+ // One occupant arc, as the host holds it. `guest` is null until somebody
44
+ // knocks; `live` goes false when she is kicked. Both are the host's business
45
+ // alone -- the guest is never told either one.
46
+ type Seat = { host: string; id: string; inv: Invitation; guest: string | null; answered: boolean; live: boolean; taken: boolean };
47
+ // One standing arc, as the guest holds it. It carries the seat it was born
48
+ // on, because what an ask on it should come back with is the host's name for
49
+ // her, and whether the host has since kicked her.
50
+ type Hold = { owner: string; id: string; seat: Seat; live: boolean };
51
+ type Graph = { seats: Seat[]; holds: Hold[] };
52
+
53
+ // A pseudo-random source in the language alone: the estate must replay, on
54
+ // every terrain, from a number a failure can print.
55
+ const rolls = (seed: number) => () => {
56
+ seed = (seed + 0x6d2b79f5) | 0;
57
+ let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
58
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
59
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
60
+ };
61
+
62
+ // The graph, with nothing of the world in it: no key, no ward pk, no harbor.
63
+ // Two runs under two topologies are the same estate when these agree.
64
+ const abc = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0);
65
+ const fingerprint = (g: Graph): string =>
66
+ JSON.stringify({
67
+ seats: g.seats.map((s) => JSON.stringify([s.host, s.id, s.guest, s.answered, s.live, s.taken])).sort(abc),
68
+ holds: g.holds.map((h) => JSON.stringify([h.owner, h.id, h.seat.host, h.seat.id, h.live])).sort(abc),
69
+ });
70
+
71
+ // Every run of one seed, under every topology, must land on the same graph.
72
+ // The topologies run in one process, so the first to finish records and the
73
+ // rest are held to it.
74
+ const landed = new Map<string, { by: string; print: string }>();
75
+
76
+ // ---- the ledger is a graph
77
+
78
+ // Held after every move. Each line is a way the bookkeeping could stop being
79
+ // a graph: an arc with one end, an arc with two owners, a relation filed in
80
+ // two places at once, a count that went backwards, a secret in the cells.
81
+ function ledger(c: Census, before: Census | undefined, where: string) {
82
+ const at = (why: string) => `${where}: ${why}`;
83
+ const heirsSeen = new Set<string>();
84
+
85
+ for (const [pk, w] of Object.entries(c.wards)) {
86
+ for (const [heir, r] of Object.entries(w.heirs)) {
87
+ assert.ok(!heirsSeen.has(heir), at(`heir ${heir} is held by two wards`));
88
+ heirsSeen.add(heir);
89
+ assert.match(heir, HEX64, at(`heir ${heir} is not a key`));
90
+ // the far end of the arc: the being the ward says minted it holds it back, under the same id
91
+ const b = c.binds[r.being];
92
+ assert.ok(b, at(`heir ${heir} names ${r.being}, who lives in no ward`));
93
+ assert.equal(b.ward, pk, at(`heir ${heir} is kept by ${pk}, but ${r.being} lives behind ${b.ward}`));
94
+ assert.equal(b.occupants[r.id], heir, at(`heir ${heir} says it is ${r.being}'s ${r.id}, who does not hold it`));
95
+ assert.ok(r.spent.every((n) => n <= r.mark), at(`heir ${heir} spent a number above its mark`));
96
+ }
97
+ }
98
+
99
+ for (const [key, b] of Object.entries(c.binds)) {
100
+ // every occupant id the being holds is a heir her own ward keeps
101
+ for (const [id, heir] of Object.entries(b.occupants)) {
102
+ const r = c.wards[b.ward].heirs[heir];
103
+ assert.ok(r, at(`${key}'s occupant ${id} names heir ${heir}, which her ward has forgotten`));
104
+ assert.equal(r.being, key, at(`${key}'s occupant ${id} names a heir of ${r.being}'s`));
105
+ assert.equal(r.id, id, at(`${key}'s occupant ${id} names a heir filed under ${r.id}`));
106
+ }
107
+ // a taken relation lives in the standing and never also in a knock record
108
+ for (const [id, s] of Object.entries(b.standings)) {
109
+ const name = s.heir === null ? `public:${s.ward}` : `${s.ward}:${s.heir}`;
110
+ assert.ok(!b.knocks.includes(name), at(`${key}'s standing ${id} still has a knock record`));
111
+ assert.ok(!b.answered.includes(name), at(`${key}'s standing ${id} is still marked answered`));
112
+ }
113
+ // no key her side holds is anywhere in anyone's cells
114
+ const cells = JSON.stringify(c.cells);
115
+ for (const word of b.secrets) assert.ok(!cells.includes(word), at(`a key of ${key}'s is in the cells`));
116
+ // counts climb and never fall. a door that forgot what it honoured honours it again
117
+ const was = before?.binds[key];
118
+ if (was)
119
+ for (const [id, s] of Object.entries(b.standings)) assert.ok(!was.standings[id] || s.seq >= was.standings[id].seq, at(`${key}'s standing ${id} counted backwards`));
120
+ }
121
+ if (before)
122
+ for (const [pk, w] of Object.entries(c.wards))
123
+ for (const [heir, r] of Object.entries(w.heirs)) {
124
+ const was = before.wards[pk]?.heirs[heir];
125
+ if (was) assert.ok(r.mark >= was.mark, at(`heir ${heir} marked backwards`));
126
+ }
127
+ }
128
+
129
+ // ---- the chapter
130
+
131
+ export function estate(label: string, make: () => Promise<World>, { canDown, test }: { canDown: boolean; test: Runner }) {
132
+ const t = (name: string, fn: () => Promise<void> | void) => test(`[${label}] ${name}`, {}, fn);
133
+
134
+ // The story, told once, by hand, so that the next person can read what the
135
+ // model is for. A partner is invited into a harbor she is borrowing, works,
136
+ // moves to a harbor of her own, is kicked, and is invited back as somebody
137
+ // else. Nothing here is random and nothing here is generated.
138
+ t('estate: a partner is invited into a borrowed harbor, works, moves to her own, is kicked, and comes back as somebody else', async () => {
139
+ const w = await make();
140
+ const acme = await w.boot<Member>('ACME', Member);
141
+ const partner = await w.boot<Member>('PARTNER', Member);
142
+
143
+ // 1. acme mints a name for her and asks her ward for an invitation. the
144
+ // occupant exists from that moment, and the partner knows nothing yet.
145
+ const first = (await acme.being.invite('partner-1'))!;
146
+ assert.deepEqual(Object.keys(acme.cells.occupants), ['partner-1']);
147
+ assert.deepEqual(partner.cells.standings, {});
148
+
149
+ // 2. she knocks. from now on every ask of hers arrives as that name.
150
+ assert.deepEqual(await partner.being.knock(first, 'ping'), { pong: 'partner-1' });
151
+ // 3. and only then does she keep acme, under a name of her own minting.
152
+ assert.equal(await partner.being.take('the-client', first), 'the-client');
153
+ assert.deepEqual(await partner.being.standings['the-client']!.ask('ping'), { pong: 'partner-1' });
154
+
155
+ // 4. she gets her own harbor. the same seed, the same pk, so the arc acme
156
+ // holds still points at her and the arc she holds still points at acme.
157
+ await w.migrate('PARTNER');
158
+ await w.migrate('ACME');
159
+ assert.deepEqual(await partner.being.standings['the-client']!.ask('ping'), { pong: 'partner-1' });
160
+ const back = (await partner.being.invite('acme'))!;
161
+ assert.deepEqual(await acme.being.knock(back, 'ping'), { pong: 'acme' });
162
+ assert.equal(await acme.being.take('them', back), 'them');
163
+
164
+ // 5. acme kicks her. her standing is still hers to hold -- she is never
165
+ // told -- and it is silence from here on.
166
+ acme.being.occupants.remove('partner-1');
167
+ assert.ok(isSilence(await partner.being.standings['the-client']!.ask('ping')));
168
+ assert.deepEqual(Object.keys(partner.cells.standings), ['the-client']); // she never learns
169
+ assert.deepEqual(await acme.being.standings.them!.ask('ping'), { pong: 'acme' }); // the other arc never moved
170
+
171
+ // 6. and back, as somebody else: a second invitation is a second relation,
172
+ // sharing nothing with the first. the old one stays dead.
173
+ const second = (await acme.being.invite('partner-2'))!;
174
+ assert.notEqual(second.heir, first.heir);
175
+ assert.deepEqual(await partner.being.knock(second, 'ping'), { pong: 'partner-2' });
176
+ assert.equal(await partner.being.take('the-client-again', second), 'the-client-again');
177
+ assert.ok(isSilence(await partner.being.standings['the-client']!.ask('ping')));
178
+ ledger(w.census(), undefined, 'the story');
179
+ });
180
+
181
+ for (const seed of [1, 2, 3]) {
182
+ t(`estate: sixty moves from seed ${seed}, the ledger a graph and every arc asked after each one`, async () => {
183
+ const w = await make();
184
+ const roll = rolls(seed);
185
+ const pick = <T>(xs: T[]): T => xs[Math.floor(roll() * xs.length)];
186
+ const hands: Record<string, Handle<Member>> = {};
187
+ const g: Graph = { seats: [], holds: [] };
188
+ let minted = 0;
189
+ const born = async (key: string) => (hands[key] = await w.boot<Member>(key, Member));
190
+ for (const key of ['M1', 'M2', 'M3']) await born(key);
191
+ let before: Census | undefined;
192
+
193
+ // Every move that is legal right now, as a thunk. A move that would not
194
+ // be legal is not written down, so the chapter never asserts on a
195
+ // question the spec does not answer.
196
+ const moves = (): (() => Promise<string>)[] => {
197
+ const keys = Object.keys(hands);
198
+ const out: (() => Promise<string>)[] = [];
199
+
200
+ // invite: a fresh occupant arc, and nothing of the guest's in it
201
+ out.push(async () => {
202
+ const host = pick(keys);
203
+ const id = `o${++minted}`;
204
+ const inv = (await hands[host].being.invite(id))!;
205
+ assert.equal(inv.ward, hands[host].pk);
206
+ assert.match(inv.heir!, HEX64);
207
+ g.seats.push({ host, id, inv, guest: null, answered: false, live: true, taken: false });
208
+ return `${host} invites ${id}`;
209
+ });
210
+
211
+ // knock: the guest an unspent seat gets is anyone but the host; a
212
+ // spent one only ever answers the one who spent it. Once she has
213
+ // taken it the invitation is not a knock any more -- it joins the
214
+ // standing's lane, and is exactly as alive as the standing is.
215
+ for (const s of g.seats) {
216
+ out.push(async () => {
217
+ const guest = s.guest ?? pick(keys.filter((k) => k !== s.host));
218
+ const hold = g.holds.find((h) => h.seat === s);
219
+ const back = await hands[guest].being.knock(s.inv, 'ping');
220
+ if (s.live && (s.guest === null || s.guest === guest) && (hold === undefined || hold.live)) {
221
+ assert.deepEqual(back, { pong: s.id }, `${guest} knocking ${s.host}/${s.id}`);
222
+ s.guest = guest;
223
+ s.answered = true;
224
+ } else assert.ok(isSilence(back), `${guest} knocking ${s.host}/${s.id} should be silence`);
225
+ return `${guest} knocks ${s.host}/${s.id}`;
226
+ });
227
+ // a stranger on a spent seat: silence, and nothing rebinds
228
+ if (s.guest !== null)
229
+ out.push(async () => {
230
+ const other = keys.filter((k) => k !== s.host && k !== s.guest);
231
+ if (!other.length) return 'no stranger to try';
232
+ const who = pick(other);
233
+ assert.ok(isSilence(await hands[who].being.knock(s.inv, 'ping')), `${who} should not get in on ${s.host}/${s.id}`);
234
+ return `${who} is refused at ${s.host}/${s.id}`;
235
+ });
236
+ }
237
+
238
+ // take: only after an answer, only once. the far side may have kicked
239
+ // her in between -- take reads the knock record, and still births one.
240
+ for (const s of g.seats.filter((s) => s.answered && !s.taken))
241
+ out.push(async () => {
242
+ const id = `s${++minted}`;
243
+ assert.equal(await hands[s.guest!].being.take(id, s.inv), id);
244
+ s.taken = true;
245
+ g.holds.push({ owner: s.guest!, id, seat: s, live: true });
246
+ assert.equal(await hands[s.guest!].being.take(`${id}-again`, s.inv), null); // spent for her
247
+ return `${s.guest} takes ${s.host}/${s.id} as ${id}`;
248
+ });
249
+
250
+ // drop: hers to drop, and the host is never told
251
+ for (const h of g.holds.filter((h) => h.live))
252
+ out.push(async () => {
253
+ hands[h.owner].being.standings.remove(h.id);
254
+ h.live = false;
255
+ assert.equal(hands[h.owner].being.standings[h.id], undefined);
256
+ return `${h.owner} drops ${h.id}`;
257
+ });
258
+
259
+ // kick: the host's, and the guest is never told either
260
+ for (const s of g.seats.filter((s) => s.live))
261
+ out.push(async () => {
262
+ hands[s.host].being.occupants.remove(s.id);
263
+ s.live = false;
264
+ return `${s.host} kicks ${s.id}`;
265
+ });
266
+
267
+ // a new being: an estate grows
268
+ if (keys.length < 6) out.push(async () => (await born(`M${keys.length + 1}`), `M${keys.length + 1} is booted`));
269
+
270
+ // a ward moves harbor, under everyone standing on it
271
+ out.push(async () => {
272
+ const who = pick(keys);
273
+ await w.migrate(who);
274
+ return `${who}'s ward moves harbor`;
275
+ });
276
+
277
+ // weather: the host's door is gone. unreached, never silence, and the
278
+ // relation is exactly where it was when it comes back.
279
+ // weather: the host's door is gone. unreached, never silence, and the
280
+ // relation is exactly where it was when it comes back. The move is on
281
+ // the list under every topology even where there is nothing to cut,
282
+ // because the list is the script: a move missing in one world would
283
+ // make it a different script, and the three could not be compared.
284
+ for (const h of g.holds.filter((h) => h.live && h.seat.live))
285
+ out.push(async () => {
286
+ if (!canDown) return 'nothing to cut inside one ward';
287
+ hands[h.seat.host].down = true;
288
+ assert.ok(isUnreached(await hands[h.owner].being.standings[h.id]!.ask('ping')), `${h.owner}/${h.id} should be unreached`);
289
+ hands[h.seat.host].down = false;
290
+ return `${h.seat.host} is cut and comes back`;
291
+ });
292
+
293
+ return out;
294
+ };
295
+
296
+ // Every arc in the model, asked. This is where the ledger stops being
297
+ // bookkeeping and becomes what the door actually does.
298
+ const sweep = async (step: number) => {
299
+ for (const h of g.holds) {
300
+ const standing = hands[h.owner].being.standings[h.id];
301
+ if (!h.live) {
302
+ assert.equal(standing, undefined, `step ${step}: ${h.owner}'s dropped ${h.id} is still there`);
303
+ continue;
304
+ }
305
+ const out = await standing!.ask('ping');
306
+ if (h.seat.live) assert.deepEqual(out, { pong: h.seat.id }, `step ${step}: ${h.owner}/${h.id} -> ${h.seat.host}/${h.seat.id}`);
307
+ else assert.ok(isSilence(out), `step ${step}: ${h.owner}/${h.id} is kicked and should be silence`);
308
+ }
309
+ };
310
+
311
+ // The trace is the seed's own account of itself: a red run prints the
312
+ // moves that got there, which is the only thing that makes a generated
313
+ // sequence debuggable at all.
314
+ const trace: string[] = [];
315
+ const at = (step: number) => `seed ${seed} step ${step}\n ${trace.join('\n ')}`;
316
+ for (let step = 1; step <= 60; step++) {
317
+ const move = pick(moves());
318
+ try {
319
+ trace.push(await move());
320
+ } catch (e) {
321
+ throw new Error(`${at(step)}\n -> ${(e as Error).message}`, { cause: e });
322
+ }
323
+ const c = w.census();
324
+ ledger(c, before, at(step));
325
+ before = c;
326
+ await sweep(step).catch((e: Error) => {
327
+ throw new Error(`${at(step)}\n -> ${e.message}`, { cause: e });
328
+ });
329
+ }
330
+
331
+ // and the estate that came out is the estate every topology comes out with
332
+ const print = fingerprint(g);
333
+ const key = `seed ${seed}`;
334
+ const first = landed.get(key);
335
+ if (!first) landed.set(key, { by: label, print });
336
+ else assert.equal(print, first.print, `the estate under ${label} is not the estate under ${first.by}: a being learned the topology`);
337
+ });
338
+ }
339
+ }