nervur 0.19.2 → 0.20.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.
Files changed (205) hide show
  1. package/README.md +66 -101
  2. package/dist/being/being.d.ts +7 -15
  3. package/dist/being/being.js +16 -67
  4. package/dist/being/digest.d.ts +1 -1
  5. package/dist/being/digest.js +7 -27
  6. package/dist/being/faculty.d.ts +9 -0
  7. package/dist/being/faculty.js +68 -0
  8. package/dist/being/index.d.ts +4 -5
  9. package/dist/being/index.js +5 -5
  10. package/dist/being/types.d.ts +35 -62
  11. package/dist/being/types.js +3 -57
  12. package/dist/being/words.d.ts +13 -0
  13. package/dist/being/words.js +14 -0
  14. package/dist/contract/index.d.ts +26 -0
  15. package/dist/contract/index.js +19 -0
  16. package/dist/crypto/aes.d.ts +3 -0
  17. package/dist/crypto/aes.js +24 -0
  18. package/dist/crypto/bytes.d.ts +6 -0
  19. package/dist/crypto/bytes.js +33 -0
  20. package/dist/crypto/ed25519.d.ts +3 -0
  21. package/dist/crypto/ed25519.js +85 -0
  22. package/dist/crypto/hash.d.ts +2 -0
  23. package/dist/crypto/hash.js +11 -0
  24. package/dist/crypto/index.d.ts +7 -0
  25. package/dist/crypto/index.js +10 -0
  26. package/dist/crypto/json.d.ts +8 -0
  27. package/dist/crypto/json.js +250 -0
  28. package/dist/crypto/mlkem.d.ts +12 -0
  29. package/dist/crypto/mlkem.js +36 -0
  30. package/dist/crypto/subtle.d.ts +10 -0
  31. package/dist/crypto/subtle.js +29 -0
  32. package/dist/crypto/x25519.d.ts +2 -0
  33. package/dist/crypto/x25519.js +21 -0
  34. package/dist/folder/index.d.ts +13 -0
  35. package/dist/folder/index.js +125 -0
  36. package/dist/harbor/dock.d.ts +19 -0
  37. package/dist/harbor/dock.js +92 -0
  38. package/dist/harbor/harbor.d.ts +22 -0
  39. package/dist/harbor/harbor.js +84 -0
  40. package/dist/harbor/index.d.ts +4 -10
  41. package/dist/harbor/index.js +5 -18
  42. package/dist/harbor/registry.d.ts +7 -0
  43. package/dist/harbor/registry.js +13 -0
  44. package/dist/harbor/terrain.d.ts +8 -0
  45. package/dist/harbor/terrain.js +2 -0
  46. package/dist/index.d.ts +7 -0
  47. package/dist/index.js +14 -0
  48. package/dist/pointer/bodies.d.ts +20 -0
  49. package/dist/pointer/bodies.js +45 -0
  50. package/dist/pointer/index.d.ts +2 -0
  51. package/dist/pointer/index.js +4 -0
  52. package/dist/pointer/world.d.ts +34 -0
  53. package/dist/pointer/world.js +76 -0
  54. package/dist/quo/door.d.ts +34 -0
  55. package/dist/quo/door.js +172 -0
  56. package/dist/quo/index.d.ts +8 -0
  57. package/dist/quo/index.js +11 -0
  58. package/dist/quo/invitation.d.ts +7 -0
  59. package/dist/quo/invitation.js +15 -0
  60. package/dist/quo/keys.d.ts +40 -0
  61. package/dist/quo/keys.js +79 -0
  62. package/dist/quo/payload.d.ts +15 -0
  63. package/dist/quo/payload.js +55 -0
  64. package/dist/quo/relations.d.ts +40 -0
  65. package/dist/quo/relations.js +33 -0
  66. package/dist/quo/reply.d.ts +13 -0
  67. package/dist/quo/reply.js +35 -0
  68. package/dist/quo/seal.d.ts +42 -0
  69. package/dist/quo/seal.js +78 -0
  70. package/dist/quo/standing.d.ts +39 -0
  71. package/dist/quo/standing.js +90 -0
  72. package/dist/ward/allowance.d.ts +13 -9
  73. package/dist/ward/allowance.js +28 -67
  74. package/dist/ward/cells.d.ts +6 -6
  75. package/dist/ward/cells.js +67 -179
  76. package/dist/ward/index.d.ts +5 -10
  77. package/dist/ward/index.js +6 -13
  78. package/dist/ward/partition.d.ts +44 -60
  79. package/dist/ward/partition.js +56 -285
  80. package/dist/ward/stance.d.ts +29 -20
  81. package/dist/ward/stance.js +173 -406
  82. package/dist/ward/ward-being.d.ts +33 -0
  83. package/dist/ward/ward-being.js +76 -0
  84. package/dist/ward/ward.d.ts +43 -11
  85. package/dist/ward/ward.js +215 -363
  86. package/package.json +19 -33
  87. package/src/being/being.ts +31 -78
  88. package/src/being/digest.ts +7 -35
  89. package/src/being/faculty.ts +66 -0
  90. package/src/being/index.ts +5 -6
  91. package/src/being/types.ts +47 -145
  92. package/src/being/words.ts +31 -0
  93. package/src/contract/index.ts +44 -0
  94. package/src/crypto/aes.ts +26 -0
  95. package/src/crypto/bytes.ts +37 -0
  96. package/src/crypto/ed25519.ts +84 -0
  97. package/src/crypto/hash.ts +14 -0
  98. package/src/crypto/index.ts +10 -0
  99. package/src/crypto/json.ts +241 -0
  100. package/src/crypto/mlkem.ts +38 -0
  101. package/src/crypto/subtle.ts +33 -0
  102. package/src/crypto/x25519.ts +21 -0
  103. package/src/folder/index.ts +134 -0
  104. package/src/harbor/dock.ts +101 -0
  105. package/src/harbor/harbor.ts +105 -0
  106. package/src/harbor/index.ts +5 -19
  107. package/src/harbor/registry.ts +20 -0
  108. package/src/harbor/terrain.ts +13 -0
  109. package/src/index.ts +20 -0
  110. package/src/pointer/bodies.ts +47 -0
  111. package/src/pointer/index.ts +4 -0
  112. package/src/pointer/world.ts +91 -0
  113. package/src/quo/door.ts +178 -0
  114. package/src/quo/index.ts +11 -0
  115. package/src/quo/invitation.ts +15 -0
  116. package/src/quo/keys.ts +91 -0
  117. package/src/quo/payload.ts +61 -0
  118. package/src/quo/relations.ts +62 -0
  119. package/src/quo/reply.ts +38 -0
  120. package/src/quo/seal.ts +101 -0
  121. package/src/quo/standing.ts +111 -0
  122. package/src/stand/main.ts +19 -0
  123. package/src/stand/stand.ts +178 -0
  124. package/src/ward/allowance.ts +37 -75
  125. package/src/ward/cells.ts +63 -176
  126. package/src/ward/index.ts +6 -17
  127. package/src/ward/partition.ts +86 -326
  128. package/src/ward/stance.ts +185 -420
  129. package/src/ward/ward-being.ts +97 -0
  130. package/src/ward/ward.ts +229 -363
  131. package/dist/being/lent.d.ts +0 -32
  132. package/dist/being/lent.js +0 -72
  133. package/dist/being/silence.d.ts +0 -12
  134. package/dist/being/silence.js +0 -41
  135. package/dist/conformance/assert.d.ts +0 -11
  136. package/dist/conformance/assert.js +0 -106
  137. package/dist/conformance/beings.d.ts +0 -199
  138. package/dist/conformance/beings.js +0 -188
  139. package/dist/conformance/estate.d.ts +0 -5
  140. package/dist/conformance/estate.js +0 -388
  141. package/dist/conformance/index.d.ts +0 -80
  142. package/dist/conformance/index.js +0 -819
  143. package/dist/conformance/reach.d.ts +0 -10
  144. package/dist/conformance/reach.js +0 -72
  145. package/dist/conformance/store.d.ts +0 -5
  146. package/dist/conformance/store.js +0 -113
  147. package/dist/harbor/box.d.ts +0 -121
  148. package/dist/harbor/box.js +0 -121
  149. package/dist/harbor/core.d.ts +0 -55
  150. package/dist/harbor/core.js +0 -662
  151. package/dist/harbor/dial.d.ts +0 -9
  152. package/dist/harbor/dial.js +0 -81
  153. package/dist/harbor/memory.d.ts +0 -29
  154. package/dist/harbor/memory.js +0 -104
  155. package/dist/harbor/reach.d.ts +0 -36
  156. package/dist/harbor/reach.js +0 -199
  157. package/dist/harbor/store.d.ts +0 -35
  158. package/dist/harbor/store.js +0 -62
  159. package/dist/vector/cases.d.ts +0 -42
  160. package/dist/vector/cases.js +0 -223
  161. package/dist/vector/index.d.ts +0 -6
  162. package/dist/vector/index.js +0 -8
  163. package/dist/vector/stand.d.ts +0 -9
  164. package/dist/vector/stand.js +0 -77
  165. package/dist/vector/world.d.ts +0 -144
  166. package/dist/vector/world.js +0 -209
  167. package/dist/ward/arithmetic.d.ts +0 -31
  168. package/dist/ward/arithmetic.js +0 -249
  169. package/dist/ward/door.d.ts +0 -18
  170. package/dist/ward/door.js +0 -186
  171. package/dist/ward/ground.d.ts +0 -29
  172. package/dist/ward/ground.js +0 -56
  173. package/dist/ward/heirs.d.ts +0 -13
  174. package/dist/ward/heirs.js +0 -117
  175. package/dist/ward/json.d.ts +0 -2
  176. package/dist/ward/json.js +0 -163
  177. package/dist/ward/owner.d.ts +0 -13
  178. package/dist/ward/owner.js +0 -220
  179. package/dist/ward/seal.d.ts +0 -52
  180. package/dist/ward/seal.js +0 -150
  181. package/src/being/lent.ts +0 -72
  182. package/src/being/silence.ts +0 -46
  183. package/src/conformance/assert.ts +0 -100
  184. package/src/conformance/beings.ts +0 -188
  185. package/src/conformance/estate.ts +0 -412
  186. package/src/conformance/index.ts +0 -965
  187. package/src/conformance/reach.ts +0 -83
  188. package/src/conformance/store.ts +0 -125
  189. package/src/harbor/box.ts +0 -131
  190. package/src/harbor/core.ts +0 -699
  191. package/src/harbor/dial.ts +0 -112
  192. package/src/harbor/memory.ts +0 -123
  193. package/src/harbor/reach.ts +0 -221
  194. package/src/harbor/store.ts +0 -91
  195. package/src/vector/cases.ts +0 -257
  196. package/src/vector/index.ts +0 -11
  197. package/src/vector/stand.ts +0 -76
  198. package/src/vector/world.ts +0 -232
  199. package/src/ward/arithmetic.ts +0 -251
  200. package/src/ward/door.ts +0 -186
  201. package/src/ward/ground.ts +0 -142
  202. package/src/ward/heirs.ts +0 -116
  203. package/src/ward/json.ts +0 -144
  204. package/src/ward/owner.ts +0 -214
  205. package/src/ward/seal.ts +0 -178
