@quo-systems/quo 0.2.9 → 0.2.11

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 (78) hide show
  1. package/README.md +19 -10
  2. package/SPEC.md +301 -110
  3. package/dist/being/being.d.ts +2 -2
  4. package/dist/being/being.js +34 -12
  5. package/dist/being/digest.js +26 -8
  6. package/dist/being/index.d.ts +2 -2
  7. package/dist/being/index.js +2 -2
  8. package/dist/being/silence.d.ts +2 -0
  9. package/dist/being/silence.js +12 -0
  10. package/dist/being/types.d.ts +4 -2
  11. package/dist/being/types.js +25 -0
  12. package/dist/conformance/assert.js +40 -6
  13. package/dist/conformance/beings.d.ts +48 -5
  14. package/dist/conformance/beings.js +39 -8
  15. package/dist/conformance/estate.js +110 -21
  16. package/dist/conformance/index.d.ts +5 -2
  17. package/dist/conformance/index.js +165 -7
  18. package/dist/harbor/core.d.ts +5 -2
  19. package/dist/harbor/core.js +195 -45
  20. package/dist/harbor/dial.js +32 -15
  21. package/dist/harbor/index.d.ts +1 -0
  22. package/dist/harbor/index.js +3 -0
  23. package/dist/harbor/memory.d.ts +3 -3
  24. package/dist/harbor/memory.js +7 -12
  25. package/dist/harbor/reach.js +42 -17
  26. package/dist/ward/allowance.js +15 -4
  27. package/dist/ward/arithmetic.d.ts +1 -0
  28. package/dist/ward/arithmetic.js +22 -6
  29. package/dist/ward/cells.d.ts +3 -1
  30. package/dist/ward/cells.js +79 -21
  31. package/dist/ward/door.d.ts +3 -2
  32. package/dist/ward/door.js +38 -10
  33. package/dist/ward/ground.d.ts +5 -1
  34. package/dist/ward/ground.js +38 -1
  35. package/dist/ward/heirs.d.ts +2 -3
  36. package/dist/ward/heirs.js +19 -13
  37. package/dist/ward/index.d.ts +1 -0
  38. package/dist/ward/index.js +3 -0
  39. package/dist/ward/owner.d.ts +6 -27
  40. package/dist/ward/owner.js +59 -33
  41. package/dist/ward/partition.d.ts +3 -0
  42. package/dist/ward/partition.js +109 -4
  43. package/dist/ward/seal.d.ts +1 -0
  44. package/dist/ward/seal.js +41 -13
  45. package/dist/ward/stance.d.ts +7 -3
  46. package/dist/ward/stance.js +156 -66
  47. package/dist/ward/ward.d.ts +10 -0
  48. package/dist/ward/ward.js +123 -51
  49. package/package.json +4 -2
  50. package/src/being/being.ts +33 -11
  51. package/src/being/digest.ts +28 -13
  52. package/src/being/index.ts +2 -2
  53. package/src/being/silence.ts +14 -0
  54. package/src/being/types.ts +39 -5
  55. package/src/conformance/assert.ts +37 -4
  56. package/src/conformance/beings.ts +41 -10
  57. package/src/conformance/estate.ts +107 -20
  58. package/src/conformance/index.ts +188 -13
  59. package/src/harbor/core.ts +203 -46
  60. package/src/harbor/dial.ts +46 -17
  61. package/src/harbor/index.ts +3 -0
  62. package/src/harbor/memory.ts +8 -13
  63. package/src/harbor/reach.ts +47 -21
  64. package/src/ward/allowance.ts +15 -4
  65. package/src/ward/arithmetic.ts +25 -8
  66. package/src/ward/cells.ts +76 -25
  67. package/src/ward/door.ts +38 -12
  68. package/src/ward/ground.ts +52 -3
  69. package/src/ward/heirs.ts +19 -13
  70. package/src/ward/index.ts +3 -0
  71. package/src/ward/owner.ts +65 -46
  72. package/src/ward/partition.ts +109 -5
  73. package/src/ward/seal.ts +41 -12
  74. package/src/ward/stance.ts +163 -64
  75. package/src/ward/ward.ts +124 -52
  76. package/vectors/arithmetic.json +7 -0
  77. package/vectors/framing.json +30 -15
  78. package/vectors/wire.json +4 -4
