@quo-systems/quo 0.1.1 → 0.2.1

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 (49) hide show
  1. package/SPEC.md +259 -129
  2. package/dist/being/index.d.ts +1 -1
  3. package/dist/being/index.js +1 -1
  4. package/dist/being/silence.d.ts +6 -1
  5. package/dist/being/silence.js +25 -7
  6. package/dist/being/types.d.ts +8 -4
  7. package/dist/being/types.js +1 -1
  8. package/dist/conformance/beings.d.ts +1 -1
  9. package/dist/conformance/beings.js +5 -5
  10. package/dist/conformance/estate.js +23 -34
  11. package/dist/conformance/index.js +14 -12
  12. package/dist/harbor/core.d.ts +7 -1
  13. package/dist/harbor/core.js +25 -2
  14. package/dist/harbor/dial.js +7 -1
  15. package/dist/harbor/memory.d.ts +5 -1
  16. package/dist/harbor/memory.js +7 -2
  17. package/dist/ward/door.d.ts +6 -2
  18. package/dist/ward/door.js +55 -35
  19. package/dist/ward/ground.d.ts +4 -1
  20. package/dist/ward/heirs.d.ts +4 -2
  21. package/dist/ward/heirs.js +20 -3
  22. package/dist/ward/owner.d.ts +5 -3
  23. package/dist/ward/owner.js +55 -16
  24. package/dist/ward/partition.d.ts +6 -0
  25. package/dist/ward/partition.js +2 -0
  26. package/dist/ward/seal.d.ts +3 -1
  27. package/dist/ward/seal.js +6 -2
  28. package/dist/ward/stance.d.ts +2 -2
  29. package/dist/ward/stance.js +21 -12
  30. package/dist/ward/ward.js +37 -12
  31. package/package.json +19 -31
  32. package/src/being/index.ts +1 -1
  33. package/src/being/silence.ts +26 -8
  34. package/src/being/types.ts +13 -3
  35. package/src/conformance/beings.ts +5 -5
  36. package/src/conformance/estate.ts +22 -36
  37. package/src/conformance/index.ts +14 -12
  38. package/src/harbor/core.ts +24 -2
  39. package/src/harbor/dial.ts +7 -1
  40. package/src/harbor/memory.ts +6 -1
  41. package/src/ward/door.ts +54 -32
  42. package/src/ward/ground.ts +6 -2
  43. package/src/ward/heirs.ts +20 -5
  44. package/src/ward/owner.ts +54 -16
  45. package/src/ward/partition.ts +10 -0
  46. package/src/ward/seal.ts +8 -5
  47. package/src/ward/stance.ts +20 -14
  48. package/src/ward/ward.ts +37 -13
  49. package/vectors/framing.json +7 -1
package/dist/ward/ward.js CHANGED
@@ -4,9 +4,9 @@
4
4
  // the first being in its own map, and every being it later instantiates gets
5
5
  // a stance built by the same code. Mints every key, seals every ask that
6
6
  // leaves, opens every one that arrives.
7
- import { silence, isSilence, unreached, isUnreached } from '../being/silence.js';
7
+ import { silence, isSilence, unreached, isWord, word } from '../being/silence.js';
8
8
  import { OWNER } from '../being/types.js';
9
- import { at, open, put, emptyBind, emptyCells } from './partition.js';
9
+ import { at, drop, open, put, emptyBind, emptyCells } from './partition.js';
10
10
  import { Heirs } from './heirs.js';
11
11
  import { makeDoor } from './door.js';
12
12
  import { buildStance } from './stance.js';
@@ -65,7 +65,7 @@ class Self {
65
65
  this.ask = async (method, args = {}) => {
66
66
  try {
67
67
  const out = await this.answer({ id: OWNER }, method, args);
68
- return isSilence(out) || isUnreached(out) ? silence : out;
68
+ return isSilence(out) || isWord(out) ? silence : out;
69
69
  }
70
70
  catch {
71
71
  return silence;
@@ -83,21 +83,46 @@ class Self {
83
83
  pk: this.pk,
84
84
  doors: this.doors,
85
85
  publicKey: () => this.p.public,
86
- instantiate: (key, className, isPublic) => this.#instantiate(key, className, isPublic),
86
+ instantiate: (key, className) => this.#instantiate(key, className),
87
+ unboot: (key) => this.#unboot(key),
88
+ setPublic: (key) => void (this.p.public = key),
87
89
  }, asker, method, args);
88
90
  }
89
91
  // ---- ward functions. on the object. no stance reaches them.
90
- #instantiate(key, className, isPublic = false) {
92
+ #instantiate(key, className) {
91
93
  if (!key || this.doors.has(key))
92
94
  return null;
93
95
  const door = this.#boot(key, className);
94
96
  if (!door)
95
97
  return null;
96
98
  door.cells.class = className; // so a restart finds her
97
- if (isPublic)
98
- this.p.public = key;
99
99
  return door;
100
100
  }