@@ -1,412 +0,0 @@
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 two 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
- //
21
- // The second is asked under one ward, under a ward per being, and under two
22
- // harbors, and that is where the promise of Quo is held: a being never learns
23
- // where the other one is, so every answer, after every move, under every
24
- // topology, is the answer the model wrote without knowing the topology
25
- // either. Held after every one of sixty moves, it is the promise at a scale a scene
26
- // cannot reach, and it is what makes a migration honest: if no answer can
27
- // tell the topology apart, moving a ward between harbors cannot be felt.
28
- // The model itself is the script's own bookkeeping and takes nothing from a
29
- // ward, so it is never compared with itself across topologies: that would
30
- // hold by construction and prove nothing.
31
- //
32
- // A relation is two arcs and never one edge. The occupant arc is the host's,
33
- // born at invite; the standing arc is the guest's, born at take, and only
34
- // after an answer. `SPEC.md`: take binds B's side only, the knock bound hers.
35
- // A model that kept one symmetric edge would be a wrong model, and would
36
- // agree happily with a ward that had drifted.
37
- import { assert } from './assert.ts';
38
- import { isSilence, isUnreached, told } from '../being/silence.ts';
39
- import type { Invitation } from '../being/types.ts';
40
- import { Member } from './beings.ts';
41
- import { HEX64 } from './index.ts';
42
- import type { Census, Handle, Runner, World } from './index.ts';
43
-
44
- const said = (x: unknown, w: string): boolean => told(x) === w;
45
-
46
- // One occupant arc, as the host holds it. `guest` is null until somebody
47
- // knocks; `live` goes false when she is kicked. Both are the host's business
48
- // alone -- the guest is never told either one.
49
- type Seat = { host: string; id: string; inv: Invitation; guest: string | null; answered: boolean; live: boolean; taken: boolean };
50
- // One standing arc, as the guest holds it. It carries the seat it was born
51
- // on, because what an ask on it should come back with is the host's name for
52
- // her, and whether the host has since kicked her.
53
- type Hold = { owner: string; id: string; seat: Seat; live: boolean };
54
- type Graph = { seats: Seat[]; holds: Hold[] };
55
-
56
- // A pseudo-random source in the language alone: the estate must replay, on
57
- // every terrain, from a number a failure can print.
58
- const rolls = (seed: number) => () => {
59
- seed = (seed + 0x6d2b79f5) | 0;
60
- let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
61
- t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
62
- return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
63
- };
64
-
65
- // ---- the ledger is a graph
66
-
67
- // Held after every move. Each line is a way the bookkeeping could stop being
68
- // a graph: an arc with one end, an arc with two owners, a relation filed in
69
- // two places at once, a count that went backwards, a secret in the cells.
70
- function ledger(c: Census, before: Census | undefined, where: string, g?: Graph) {
71
- const at = (why: string) => `${where}: ${why}`;
72
- const heirsSeen = new Set<string>();
73
-
74
- // What the model knows, which the ward's own bookkeeping cannot tell it.
75
- // Without this the ledger only checks the ward against itself, and a drift
76
- // is caught late, if the sweep happens to ask the affected arc, or never.
77
- if (g) {
78
- for (const s of g.seats) {
79
- const heir = c.wards[c.binds[s.host].ward].heirs[s.inv.heir!];
80
- if (!s.live) {
81
- assert.ok(!heir, at(`${s.host} kicked ${s.id} and the heir is still held`));
82
- assert.ok(!(s.id in c.binds[s.host].occupants), at(`${s.host} kicked ${s.id} and still holds the occupant`));
83
- continue;
84
- }
85
- assert.ok(heir, at(`${s.host}'s live seat ${s.id} has no heir`));
86
- assert.equal(heir.id, s.id, at(`the heir for ${s.host}/${s.id} is filed under ${heir.id}`));
87
- // A seat nobody has knocked is a heir that has never spoken: fresh, and
88
- // at zero. One that answered has spoken, so it is neither.
89
- assert.equal(heir.fresh, !s.answered, at(`${s.host}/${s.id} is ${s.answered ? 'answered' : 'unknocked'} and its heir says fresh=${heir.fresh}`));
90
- assert.equal(heir.mark === 0, !s.answered, at(`${s.host}/${s.id} is ${s.answered ? 'answered' : 'unknocked'} and its heir is at ${heir.mark}`));
91
- }
92
- for (const h of g.holds) {
93
- const standing = c.binds[h.owner].standings[h.id];
94
- if (!h.live) {
95
- assert.ok(!standing, at(`${h.owner} dropped ${h.id} and the standing is still there`));
96
- continue;
97
- }
98
- assert.ok(standing, at(`${h.owner}'s live standing ${h.id} is gone from the bind table`));
99
- assert.equal(standing.heir, h.seat.inv.heir, at(`${h.owner}/${h.id} names a heir that is not the seat it was born on`));
100
- // The count she has spent, against the number the far door honoured.
101
- // Her side is never behind: she takes a number, then it crosses.
102
- const far = c.wards[c.binds[h.seat.host].ward].heirs[h.seat.inv.heir!];
103
- if (far) assert.ok(standing.seq >= far.mark, at(`${h.owner}/${h.id} counted ${standing.seq} and the far door has honoured ${far.mark}`));
104
- }
105
- // Every seat and hold the model knows about, and no relation the ward
106
- // holds that the model never made.
107
- const mine = new Set(g.seats.filter((s) => s.live).map((s) => `${s.host}/${s.id}`));
108
- for (const [key, b] of Object.entries(c.binds)) for (const id of Object.keys(b.occupants)) assert.ok(mine.has(`${key}/${id}`), at(`${key} holds an occupant ${id} the model never made`));
109
- }
110
-
111
- for (const [pk, w] of Object.entries(c.wards)) {
112
- for (const [heir, r] of Object.entries(w.heirs)) {
113
- assert.ok(!heirsSeen.has(heir), at(`heir ${heir} is held by two wards`));
114
- heirsSeen.add(heir);
115
- assert.match(heir, HEX64, at(`heir ${heir} is not a key`));
116
- // the far end of the arc: the being the ward says minted it holds it back, under the same id
117
- const b = c.binds[r.being];
118
- assert.ok(b, at(`heir ${heir} names ${r.being}, who lives in no ward`));
119
- assert.equal(b.ward, pk, at(`heir ${heir} is kept by ${pk}, but ${r.being} lives behind ${b.ward}`));
120
- assert.equal(b.occupants[r.id], heir, at(`heir ${heir} says it is ${r.being}'s ${r.id}, who does not hold it`));
121
- assert.ok(r.spent.every((n) => n <= r.mark), at(`heir ${heir} spent a number above its mark`));
122
- }
123
- }
124
-
125
- for (const [key, b] of Object.entries(c.binds)) {
126
- // every occupant id the being holds is a heir her own ward keeps
127
- for (const [id, heir] of Object.entries(b.occupants)) {
128
- const r = c.wards[b.ward].heirs[heir];
129
- assert.ok(r, at(`${key}'s occupant ${id} names heir ${heir}, which her ward has forgotten`));
130
- assert.equal(r.being, key, at(`${key}'s occupant ${id} names a heir of ${r.being}'s`));
131
- assert.equal(r.id, id, at(`${key}'s occupant ${id} names a heir filed under ${r.id}`));
132
- }
133
- // a taken relation lives in the standing and never also in a knock record
134
- for (const [id, s] of Object.entries(b.standings)) {
135
- const name = s.heir === null ? `public:${s.ward}` : `${s.ward}:${s.heir}`;
136
- assert.ok(!b.knocks.includes(name), at(`${key}'s standing ${id} still has a knock record`));
137
- assert.ok(!b.answered.includes(name), at(`${key}'s standing ${id} is still marked answered`));
138
- }
139
- // no key her side holds is anywhere in anyone's cells
140
- const cells = JSON.stringify(c.cells);
141
- for (const word of b.secrets) assert.ok(!cells.includes(word), at(`a key of ${key}'s is in the cells`));
142
- // counts climb and never fall. a door that forgot what it honoured honours it again
143
- const was = before?.binds[key];
144
- if (was)
145
- 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`));
146
- }
147
- if (before)
148
- for (const [pk, w] of Object.entries(c.wards))
149
- for (const [heir, r] of Object.entries(w.heirs)) {
150
- const was = before.wards[pk]?.heirs[heir];
151
- if (was) assert.ok(r.mark >= was.mark, at(`heir ${heir} marked backwards`));
152
- }
153
- }
154
-
155
- // ---- the chapter
156
-
157
- export function estate(label: string, make: () => Promise<World>, { canDown, test }: { canDown: boolean; test: Runner }) {
158
- const t = (name: string, fn: () => Promise<void> | void) => test(`[${label}] ${name}`, {}, fn);
159
-
160
- // The story, told once, by hand, so that the next person can read what the
161
- // model is for. A partner is invited into a harbor she is borrowing, works,
162
- // moves to a harbor of her own, is kicked, and is invited back as somebody
163
- // else. Nothing here is random and nothing here is generated.
164
- t('estate: a partner is invited into a borrowed harbor, works, moves to her own, is kicked, and comes back as somebody else', async () => {
165
- const w = await make();
166
- const acme = await w.boot<Member>('ACME', Member);
167
- const partner = await w.boot<Member>('PARTNER', Member);
168
-
169
- // 1. acme mints a name for her and asks her ward for an invitation. the
170
- // occupant exists from that moment, and the partner knows nothing yet.
171
- const first = (await acme.being.invite('partner-1'))!;
172
- assert.deepEqual(Object.keys(acme.cells.occupants), ['partner-1']);
173
- assert.deepEqual(partner.cells.standings, {});
174
-
175
- // 2. she knocks. from now on every ask of hers arrives as that name.
176
- assert.deepEqual(await partner.being.knock(first, 'ping'), { pong: 'partner-1' });
177
- // 3. and only then does she keep acme, under a name of her own minting.
178
- assert.equal(await partner.being.take('the-client', first), 'the-client');
179
- assert.deepEqual(await partner.being.standings['the-client']!.ask('ping'), { pong: 'partner-1' });
180
-
181
- // 4. she gets her own harbor. the same seed, the same pk, so the arc acme
182
- // holds still points at her and the arc she holds still points at acme.
183
- await w.migrate('PARTNER');
184
- await w.migrate('ACME');
185
- assert.deepEqual(await partner.being.standings['the-client']!.ask('ping'), { pong: 'partner-1' });
186
- const back = (await partner.being.invite('acme'))!;
187
- assert.deepEqual(await acme.being.knock(back, 'ping'), { pong: 'acme' });
188
- assert.equal(await acme.being.take('them', back), 'them');
189
-
190
- // 5. acme kicks her. her standing is still hers to hold, and she is not
191
- // told until she asks: then the door, under the key it bound for her,
192
- // says removed, and only to her.
193
- acme.being.occupants.remove('partner-1');
194
- assert.ok(said(await partner.being.standings['the-client']!.ask('ping'), 'removed'));
195
- assert.deepEqual(Object.keys(partner.cells.standings), ['the-client']); // she never learns
196
- assert.deepEqual(await acme.being.standings.them!.ask('ping'), { pong: 'acme' }); // the other arc never moved
197
-
198
- // 6. and back, as somebody else: a second invitation is a second relation,
199
- // sharing nothing with the first. the old one stays dead.
200
- const second = (await acme.being.invite('partner-2'))!;
201
- assert.notEqual(second.heir, first.heir);
202
- assert.deepEqual(await partner.being.knock(second, 'ping'), { pong: 'partner-2' });
203
- assert.equal(await partner.being.take('the-client-again', second), 'the-client-again');
204
- assert.ok(said(await partner.being.standings['the-client']!.ask('ping'), 'removed'));
205
- ledger(w.census(), undefined, 'the story');
206
- });
207
-
208
- for (const seed of [1, 2, 3]) {
209
- t(`estate: sixty moves from seed ${seed}, the ledger a graph and every arc asked after each one`, async () => {
210
- const w = await make();
211
- const roll = rolls(seed);
212
- const pick = <T>(xs: T[]): T => xs[Math.floor(roll() * xs.length)];
213
- const hands: Record<string, Handle<Member>> = {};
214
- const g: Graph = { seats: [], holds: [] };
215
- // What this run actually got to. A generated walk protects only the
216
- // states it reaches, and a list of assertions nobody ran is a list of
217
- // assertions nobody has. Every case named here is checked off as it
218
- // fires and the run fails at the end if one never did.
219
- const reached = new Set<string>();
220
- const MUST = [
221
- 'a kicked seat knocked by the guest it bound, who hears removed',
222
- 'a standing dropped by its owner, whose host is never told',
223
- 'a seat kicked by its host, whose guest is never told',
224
- 'a relation taken, so it lives in the standing and not the knock',
225
- 'a ward moved to another harbor under everyone standing on it',
226
- ];
227
- let minted = 0;
228
- const born = async (key: string) => (hands[key] = await w.boot<Member>(key, Member));
229
- for (const key of ['M1', 'M2', 'M3']) await born(key);
230
- let before: Census | undefined;
231
-
232
- // Every move that is legal right now, as a thunk. A move that would not
233
- // be legal is not written down, so the chapter never asserts on a
234
- // question the spec does not answer.
235
- const moves = (): (() => Promise<string>)[] => {
236
- const keys = Object.keys(hands);
237
- const out: (() => Promise<string>)[] = [];
238
-
239
- // invite: a fresh occupant arc, and nothing of the guest's in it
240
- out.push(async () => {
241
- const host = pick(keys);
242
- const id = `o${++minted}`;
243
- const inv = (await hands[host].being.invite(id))!;
244
- assert.equal(inv.ward, hands[host].pk);
245
- assert.match(inv.heir!, HEX64);
246
- g.seats.push({ host, id, inv, guest: null, answered: false, live: true, taken: false });
247
- return `${host} invites ${id}`;
248
- });
249
-
250
- // knock: the guest an unspent seat gets is anyone but the host; a
251
- // spent one only ever answers the one who spent it. Once she has
252
- // taken it the invitation is not a knock any more -- it joins the
253
- // standing's lane, and is exactly as alive as the standing is.
254
- for (const s of g.seats) {
255
- const knock = async () => {
256
- const guest = s.guest ?? pick(keys.filter((k) => k !== s.host));
257
- const hold = g.holds.find((h) => h.seat === s);
258
- const back = await hands[guest].being.knock(s.inv, 'ping');
259
- // Whether this knock is heard at all: the seat has no guest yet,
260
- // or its guest is this one, and nothing of hers has dropped the
261
- // key the door would bind her by. A hold exists only where a
262
- // guest does, so the two tests are one condition and not two.
263
- // Then the seat says which answer: alive is the pong, kicked is
264
- // `removed`, and unheard is silence whichever it is.
265
- const speaks = (s.guest === null || s.guest === guest) && (hold === undefined || hold.live);
266
- if (!speaks) assert.ok(isSilence(back), `${guest} knocking ${s.host}/${s.id} should be silence`);
267
- else if (s.live) {
268
- assert.deepEqual(back, { pong: s.id }, `${guest} knocking ${s.host}/${s.id}`);
269
- s.guest = guest;
270
- s.answered = true;
271
- } else {
272
- assert.ok(said(back, 'removed'), `${guest} knocking kicked ${s.host}/${s.id} should hear removed`);
273
- reached.add('a kicked seat knocked by the guest it bound, who hears removed');
274
- }
275
- return `${guest} knocks ${s.host}/${s.id}`;
276
- };
277
- out.push(knock);
278
- // Weighted, not written twice: the case this move exists to protect
279
- // is a seat kicked after it answered and before it was taken, and a
280
- // uniform walk over a list that grows with every invitation reaches
281
- // it by luck if at all. Three entries make it likely by
282
- // construction, and the count at the end of the run makes it
283
- // certain: a walk that never got there fails rather than passing
284
- // quietly on an assertion that never ran.
285
- if (!s.live && s.answered && !s.taken) out.push(knock, knock);
286
- // a stranger on a spent seat: silence, and nothing rebinds
287
- if (s.guest !== null)
288
- out.push(async () => {
289
- const other = keys.filter((k) => k !== s.host && k !== s.guest);
290
- if (!other.length) return 'no stranger to try';
291
- const who = pick(other);
292
- assert.ok(isSilence(await hands[who].being.knock(s.inv, 'ping')), `${who} should not get in on ${s.host}/${s.id}`);
293
- return `${who} is refused at ${s.host}/${s.id}`;
294
- });
295
- }
296
-
297
- // take: only after an answer, only once. the far side may have kicked
298
- // her in between -- take reads the knock record, and still births one.
299
- for (const s of g.seats.filter((s) => s.answered && !s.taken))
300
- out.push(async () => {
301
- const id = `s${++minted}`;
302
- assert.equal(await hands[s.guest!].being.take(id, s.inv), id);
303
- s.taken = true;
304
- g.holds.push({ owner: s.guest!, id, seat: s, live: true });
305
- reached.add('a relation taken, so it lives in the standing and not the knock');
306
- assert.equal(await hands[s.guest!].being.take(`${id}-again`, s.inv), null); // spent for her
307
- return `${s.guest} takes ${s.host}/${s.id} as ${id}`;
308
- });
309
-
310
- // drop: hers to drop, and the host is never told
311
- for (const h of g.holds.filter((h) => h.live))
312
- out.push(async () => {
313
- hands[h.owner].being.standings.remove(h.id);
314
- h.live = false;
315
- reached.add('a standing dropped by its owner, whose host is never told');
316
- assert.equal(hands[h.owner].being.standings[h.id], undefined);
317
- return `${h.owner} drops ${h.id}`;
318
- });
319
-
320
- // kick: the host's, and the guest is never told either
321
- for (const s of g.seats.filter((s) => s.live))
322
- out.push(async () => {
323
- hands[s.host].being.occupants.remove(s.id);
324
- s.live = false;
325
- reached.add('a seat kicked by its host, whose guest is never told');
326
- return `${s.host} kicks ${s.id}`;
327
- });
328
-
329
- // a new being: an estate grows
330
- if (keys.length < 6) out.push(async () => (await born(`M${keys.length + 1}`), `M${keys.length + 1} is booted`));
331
-
332
- // a ward moves harbor, under everyone standing on it
333
- out.push(async () => {
334
- const who = pick(keys);
335
- await w.migrate(who);
336
- reached.add('a ward moved to another harbor under everyone standing on it');
337
- return `${who}'s ward moves harbor`;
338
- });
339
-
340
- // weather: the host's door is gone. unreached, never silence, and the
341
- // relation is exactly where it was when it comes back. The move is on
342
- // the list under every topology even where there is nothing to cut,
343
- // because the list is the script: a move missing in one world would
344
- // make it a different script, and the three could not be compared.
345
- for (const h of g.holds.filter((h) => h.live && h.seat.live))
346
- out.push(async () => {
347
- if (!canDown) return 'nothing to cut inside one ward';
348
- hands[h.seat.host].down = true;
349
- assert.ok(isUnreached(await hands[h.owner].being.standings[h.id]!.ask('ping')), `${h.owner}/${h.id} should be unreached`);
350
- hands[h.seat.host].down = false;
351
- return `${h.seat.host} is cut and comes back`;
352
- });
353
-
354
- return out;
355
- };
356
-
357
- // Every arc in the model, asked. This is where the ledger stops being
358
- // bookkeeping and becomes what the door actually does.
359
- const sweep = async (step: number) => {
360
- for (const h of g.holds) {
361
- const standing = hands[h.owner].being.standings[h.id];
362
- if (!h.live) {
363
- assert.equal(standing, undefined, `step ${step}: ${h.owner}'s dropped ${h.id} is still there`);
364
- continue;
365
- }
366
- const out = await standing!.ask('ping');
367
- if (h.seat.live) assert.deepEqual(out, { pong: h.seat.id }, `step ${step}: ${h.owner}/${h.id} -> ${h.seat.host}/${h.seat.id}`);
368
- else assert.ok(said(out, 'removed'), `step ${step}: ${h.owner}/${h.id} is kicked and should hear removed`);
369
- }
370
- };
371
-
372
- // The trace is the seed's own account of itself: a red run prints the
373
- // moves that got there, which is the only thing that makes a generated
374
- // sequence debuggable at all.
375
- const trace: string[] = [];
376
- const at = (step: number) => `seed ${seed} step ${step}\n ${trace.join('\n ')}`;
377
- // Whatever fails inside a step carries the trace that got there. One
378
- // wrapper, so the move and the sweep are decorated the same way and a
379
- // third thing added to a step cannot be decorated a fourth.
380
- const inStep = async <T>(step: number, work: () => Promise<T>): Promise<T> => {
381
- try {
382
- return await work();
383
- } catch (e) {
384
- throw new Error(`${at(step)}\n -> ${(e as Error).message}`, { cause: e });
385
- }
386
- };
387
- for (let step = 1; step <= 60; step++) {
388
- const move = pick(moves());
389
- // The move that failed goes into the trace before it runs, not after
390
- // it returns: a move names itself as it finishes, so a throw left the
391
- // trace ending at the last move that worked and saying nothing about
392
- // the one that did not. It is written in twice, provisionally and
393
- // then as it named itself, and the provisional line goes when the
394
- // real one lands.
395
- trace.push(`step ${step}: a move that has not said what it did`);
396
- const did = await inStep(step, move);
397
- trace[trace.length - 1] = did;
398
- const c = w.census();
399
- ledger(c, before, at(step), g);
400
- before = c;
401
- await inStep(step, () => sweep(step));
402
- }
403
- // And the walk is held to having gone somewhere. Sixty moves under one
404
- // seed is a sample, not a proof, so the states the chapter is written
405
- // to protect are named and counted: a run that never reached one has
406
- // not tested it, and says so here rather than passing on an assertion
407
- // that never ran.
408
- const missed = MUST.filter((what) => !reached.has(what));
409
- assert.deepEqual(missed, [], `seed ${seed} never reached: ${missed.join('; ')}\n ${trace.join('\n ')}`);
410
- });
411
- }
412
- }