@@ -1,25 +1,38 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // The one stance builder. Used for every being the ward boots. Nothing
3
3
  // outer is in what she holds: ids in, values out, keys in the bind table.
4
- import { silence, isSilence, isUnreached, isWord, word } from '../being/silence.ts';
4
+ import { silence, isSilence, isWord, word, wordOf } from '../being/silence.ts';
5
5
  import { digest } from '../being/digest.ts';
6
- import { RESERVED_IDS } from '../being/types.ts';
6
+ import { RESERVED_IDS, isBlueprint } from '../being/types.ts';
7
7
  import type { Answer, Cells, Invitation, JsonObject, Silence, Stance, Standing, Standings, Wanted, Word } from '../being/types.ts';
8
- import { at, put, drop, type Bind, type StandingKeys } from './partition.ts';
8
+ import { at, put, drop, prune, type Bind, type StandingKeys } from './partition.ts';
9
9
  import { allow, within, LATE } from './allowance.ts';
10
- import { isWardPk, type ReplyPayload } from './seal.ts';
10
+ import { isHex, isWardPk, type ReplyPayload } from './seal.ts';
11
+ import { cellFault } from './cells.ts';
11
12
 
12
13
  // What the stance needs from its ward, and no more.
13
14
  export type Inside = {
14
15
  pk: string;
16
+ // Is the being this stance was built for still one of the ward's? Unboot
17
+ // takes her door and her rows, but a class whose code is still running
18
+ // holds the stance closure, and everything in it still points at live
19
+ // objects. Read before every call, so a being taken out reaches nothing.
20
+ live(): boolean;
15
21
  mintKey(bind: Bind): Promise<{ seed: string; pk: string }>; // a fresh signing key, recorded as minted
16
22
  openHeir(heir: string, being: string, id: string): void;
17
23
  closeHeir(heir: string): void;
18
24
  // one send for every destination. the far door's reply, silence, or unreached.
19
- // signs as keys.current, announces keys.next (minting one if asked), and rotates keys on an answer.
20
- send(bind: Bind, keys: StandingKeys, announce: boolean, method: string | undefined, args: JsonObject, wanted: Wanted | undefined): Promise<ReplyPayload | Silence | Word>;
25
+ // signs as keys.current, mints and announces keys.next where the relation has
26
+ // a heir to rotate on, and rotates keys on an answer.
27
+ send(bind: Bind, keys: StandingKeys, method: string | undefined, args: JsonObject, wanted: Wanted | undefined): Promise<ReplyPayload | Silence | Word>;
21
28
  // a new being of this ward, as the owner's boot makes one. the key, or null.
22
- instantiate(key: string, className: string): string | null;
29
+ instantiate(className: string, key: string): string | null;
30
+ // mint an occupant on a being of this ward, for the being who made her.
31
+ relate(key: string, id: string): Promise<Invitation | null>;
32
+ // take a being of this ward out again, with every relation she holds.
33
+ unmake(key: string): void;
34
+ // the bind table was written: a record put, dropped or moved on.
35
+ wrote(): void;
23
36
  };
24
37
 
25
38
  // An invitation as it travels: the far ward's pk, the heir pk, the heir's secret seed. Or just the ward, for its public being.
@@ -45,7 +58,7 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
45
58
  const i = inv as SealedInvitation;
46
59
  if (!i || typeof i !== 'object' || !isWardPk(i.ward)) return false;
47
60
  if (i.heir === undefined) return i.secret === undefined;
48
- return typeof i.heir === 'string' && typeof i.secret === 'string' && /^[0-9a-f]{64}$/.test(i.heir) && /^[0-9a-f]{64}$/.test(i.secret);
61
+ return isHex(i.heir) && isHex(i.secret);
49
62
  };