101
+ // The inverse of boot, and the only way a being leaves a ward. Her
102
+ // relations go first and by the same calls she would have used herself,
103
+ // so every occupant hears `removed` at the door from the keys it kept,
104
+ // rather than meeting a being who is simply not there any more. What is
105
+ // left after that is her cells and her bind table, and they go with her:
106
+ // a row naming a being no door holds would be read on the next restart
107
+ // and boot her again.
108
+ #unboot(key) {
109
+ const door = this.doors.get(key);
110
+ if (!door || key === this.pk)
111
+ return null;
112
+ const occupants = Object.keys(door.cells.occupants), standings = Object.keys(door.cells.standings);
113
+ for (const id of occupants)
114
+ door.stance.occupants.remove(id);
115
+ for (const id of standings)
116
+ door.stance.standings.remove(id);
117
+ // A ward whose public being is gone answers arrivals for no heir with
118
+ // silence, as a ward that never had one does.
119
+ if (this.p.public === key)
120
+ this.p.public = null;
121
+ drop(this.p.beings, key);
122
+ drop(this.p.bind, key);
123
+ this.doors.delete(key);
124
+ return [...occupants, ...standings];
125
+ }
101
126
  // Nothing is written until there is somebody to write it for: a class the
102
127
  // harbor does not know makes no being, and a boot that made nobody must
103
128
  // leave the partition as it found it.
@@ -173,22 +198,22 @@ class Self {
173
198
  // relation that comes back round holds a lane the answer needs, and only a
174
199
  // bound on the wait can break that. What comes back late is not read.
175
200
  //
176
- // A wait that ran out is silence, never unreached. Unreached promises
201
+ // A wait that ran out is `late`, never unreached. Unreached promises
177
202
  // nothing was delivered and is safe to retry; a bound that expired knows
178
203
  // no such thing, because the far door may have heard and be working still.
179
204
  //
180
205
  // A harbor answers bytes or nothing. One that throws instead has answered
181
206
  // nothing in a louder voice, and is read as nothing: no door was reached.
182
- const carried = (keys.ward === this.pk ? this.door(new Uint8Array(bytes)) : this.g.carry(keys.ward, bytes)).then((b) => b, () => undefined);
207
+ const carried = (keys.ward === this.pk ? this.door(new Uint8Array(bytes)).then((r) => r.bytes) : this.g.carry(keys.ward, bytes)).then((b) => b, () => undefined);
183
208
  const out = await within(budget.time, carried);
184
209
  if (out === LATE)
185
- return silence;
210
+ return word('late');
186
211
  if (out === undefined)
187
212
  return unreached(); // nothing came back. no door was reached.
188
213
  const reply = await openReply(out, ephemeral, wardSignPk(keys.ward));
189
214
  if (!reply)
190
- return silence; // did not open, or not from that ward
191
- if (!('silence' in reply) && keys.next !== null) {
215
+ return silence; // did not open, or not from that ward: not Quo's bytes
216
+ if ('object' in reply && keys.next !== null) {
192
217
  keys.current = keys.next; // the far door holds `next` as announced. move to it.
193
218
  keys.next = null;
194
219
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quo-systems/quo",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "Quo: an object asks another object and gets an answer, without knowing where it is. Being, Ward, Harbor.",
5
5
  "keywords": [
6
6
  "quo",
@@ -18,40 +18,28 @@
18
18
  "node": ">=22.18"
19
19
  },
20
20
  "exports": {
21
- ".": { "types": "./dist/being/index.d.ts", "default": "./dist/being/index.js" },
22
- "./ward": { "types": "./dist/ward/index.d.ts", "default": "./dist/ward/index.js" },
23
- "./harbor": { "types": "./dist/harbor/index.d.ts", "default": "./dist/harbor/index.js" },
24
- "./conformance": { "types": "./dist/conformance/index.d.ts", "default": "./dist/conformance/index.js" }
21
+ ".": {
22
+ "types": "./dist/being/index.d.ts",
23
+ "default": "./dist/being/index.js"
24
+ },
25
+ "./ward": {
26
+ "types": "./dist/ward/index.d.ts",
27
+ "default": "./dist/ward/index.js"
28
+ },
29
+ "./harbor": {
30
+ "types": "./dist/harbor/index.d.ts",
31
+ "default": "./dist/harbor/index.js"
32
+ },
33
+ "./conformance": {
34
+ "types": "./dist/conformance/index.d.ts",
35
+ "default": "./dist/conformance/index.js"
36
+ }
25
37
  },
26
38
  "scripts": {
27
39
  "build": "rm -rf dist && tsc -p tsconfig.build.json",
28
- "check": "npm run build && npm run typecheck && npm run lint && npm test && npm run check:estate",
29
- "check:estate": "node --test \"estate/test/*.test.ts\"",
30
- "pretypecheck": "npm run build",
31
- "typecheck": "tsc",
32
- "typecheck:watch": "tsc --watch",
33
- "lint": "npm run lint:ts && npm run lint:md",
34
- "lint:ts": "oxlint --type-aware",
35
- "lint:md": "markdownlint-cli2",
36
- "lint:fix": "oxlint --type-aware --fix && markdownlint-cli2 --fix",
37
- "pretest": "npm run build",
38
40
  "test": "node --test \"test/*.test.ts\"",
39
- "check:terrain": "node --test \"test/terrain/*.test.ts\" \"estate/test/terrain/*.test.ts\"",
40
- "test:being": "node --test test/being.test.ts",
41
- "test:ward": "node --test test/ward.test.ts",
42
- "prepublishOnly": "npm run check && npm run check:terrain"
43
- },
44
- "devDependencies": {
45
- "@types/node": "^26.4.1",
46
- "bun": "^1.4.0",
47
- "deno": "^2.9.6",
48
- "esbuild": "^0.28.2",
49
- "markdownlint-cli2": "^0.23.2",
50
- "oxlint": "^1.81.0",
51
- "oxlint-tsgolint": "^7.0.2001",
52
- "playwright": "^1.62.1",
53
- "typescript": "^7.0.2",
54
- "workerd": "^1.20260904.1"
41
+ "check:terrain": "node --test \"test/terrain/*.test.ts\"",
42
+ "prepublishOnly": "cd ../.. && npm run check && npm run check:terrain"
55
43
  },
56
44
  "publishConfig": {
57
45
  "access": "public"
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // @quo-systems/quo — the Being side. What a being author imports, if anything.
3
3
  export { Being, type AskSpec } from './being.ts';
4
- export { silence, isSilence, unreached, isUnreached } from './silence.ts';
4
+ export { silence, isSilence, unreached, isUnreached, word, isWord, wordOf, DOOR_WORDS, isDoorWord } from './silence.ts';
5
5
  export { digest, canonical } from './digest.ts';
6
6
  export { OWNER, PUBLIC, RESERVED_IDS } from './types.ts';
7
7
  export type * from './types.ts';
@@ -1,14 +1,32 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // Silence is one distinguished value. Null is an answer. Silence is no answer.
3
- import { UNREACHED_KEY, type Unreached } from './types.ts';
3
+ //
4
+ // The ward's words are the other distinguished values: what her ward tells
5
+ // her when no object came back and it knows why. Each is one frozen object
6
+ // under one symbol key, so a being cannot make one by accident, none of them
7
+ // crosses an edge as a value, and a being who compares against them needs
8
+ // no fourth shape. `unreached` is one of them.
9
+ import { WORD_KEY, type DoorWord, type Unreached, type Word, type WordName } from './types.ts';
4
10
 
5
11
  export const silence: unique symbol = Symbol.for('quo.silence');
6
12
  export const isSilence = (x: unknown): x is typeof silence => x === silence;
7
13
 
8
- // Unreached is the ward's own word: no far door was reached. It carries no
9
- // reason. Safe to retry, because nothing was delivered. A being cannot
10
- // produce it: a ward that sees it come out of a being treats it as silence.
11
- const UNREACHED: Unreached = Object.freeze({ [UNREACHED_KEY]: true });
12
- export const unreached = (): Unreached => UNREACHED;
13
- export const isUnreached = (x: unknown): x is Unreached =>
14
- x !== null && typeof x === 'object' && (x as Record<symbol, unknown>)[UNREACHED_KEY] === true;
14
+ // The words a door says to a key it has bound, and no other. They ride on
15
+ // the wire as `{ quo: word }`, so a reply is checked against this list.
16
+ export const DOOR_WORDS: readonly DoorWord[] = ['removed', 'absent', 'unannounced', 'repeated', 'threw'];
17
+ export const isDoorWord = (s: unknown): s is DoorWord => typeof s === 'string' && (DOOR_WORDS as readonly string[]).includes(s);
18
+
19
+ const WORDS = new Map<WordName, Word>();
20
+ export const word = <W extends WordName>(name: W): Word<W> => {
21
+ let w = WORDS.get(name);
22
+ if (!w) WORDS.set(name, (w = Object.freeze({ [WORD_KEY]: name })));
23
+ return w as Word<W>;
24
+ };
25
+ export const isWord = (x: unknown): x is Word => x !== null && typeof x === 'object' && typeof (x as Record<symbol, unknown>)[WORD_KEY] === 'string';
26
+ export const wordOf = (x: Word): WordName => x[WORD_KEY];
27
+
28
+ // Unreached: no far door was reached. Nothing is known to have been
29
+ // delivered, so asking again is safe. A being cannot produce it: a ward that
30
+ // sees a word come out of a being reads it as her having thrown.
31
+ export const unreached = (): Unreached => word('unreached');
32
+ export const isUnreached = (x: unknown): x is Unreached => isWord(x) && wordOf(x) === 'unreached';
@@ -44,11 +44,21 @@ export type Cells = {
44
44
  };
45
45
 
46
46
  export type Silence = typeof silence;
47
- export type Unreached = { readonly [K in typeof UNREACHED_KEY]: true };
48
- export const UNREACHED_KEY: unique symbol = Symbol.for('quo.unreached');
47
+
48
+ // The ward's words: what her ward says when no object came back and it knows
49
+ // why. A door says the first five to a key it has bound, and rides them on
50
+ // the wire as `{ quo: word }`; her own ward says the last four to her and
51
+ // they never leave the ward. Each is one frozen object under one symbol key,
52
+ // so no being makes one and none crosses an edge as a value.
53
+ export type DoorWord = 'removed' | 'absent' | 'unannounced' | 'repeated' | 'threw';
54
+ export type WardWord = 'unreached' | 'late' | 'invitation' | 'dropped';
55
+ export type WordName = DoorWord | WardWord;
56
+ export const WORD_KEY: unique symbol = Symbol.for('quo.word');
57
+ export type Word<W extends WordName = WordName> = { readonly [K in typeof WORD_KEY]: W };
58
+ export type Unreached = Word<'unreached'>;
49
59
 
50
60
  // What comes back from an ask or a knock.
51
- export type Answer = Json | Silence | Unreached;
61
+ export type Answer = Json | Silence | Word;
52
62
  // What a being answers.
53
63
  export type Reply = Json | Silence;
54
64
 
@@ -2,7 +2,7 @@
2
2
  // The fixture beings. Fixed, so that the ward is what varies. A printer, a
3
3
  // shop, a customer on the base class, and one raw being with no base at all.
4
4
  import { Being } from '../being/being.ts';
5
- import { isSilence, isUnreached } from '../being/silence.ts';
5
+ import { isSilence, isUnreached, isWord } from '../being/silence.ts';
6
6
  import type { Asker, Blueprint, Invitation, JsonObject, OccupantRecord, Reply, Stance } from '../being/types.ts';
7
7
 
8
8
  // A printer. Invites whom she is told to, prints for her occupants. Her
@@ -46,7 +46,7 @@ export class Shop extends Being {
46
46
 
47
47
  async keepPrinter(invitation: Invitation): Promise<boolean> {
48
48
  const bp = await this.knock(invitation);
49
- if (isSilence(bp) || isUnreached(bp)) return false;
49
+ if (isSilence(bp) || isWord(bp)) return false;
50
50
  this.cells.printerId = await this.take('prn', invitation);
51
51
  return true;
52
52
  }
@@ -55,7 +55,7 @@ export class Shop extends Being {
55
55
  const rec = this.occupant(asker);
56
56
  if (rec?.notes.tier === 'gold' && invitation) {
57
57
  const back = await this.knock(invitation as Invitation, 'hi');
58
- if (!isSilence(back) && !isUnreached(back)) await this.take(`vip-${asker.id}`, invitation as Invitation);
58
+ if (!isSilence(back) && !isWord(back)) await this.take(`vip-${asker.id}`, invitation as Invitation);
59
59
  }
60
60
  return { welcome: true };
61
61
  }
@@ -67,7 +67,7 @@ export class Shop extends Being {
67
67
  if (!printer) return { error: 'no printer' };
68
68
  const out = await printer.ask('print', { doc: `receipt for ${item}` });
69
69
  if (isUnreached(out)) return { error: 'printer unreachable', retry: true };
70
- if (isSilence(out)) return { error: 'printer refused' };
70
+ if (isSilence(out) || isWord(out)) return { error: 'printer refused' };
71
71
  const st = this.cells.standings[id!];
72
72
  if (st.seen !== st.digest) this.occupant(asker)!.notes.printerChanged = true;
73
73
  (this.cells.sales as JsonObject[]).push({ item: item ?? null, by: asker.id ?? null });
@@ -88,7 +88,7 @@ export class Customer extends Being {
88
88
  async join(invitation: Invitation) {
89
89
  const mine = (await this.invite('shop-back'))!;
90
90
  const out = await this.knock(invitation, 'hello', { invitation: mine as never });
91
- if (isSilence(out) || isUnreached(out)) return out; // nothing was born
91
+ if (isSilence(out) || isWord(out)) return out; // nothing was born
92
92
  await this.take('shop', invitation); // now, and only now
93
93
  return out;
94
94
  }
@@ -12,20 +12,22 @@
12
12
  //
13
13
  // So this chapter does not write the sequence. It keeps a model of the graph,
14
14
  // plays legal moves against it from three fixed seeds, and after every move
15
- // asks three things:
15
+ // asks two things:
16
16
  //
17
17
  // the ledger is a graph every arc the wards hold closes on both ends
18
18
  // the ledger is what it can do every arc in the model is asked, and answers
19
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
20
  //
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.
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 forty times a run, 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.
29
31
  //
30
32
  // A relation is two arcs and never one edge. The occupant arc is the host's,
31
33
  // born at invite; the standing arc is the guest's, born at take, and only
@@ -33,12 +35,13 @@
33
35
  // A model that kept one symmetric edge would be a wrong model, and would
34
36
  // agree happily with a ward that had drifted.
35
37
  import { assert } from './assert.ts';
36
- import { isSilence, isUnreached } from '../being/silence.ts';
38
+ import { isSilence, isUnreached, isWord, wordOf } from '../being/silence.ts';
37
39
  import type { Invitation } from '../being/types.ts';
38
40
  import { Member } from './beings.ts';
39
41
  import type { Census, Handle, Runner, World } from './index.ts';
40
42
 
41
43
  const HEX64 = /^[0-9a-f]{64}$/;
44
+ const said = (x: unknown, w: string): boolean => isWord(x) && wordOf(x) === w;
42
45
 
43
46
  // One occupant arc, as the host holds it. `guest` is null until somebody
44
47
  // knocks; `live` goes false when she is kicked. Both are the host's business
@@ -59,20 +62,6 @@ const rolls = (seed: number) => () => {
59
62
  return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
60
63
  };
61
64
 
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
65
  // ---- the ledger is a graph
77
66
 
78
67
  // Held after every move. Each line is a way the bookkeeping could stop being
@@ -161,10 +150,11 @@ export function estate(label: string, make: () => Promise<World>, { canDown, tes
161
150
  assert.deepEqual(await acme.being.knock(back, 'ping'), { pong: 'acme' });
162
151
  assert.equal(await acme.being.take('them', back), 'them');
163
152
 
164
- // 5. acme kicks her. her standing is still hers to hold -- she is never
165
- // told -- and it is silence from here on.
153
+ // 5. acme kicks her. her standing is still hers to hold, and she is not
154
+ // told until she asks: then the door, under the key it bound for her,
155
+ // says removed, and only to her.
166
156
  acme.being.occupants.remove('partner-1');
167
- assert.ok(isSilence(await partner.being.standings['the-client']!.ask('ping')));
157
+ assert.ok(said(await partner.being.standings['the-client']!.ask('ping'), 'removed'));
168
158
  assert.deepEqual(Object.keys(partner.cells.standings), ['the-client']); // she never learns
169
159
  assert.deepEqual(await acme.being.standings.them!.ask('ping'), { pong: 'acme' }); // the other arc never moved
170
160
 
@@ -174,7 +164,7 @@ export function estate(label: string, make: () => Promise<World>, { canDown, tes
174
164
  assert.notEqual(second.heir, first.heir);
175
165
  assert.deepEqual(await partner.being.knock(second, 'ping'), { pong: 'partner-2' });
176
166
  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')));
167
+ assert.ok(said(await partner.being.standings['the-client']!.ask('ping'), 'removed'));
178
168
  ledger(w.census(), undefined, 'the story');
179
169
  });
180
170
 
@@ -221,6 +211,9 @@ export function estate(label: string, make: () => Promise<World>, { canDown, tes
221
211
  assert.deepEqual(back, { pong: s.id }, `${guest} knocking ${s.host}/${s.id}`);
222
212
  s.guest = guest;
223
213
  s.answered = true;
214
+ } else if (!s.live && (s.guest === null || (s.guest === guest && (hold === undefined || hold.live)))) {
215
+ // kicked, and she speaks under a key the door bound for this seat: she hears why
216
+ assert.ok(said(back, 'removed'), `${guest} knocking kicked ${s.host}/${s.id} should hear removed`);
224
217
  } else assert.ok(isSilence(back), `${guest} knocking ${s.host}/${s.id} should be silence`);
225
218
  return `${guest} knocks ${s.host}/${s.id}`;
226
219
  });
@@ -304,7 +297,7 @@ export function estate(label: string, make: () => Promise<World>, { canDown, tes
304
297
  }
305
298
  const out = await standing!.ask('ping');
306
299
  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`);
300
+ else assert.ok(said(out, 'removed'), `step ${step}: ${h.owner}/${h.id} is kicked and should hear removed`);
308
301
  }
309
302
  };
310
303
 
@@ -327,13 +320,6 @@ export function estate(label: string, make: () => Promise<World>, { canDown, tes
327
320
  throw new Error(`${at(step)}\n -> ${e.message}`, { cause: e });
328
321
  });
329
322
  }
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
323
  });
338
324
  }
339
325
  }
@@ -19,7 +19,7 @@
19
19
  //
20
20
  // Every assertion here is the truth's. Nothing here knows how a ward is built.
21
21
  import { assert } from './assert.ts';
22
- import { silence, isSilence, isUnreached } from '../being/silence.ts';
22
+ import { silence, isSilence, isUnreached, isWord, wordOf } from '../being/silence.ts';
23
23
  import { digest } from '../being/digest.ts';
24
24
  import type { Asker, BeingClass, Cells, Invitation, Json, JsonObject } from '../being/types.ts';
25
25
  import { Printer, Shop, Customer, Echo, Member } from './beings.ts';
@@ -54,6 +54,8 @@ export type World = {
54
54
  export { Printer, Shop, Customer, Echo, Member };
55
55
 
56
56
  const HEX64 = /^[0-9a-f]{64}$/;
57
+ // Her ward's word, as she receives it.
58
+ const said = (x: unknown, w: string): boolean => isWord(x) && wordOf(x) === w;
57
59
 
58
60
  // The runner is handed in. Node passes `node:test`; a browser and an edge
59
61
  // worker pass a shim of their own. The suite is the ward's truth, and the
@@ -100,12 +102,12 @@ export function conform(label: string, make: () => Promise<World>, { canDown = t
100
102
  assert.equal(i1.ward, i2.ward);
101
103
  });
102
104
 
103
- t('remove before any knock: the heir is forgotten, a knock on it is silence', async () => {
105
+ t('remove before any knock: the heir is forgotten, and a knock on it by whoever holds it hears removed', async () => {
104
106
  const { w, shop, alice } = await world();
105
107
  const inv = await shop.being.invite();
106
108
  shop.being.occupants.remove('cust1');
107
109
  assert.equal(w.heir(inv), undefined);
108
- assert.ok(isSilence(await alice.being.join(inv)));
110
+ assert.ok(said(await alice.being.join(inv), 'removed'));
109
111
  assert.deepEqual(alice.cells.standings, {});
110
112
  });
111
113
 
@@ -189,16 +191,16 @@ export function conform(label: string, make: () => Promise<World>, { canDown = t
189
191
  assert.deepEqual(await alice.being.join(inv), { welcome: true }); // the same invitation still works
190
192
  const inv2 = await shop.being.invite();
191
193
  shop.being.occupants.remove('cust2'); // she changed her mind before the knock
192
- assert.ok(isSilence(await alice.being.join(inv2)));
194
+ assert.ok(said(await alice.being.join(inv2), 'removed'));
193
195
  assert.deepEqual(Object.keys(alice.cells.standings), ['shop']);
194
196
  });
195
197
 
196
- t('take without a knock, or after a silent knock, births nothing', async () => {
198
+ t('take without a knock, or after a knock that was not answered, births nothing', async () => {
197
199
  const { shop, alice } = await world();
198
200
  const inv = await shop.being.invite();
199
201
  assert.equal(await alice.being.take('shop', inv), null);
200
202
  shop.being.occupants.remove('cust1');
201
- assert.ok(isSilence(await alice.being.knock(inv, 'hello')));
203
+ assert.ok(said(await alice.being.knock(inv, 'hello'), 'removed'));
202
204
  assert.equal(await alice.being.take('shop', inv), null);
203
205
  assert.deepEqual(alice.cells.standings, {});
204
206
  });
@@ -374,16 +376,16 @@ export function conform(label: string, make: () => Promise<World>, { canDown = t
374
376
  assert.deepEqual(await alice.being.buy('hat'), { ok: true, receipt: 'receipt for hat' });
375
377
  });
376
378
 
377
- t('a removed occupant is silence to the far side and her heir is forgotten; a dropped standing is gone', async () => {
379
+ t('a removed occupant hears removed under the key the door bound, her heir is forgotten; a dropped standing is gone', async () => {
378
380
  const { w, alice, shop } = await world();
379
381
  const inv = await shop.being.invite();
380
382
  await alice.being.join(inv);
381
383
  shop.being.occupants.remove('cust1');
382
384
  assert.equal(w.heir(inv), undefined);
383
- assert.ok(isSilence(await alice.being.buy('hat')));
385
+ assert.ok(said(await alice.being.buy('hat'), 'removed'));
384
386
  alice.being.standings.remove('shop');
385
387
  assert.equal(alice.being.standings.shop, undefined);
386
- assert.ok(isSilence(await alice.being.knock(inv, 'hello'))); // and a re-knock finds no heir
388
+ assert.ok(isSilence(await alice.being.knock(inv, 'hello'))); // her bound key went with the standing: a re-knock is as the heir, which is spent, a stranger
387
389
  const again = await shop.being.invite(); // cust2. a new heir
388
390
  assert.notEqual(again.heir, inv.heir);
389
391
  assert.deepEqual(await alice.being.knock(again, 'hello'), { welcome: true });
@@ -398,7 +400,7 @@ export function conform(label: string, make: () => Promise<World>, { canDown = t
398
400
  assert.equal((shop.cells.sales as unknown[]).length, 1);
399
401
  });
400
402
 
401
- t('a being that throws is silence outside; the ward stays up', async () => {
403
+ t('a being that throws is the word threw to whom the door has bound; the ward stays up', async () => {
402
404
  const w = await make();
403
405
  class Bomb {
404
406
  s: unknown;
@@ -413,7 +415,7 @@ export function conform(label: string, make: () => Promise<World>, { canDown = t
413
415
  const x = await w.boot<Bomb>('X', Bomb);
414
416
  const a = await w.boot<Customer>('A', Customer);
415
417
  const inv = (await (x.being.s as Customer['stance']).occupants.invite('a'))!;
416
- assert.ok(isSilence(await a.being.knock(inv, 'any')));
418
+ assert.ok(said(await a.being.knock(inv, 'any'), 'threw'));
417
419
  assert.deepEqual(await a.being.knock(inv), { asks: [], notes: {} });
418
420
  });
419
421
 
@@ -473,7 +475,7 @@ export function conform(label: string, make: () => Promise<World>, { canDown = t
473
475
  assert.deepEqual(pub.cells.occupants, {}); // she never invited anyone
474
476
  if (!w.oneWard) assert.ok(isSilence(await a.being.knock(at(priv), 'time'))); // a ward with no public being: silence
475
477
  assert.ok(isUnreached(await a.being.knock({ ward: 'a'.repeat(128) }, 'time'))); // no door anywhere: unreached
476
- assert.ok(isSilence(await a.being.knock({ ward: 'not a pk' }, 'time'))); // not even an address
478
+ assert.ok(said(await a.being.knock({ ward: 'not a pk' }, 'time'), 'invitation')); // not even an address
477
479
  assert.equal(await a.being.take('clock', at(pub)), 'clock');
478
480
  assert.deepEqual(await a.being.standings.clock!.ask('time'), { time: 12, got: {} });
479
481
  assert.equal(a.cells.standings.clock.seen, await digest((await a.being.standings.clock!.ask()) as Json));
@@ -45,6 +45,7 @@ export class Harbor {
45
45
  readonly doors = new Map<string, WardPointers['door']>(); // ward pk -> door
46
46
  readonly reaches = new Map<string, Bound>(); // the directory: foreign ward pk -> reach
47
47
  readonly down = new Set<string>(); // pks this harbor will not reach right now: weather, for a test
48
+ readonly refused = new Map<string, number>(); // own ward pk -> arrivals its door would not admit. what a terrain does with it is its own
48
49
  readonly classes: Record<string, BeingClass> = {}; // classes handed in-process, beside the loader's
49
50
  // The dialers' sockets, in the order they were dialed: where a pk nobody
50
51
  // here knows is sent, because the listener a harbor dialed is a rendezvous
@@ -54,6 +55,17 @@ export class Harbor {
54
55
  // carries it: with a single slot, a harbor holding two lines answered
55
56
  // unreached for a pk the other line could have reached.
56
57
  readonly fallbacks: Reach[] = [];
58
+ // How a dialer says what this harbor holds now. An announce is a claim and
59
+ // a claim is worth nothing until the door behind it answers a probe, so
60
+ // saying it again costs a proof and buys nothing to an impostor. What is
61
+ // not free is never saying it again: a ward booted, adopted or dropped
62
+ // after a line opened would be announced only by the next line, and a line
63
+ // that does not drop never opens again, so it would be unreachable through
64
+ // that listener for as long as the socket stayed healthy.
65
+ readonly announcers = new Set<() => void>();
66
+ #announce(): void {
67
+ for (const say of this.announcers) say();
68
+ }
57
69
 
58
70
  constructor(store: Store, loader: Loader = async () => ({})) {
59
71
  this.store = store;
@@ -71,10 +83,18 @@ export class Harbor {
71
83
 
72
84
  // ---- the directory
73
85
 
86
+ // Bytes to one of this harbor's own doors, and the one bit the door says
87
+ // beside its reply, counted.
88
+ async #own(pk: string, door: WardPointers['door'], bytes: Uint8Array): Promise<Uint8Array> {
89
+ const r = await door(bytes);
90
+ if (!r.heard) this.refused.set(pk, (this.refused.get(pk) ?? 0) + 1);
91
+ return new Uint8Array(r.bytes);
92
+ }
93
+
74
94
  async carry(pk: string, bytes: Uint8Array): Promise<Uint8Array | undefined> {
75
95
  if (this.down.has(pk)) return undefined;
76
96
  const door = this.doors.get(pk);
77
- if (door) return new Uint8Array(await door(bytes));
97
+ if (door) return this.#own(pk, door, bytes);
78
98
  const bound = this.reaches.get(pk);
79
99
  if (bound) return bound.reach.carry(pk, bytes);
80
100
  for (const reach of this.fallbacks) {
@@ -87,7 +107,7 @@ export class Harbor {
87
107
  async deliver(pk: string, bytes: Uint8Array): Promise<Uint8Array | undefined> {
88
108
  if (this.down.has(pk)) return undefined;
89
109
  const door = this.doors.get(pk);
90
- if (door) return new Uint8Array(await door(bytes));
110
+ if (door) return this.#own(pk, door, bytes);
91
111
  const bound = this.reaches.get(pk);
92
112
  return bound?.held ? bound.reach.carry(pk, bytes) : undefined;
93
113
  }
@@ -163,6 +183,7 @@ export class Harbor {
163
183
  await h.save();
164
184
  this.wards.delete(name);
165
185
  this.doors.delete(h.pk);
186
+ this.#announce(); // this harbor no longer claims it: a listener unbinds a pk an announce stops naming
166
187
  }
167
188
  return this.store.take(name);
168
189
  }
@@ -214,6 +235,7 @@ export class Harbor {
214
235
  this.wards.set(name, hosted);
215
236
  this.doors.set(hosted.pk, door);
216
237
  await save();
238
+ this.#announce(); // reachable through every line this harbor already holds, not only the next one
217
239
  return hosted;
218
240
  }
219
241
  }
@@ -33,6 +33,7 @@ export function dial(harbor: Harbor, url: string): Dialer {
33
33
  void harbor.bind(far, s, false); // the listener's claims, each proven at its door: reached through this socket, not held for others
34
34
  },
35
35
  (why) => {
36
+ harbor.announcers.delete(say);
36
37
  harbor.unbind(s);
37
38
  if (d.socket === s) d.socket = null;
38
39
  const at = harbor.fallbacks.indexOf(s);
@@ -46,9 +47,14 @@ export function dial(harbor: Harbor, url: string): Dialer {
46
47
  },
47
48
  );
48
49
  harbor.fallbacks.push(s); // from the moment it is dialed: an ask made before the handshake waits on the socket, and is not unreached
50
+ // What this harbor holds, said when the line opens and again whenever it
51
+ // changes. A ward booted or adopted after the handshake is announced on
52
+ // the line already in hand, and a dropped one stops being claimed at once.
53
+ const say = () => s.announce([...harbor.doors.keys()]);
49
54
  line.addEventListener('open', () => {
50
55
  d.socket = s;
51
- s.announce([...harbor.doors.keys()]);
56
+ harbor.announcers.add(say);
57
+ say();
52
58
  });
53
59
  };
54
60
  d.close = () => {
@@ -15,6 +15,7 @@ export class MemoryHarbor {
15
15
  readonly doors = new Map<string, WardPointers['door']>(); // ward pk -> door
16
16
  readonly down = new Set<string>(); // ward pks this harbor cannot reach right now
17
17
  readonly wire: string[] = []; // every byte string that ever crossed, as hex. for inspection
18
+ readonly refused = new Map<string, number>(); // ward pk -> arrivals its door would not admit: strangers, counted
18
19
  readonly peers = new Set<MemoryHarbor>();
19
20
  cut = false;
20
21
  readonly partitions = new Map<string, Record<string, unknown>>(); // seed -> memory. the harbor keeps it and reads nothing
@@ -25,7 +26,11 @@ export class MemoryHarbor {
25
26
  async route(farPk: string, bytes: Uint8Array): Promise<Uint8Array | undefined> {
26
27
  if (this.down.has(farPk)) return undefined;
27
28
  const door = this.doors.get(farPk);
28
- if (door) return door(bytes);
29
+ if (door) {
30
+ const r = await door(bytes);
31
+ if (!r.heard) this.refused.set(farPk, (this.refused.get(farPk) ?? 0) + 1);
32
+ return r.bytes;
33
+ }
29
34
  const peer = [...this.peers].find((h) => h.doors.has(farPk) && !h.down.has(farPk));
30
35
  if (!peer || this.cut) return undefined; // nothing. the harbor has no word silence
31
36
  return peer.route(farPk, bytes);