50
63
 
51
64
  // One relation, one ask at a time. The rotation is a conversation: a send
@@ -72,9 +85,9 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
72
85
  // Every ask a being makes, bounded. The bound is outside the lane on
73
86
  // purpose: an ask that waits its turn behind a relation that comes back
74
87
  // round never reaches a send, and a bound that only watched the wire would
75
- // never see it hang. A bound that ran out is silence, never unreached:
76
- // unreached promises nothing was delivered and is safe to ask again, and a
77
- // wait that gave up knows no such thing.
88
+ // never see it hang. A bound that ran out is the word late, never
89
+ // unreached: unreached promises nothing was delivered and is safe to ask
90
+ // again, and a wait that gave up knows no such thing.
78
91
  //
79
92
  // The bell rings before the work is reached, too: an ask that was still
80
93
  // waiting its turn when the wait ran out is never sent. Sent then, it would
@@ -89,53 +102,96 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
89
102
  return out === LATE ? word('late') : out;
90
103
  };
91
104
 
92
- const send = async (keys: StandingKeys, method: string | undefined, args: JsonObject, wanted: Wanted | undefined, rec?: Cells['standings'][string]): Promise<Answer> => {
93
- const r = await inside.send(bind, keys, true, method, args, wanted);
94
- if (isSilence(r) || isWord(r)) return r;
95
- if ('silence' in r) return silence;
96
- if ('quo' in r) return word(r.quo); // the far door's word for her, sealed to her lid
97
- if (rec && method !== undefined && r.seen !== null) rec.seen = r.seen; // the digest rode along
98
- return r.object;
105
+ // What one send came back as, read in one place for every path that sends.
106
+ // A reply that opened and was signed by the far ward is the door speaking,
107
+ // whatever it said: an object, its silence, or a word. Bytes that were not
108
+ // Quo's, late and unreached are not the door speaking, and say nothing
109
+ // about whether it heard.
110
+ type Read = { answer: Answer; door: boolean; seen: string | null };
111
+ const read = (r: ReplyPayload | Silence | Word): Read => {
112
+ if (isSilence(r) || isWord(r)) return { answer: r, door: false, seen: null };
113
+ if ('silence' in r) return { answer: silence, door: true, seen: null };
114
+ if ('quo' in r) return { answer: word(r.quo), door: true, seen: null }; // the far door's word for her, sealed to her lid
115
+ return { answer: r.object, door: true, seen: r.seen };
99
116
  };
117
+ // Whether the door spent the heir this send spoke under. An object, the
118
+ // door's silence and threw all mean it was honoured. A refusal word wrote
119
+ // nothing and says so. Anything that is not the door speaking says nothing.
120
+ const honoured = (r: Read): boolean => r.door && !(isWord(r.answer) && wordOf(r.answer) !== 'threw');
121
+ const answered = (r: Read): boolean => r.door && !isSilence(r.answer) && !isWord(r.answer);
100
122
 
101
- const knocking = async (inv: SealedInvitation, method: string | undefined, args: JsonObject, wanted: Wanted | undefined): Promise<Answer> => {
123
+ // The ask is bounded twice with one budget, once around the lane and once
124
+ // around the wire, and the two bells are microseconds apart. A reply that
125
+ // lands between them is a reply she was already told was late: its keys are
126
+ // rotated, since the far door rotated too and nothing may un-rotate that,
127
+ // but nothing of it is written into her cells. What she was handed is the
128
+ // last word on what she saw.
129
+ const send = async (keys: StandingKeys, method: string | undefined, args: JsonObject, wanted: Wanted | undefined, rec?: Cells['standings'][string], live: () => boolean = () => true): Promise<Answer> => {
130
+ const r = read(await inside.send(bind, keys, method, args, wanted));
131
+ if (rec && method !== undefined && r.seen !== null && live()) rec.seen = r.seen; // the digest rode along
132
+ return r.answer;
133
+ };
134
+
135
+ const knocking = async (inv: SealedInvitation, method: string | undefined, args: JsonObject, wanted: Wanted | undefined, live: () => boolean = () => true): Promise<Answer> => {
102
136
  const name = nameOf(inv);
137
+ // Taken while this knock waited its turn: the relation lives in the
138
+ // standing now and speaks on its lane, under its keys and its count.
139
+ const taken = takenAs(name);
140
+ if (taken !== undefined) {
141
+ const keys = at(bind.standings, taken);
142
+ if (!keys) return word('dropped');
143
+ return lane(`ask:${taken}`, () => asking(taken, keys, at(cells.standings, taken), method, args, wanted, live));
144
+ }
103
145
  // her own key for this relation, minted once: rotation two. reused if she knocks again.
104
146
  const k = (bind.knocks[name] ??= { current: (await inside.mintKey(bind)).seed, next: null, spoke: false, sent: false, seq: 0 });
105
- // the first knock on a heir speaks as the heir, the key the inviter handed out, and announces her own.
106
- // once bytes have come back, the heir is spent whatever they said, and her own key speaks.
107
- // She sent once as the heir and nothing came back. Either the door heard,
108
- // spent the heir and rotated to the key she announced, or nothing arrived
109
- // and the heir still stands. Only the far door knows, and it will not say.
147
+ prune(bind.knocks, bind.answered); // this one is the newest, so it stays and the oldest never answered goes
148
+ // The first knock on a heir speaks as the heir, the key the inviter
149
+ // handed out, and announces her own. Once the door has honoured it the
150
+ // heir is spent, whatever she then said, and her own key speaks; a heir
151
+ // spent once stays spent.
152
+ //
153
+ // She sent once as the heir and the door did not speak: nothing came
154
+ // back, or bytes that were not Quo's, or the wait ran out. Either the
155
+ // door heard, spent the heir and rotated to the key she announced, or
156
+ // nothing arrived and the heir still stands. Only the far door knows,
157
+ // and it will not say.
110
158
  //
111
159
  // So she asks, and the asking tells her. Under her own key: if the door
112
- // heard, that key is the one it admits, and she is answered. If it did
113
- // not, the key means nothing there, the ask is refused, and a refusal at
114
- // that door writes nothing the heir is untouched and still speaks. Then
115
- // she knocks as the heir, as she would have. One extra round trip in the
116
- // one case where a reply was lost, and the relation is not stranded.
160
+ // heard, that key is the one it admits, and she is answered, or told the
161
+ // door's word. If it did not, the key means nothing there, the ask is
162
+ // refused, and a refusal at that door writes nothing: the heir is
163
+ // untouched and still speaks. Then she knocks as the heir, as she would
164
+ // have. One extra round trip in the one case where a reply was lost, and
165
+ // the relation is not stranded.
117
166
  //
118
167
  // No stranger gains anything by it: whoever holds the invitation could
119
168
  // always knock as the heir, and her own key is admitted only where the
120
169
  // door already bound it to her.
121
170
  if (inv.heir !== undefined && !k.spoke && k.sent) {
122
171
  const own: StandingKeys = { ward: inv.ward, heir: inv.heir, current: k.current, next: k.next, seq: k.seq };
123
- const r = await inside.send(bind, own, true, method, args, wanted);
172
+ const r = read(await inside.send(bind, own, method, args, wanted));
124
173
  k.seq = own.seq;
125
174
  k.current = own.current;
126
175
  k.next = own.next;
127
- if (isUnreached(r)) return r; // still nothing. she knows no more than before.
128
- if (!isSilence(r) && !isWord(r) && 'object' in r) {
129
- k.spoke = true; // it was heard the first time, and this is the answer
130
- bind.answered[name] = true;
131
- return r.object;
176
+ inside.wrote();
177
+ if (!r.door) {
178
+ // Nothing the door said. Unreached and late are hers to hear now; bytes
179
+ // that were not Quo's leave her where she was, and the heir is tried.
180
+ if (!isSilence(r.answer)) return r.answer;
181
+ } else if (!isSilence(r.answer)) {
182
+ // The door spoke to her own key, so it heard the first knock: this is
183
+ // the answer, or the door's word for her. Its silence alone cannot be
184
+ // told from a refusal, so silence is followed by the knock as the heir,
185
+ // which the door then refuses if it had heard, and she hears silence.
186
+ k.spoke = true;
187
+ if (answered(r)) bind.answered[name] = true;
188
+ return r.answer;
132
189
  }
133
- // Refused. The door never heard her, so the heir is still hers to spend.
134
190
  }
135
191
 
136
192
  const first = inv.heir !== undefined && !k.spoke;
137
193
  const keys: StandingKeys = first ? { ward: inv.ward, heir: inv.heir!, current: inv.secret!, next: k.current, seq: k.seq } : { ward: inv.ward, heir: inv.heir ?? null, current: k.current, next: k.next, seq: k.seq };
138
- const r = await inside.send(bind, keys, true, method, args, wanted);
194
+ const r = read(await inside.send(bind, keys, method, args, wanted));
139
195
  k.sent = true; // bytes went out. whether they arrived is the far door's to know.
140
196
  // The number she spoke under, and the key she announced, kept whoever
141
197
  // answered and kept when nobody did. A reply lost on the way back is a
@@ -146,16 +202,10 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
146
202
  k.current = keys.current;
147
203
  k.next = keys.next;
148
204
  }
149
- if (isUnreached(r)) return r; // nothing came back. she does not know whether it was heard.
150
- // The heir is spent once the door honoured it, whatever she then said: an
151
- // object, silence, or that she threw. A refusal wrote nothing, and says
152
- // so by its word, so the heir still speaks and the next knock is as it.
153
- k.spoke = !(!isSilence(r) && !isWord(r) && 'quo' in r && r.quo !== 'threw');
154
- if (isSilence(r) || isWord(r)) return r;
155
- if ('silence' in r) return silence;
156
- if ('quo' in r) return word(r.quo);
157
- bind.answered[name] = true;
158
- return r.object;
205
+ if (honoured(r)) k.spoke = true;
206
+ if (answered(r)) bind.answered[name] = true;
207
+ inside.wrote();
208
+ return r.answer;
159
209
  };
160
210
 
161
211
  // The id she took this relation under, if she took it. One relation is one
@@ -164,16 +214,24 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
164
214
  const keyName = (k: StandingKeys) => (k.heir === null ? `public:${k.ward}` : `${k.ward}:${k.heir}`);
165
215
  const takenAs = (name: string): string | undefined => Object.keys(bind.standings).find((id) => keyName(bind.standings[id]) === name);
166
216
 
217
+ // A being the ward has taken out reaches nothing through the stance she
218
+ // still holds. Her relations went with her, so every one of them reads as
219
+ // dropped, and nothing she asks for is made: a heir written here would name
220
+ // a being no door holds, and an id minted here would be handed to whoever
221
+ // is booted under her key next.
222
+ const gone = () => !inside.live();
223
+
167
224
  const calls = {
168
225
  knock: async (inv: Invitation, method?: string, args: JsonObject = {}, wanted?: Wanted): Promise<Answer> => {
226
+ if (gone()) return word('dropped');
169
227
  if (!valid(inv)) return word('invitation'); // S1. nothing is sent
170
228
  // She may knock again, and after take that knock is an ask: the relation
171
229
  // has one home, so it answers on the standing's lane, under the
172
230
  // standing's keys and its count. A second lane here would speak for the
173
231
  // same relation, and the two would refuse each other.
174
232
  const id = takenAs(nameOf(inv));
175
- if (id !== undefined) return bounded(wanted, (live) => lane(`ask:${id}`, () => (live() ? asking(id, bind.standings[id], cells.standings[id], method, args, wanted) : Promise.resolve(word('late')))));
176
- return bounded(wanted, (live) => lane(`knock:${nameOf(inv)}`, () => (live() ? knocking(inv, method, args, wanted) : Promise.resolve(word('late')))));
233
+ if (id !== undefined) return standingAt(id).ask(method, args, wanted);
234
+ return bounded(wanted, (live) => lane(`knock:${nameOf(inv)}`, () => (live() ? knocking(inv, method, args, wanted, live) : Promise.resolve(word('late')))));
177
235
  },
178
236
  // Take consumes. Until now the relation lived in the knock record, under
179
237
  // the ward and the heir; from now it lives in the standing, under her id.
@@ -193,7 +251,7 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
193
251
  // ask on it is refused. The relation she took correctly would be dead
194
252
  // because a second knock on the same invitation was careless.
195
253
  take: async (id: string, inv: Invitation): Promise<string | null> => {
196
- if (!valid(inv)) return null;
254
+ if (gone() || !valid(inv)) return null;
197
255
  const name = nameOf(inv);
198
256
  return lane(`knock:${name}`, () => {
199
257
  if (!bind.answered[name]) return Promise.resolve(null);
@@ -206,12 +264,15 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
206
264
  put(cells.standings, id, { id, digest: null, blueprint: null, seen: null });
207
265
  delete bind.knocks[name];
208
266
  delete bind.answered[name];
267
+ inside.wrote();
209
268
  return Promise.resolve(id);
210
269
  });
211
270
  },
212
271
  remove: (id: string): void => {
272
+ if (gone()) return;
213
273
  drop(cells.standings, id);
214
274
  drop(bind.standings, id);
275
+ inside.wrote();
215
276
  },
216
277
  };
217
278
 
@@ -221,20 +282,31 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
221
282
  // issued while the standing stood is answered even if she drops it in
222
283
  // the next line. Waiting for the lane is not a reason to lose it.
223
284
  ask: (method?: string, args: JsonObject = {}, wanted?: Wanted): Promise<Answer> => {
285
+ if (gone()) return Promise.resolve(word('dropped') as Answer);
224
286
  const keys = at(bind.standings, id);
225
287
  if (!keys) return Promise.resolve(word('dropped') as Answer); // S2. she dropped it between one line and the next
226
288
  const rec = at(cells.standings, id);
227
- return bounded(wanted, (live) => lane(`ask:${id}`, () => (live() ? asking(id, keys, rec, method, args, wanted) : Promise.resolve(word('late')))));
289
+ return bounded(wanted, (live) => lane(`ask:${id}`, () => (live() ? asking(id, keys, rec, method, args, wanted, live) : Promise.resolve(word('late')))));
228
290
  },
229
291
  });
230
292
 
231
- const asking = async (id: string, keys: StandingKeys, rec: Cells['standings'][string] | undefined, method: string | undefined, args: JsonObject, wanted: Wanted | undefined): Promise<Answer> => {
232
- const out = await send(keys, method, args, wanted, rec);
293
+ // The record an ask writes into is the one it was issued on. A standing
294
+ // dropped and re-taken under the same id while the ask was in flight is
295
+ // another relation, and the blueprint that came back is not hers.
296
+ const asking = async (id: string, keys: StandingKeys, rec: Cells['standings'][string] | undefined, method: string | undefined, args: JsonObject, wanted: Wanted | undefined, live: () => boolean = () => true): Promise<Answer> => {
297
+ const out = await send(keys, method, args, wanted, rec, live);
233
298
  if (method === undefined && !isSilence(out) && !isWord(out)) {
234
- const here = at(cells.standings, id);
235
- if (here) {
236
- here.blueprint = out as never;
237
- here.digest = here.seen = await digest(out);
299
+ // A blueprint her cells cannot keep, nested past the bound or carrying
300
+ // a key no harbor writes, is not Quo's bytes: silence, and nothing written.
301
+ if (cellFault(out, 'blueprint') !== null) return silence;
302
+ if (rec && live() && at(cells.standings, id) === rec) {
303
+ // Read as a blueprint before it is kept as one. A far describe is
304
+ // somebody else's code and may answer anything; what is not a
305
+ // blueprint is not written as one, and she holds no blueprint for
306
+ // that relation rather than one a side would walk and break on. The
307
+ // answer itself is hers either way: it is returned below, unread.
308
+ rec.blueprint = isBlueprint(out) ? out : null;
309
+ rec.digest = rec.seen = await digest(out);
238
310
  }
239
311
  }
240
312
  return out;
@@ -251,26 +323,53 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
251
323
  cells,
252
324
  // A being may make. The ward refuses what it refuses the owner: a key
253
325
  // already booted, a class the harbor does not hold, and a throw at birth
254
- // leaves the partition as it was. Reaching into the being made is not
255
- // offered here: that is the owner's, and she has no relation to her
256
- // until one is invited and taken like any other.
257
- boot: (className: string, key: string): string | null => inside.instantiate(key, className),
326
+ // leaves the partition as it was. Reaching into another is still the
327
+ // owner's alone: what a maker may have is a relation to what she made,
328
+ // by naming an id, and it is made the way every relation is. The being
329
+ // made mints an occupant for her maker under the maker's own key, so she
330
+ // knows who made her by that name and by nothing else; her maker knocks,
331
+ // takes it under the id she gave, and holds one standing like any other.
332
+ // Nothing half-lives: a relation that could not be made unmakes the
333
+ // being, who is a moment old and known to nobody.
334
+ boot: async (className: string, made: string, id?: string): Promise<string | null> => {
335
+ if (gone()) return null;
336
+ if (id !== undefined && (reserved(id) || at(cells.standings, id) || at(cells.occupants, id))) return null;
337
+ const born = inside.instantiate(className, made);
338
+ if (born === null || id === undefined) return born;
339
+ const inv = await inside.relate(born, key);
340
+ if (inv) {
341
+ const out = await calls.knock(inv);
342
+ if (!isSilence(out) && !isWord(out) && (await calls.take(id, inv)) === id) return born;
343
+ }
344
+ inside.unmake(born);
345
+ return null;
346
+ },
258
347
  occupants: {
259
348
  // rotation one: the ward mints the occupant's first key and gives the secret away.
260
349
  // it keeps the pk beside the id and nothing else. the invitation IS the key.
261
- invite: async (id: string): Promise<Invitation | null> => {
262
- if (reserved(id) || at(cells.occupants, id) || at(cells.standings, id)) return null;
263
- put(cells.occupants, id, { id, notes: {} });
350
+ invite: async (id: string, notes?: JsonObject): Promise<Invitation | null> => {
351
+ if (gone() || reserved(id) || at(cells.occupants, id) || at(cells.standings, id)) return null;
352
+ // The key first, and nothing written until it exists: a record put
353
+ // before the mint would be a record a remove in the meantime drops
354
+ // with no heir to close, and the heir opened after it would name an
355
+ // id nobody holds, for a later invite under that id to resurrect.
264
356
  const k = await inside.mintKey(bind);
357
+ if (reserved(id) || at(cells.occupants, id) || at(cells.standings, id)) return null; // taken while the key was minted
358
+ // The notes are the terms the inviter minted under, hers to read on her
359
+ // gate. She may write more later; nobody outside ever writes them.
360
+ put(cells.occupants, id, { id, notes: notes ? { ...notes } : {} });
265
361
  inside.openHeir(k.pk, key, id);
266
362
  put(bind.occupants, id, k.pk);
363
+ inside.wrote();
267
364
  return { ward: inside.pk, heir: k.pk, secret: k.seed };
268
365
  },
269
366
  remove: (id: string): void => {
367
+ if (gone()) return;
270
368
  drop(cells.occupants, id);
271
369
  const heir = at(bind.occupants, id);
272
370
  drop(bind.occupants, id);
273
371
  if (heir) inside.closeHeir(heir); // the heir dies with the id
372
+ inside.wrote();
274
373
  },
275
374
  },
276
375
  standings,