@quo-systems/quo 0.2.13 → 0.2.15

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 (55) hide show
  1. package/README.md +10 -4
  2. package/SPEC.md +354 -536
  3. package/dist/being/being.d.ts +1 -0
  4. package/dist/being/being.js +8 -3
  5. package/dist/being/index.d.ts +1 -1
  6. package/dist/being/index.js +1 -1
  7. package/dist/being/types.d.ts +2 -0
  8. package/dist/being/types.js +17 -0
  9. package/dist/conformance/beings.d.ts +41 -0
  10. package/dist/conformance/beings.js +28 -2
  11. package/dist/conformance/index.d.ts +10 -1
  12. package/dist/conformance/index.js +162 -6
  13. package/dist/harbor/core.d.ts +4 -2
  14. package/dist/harbor/core.js +25 -4
  15. package/dist/harbor/index.d.ts +1 -1
  16. package/dist/harbor/memory.d.ts +3 -3
  17. package/dist/harbor/memory.js +6 -3
  18. package/dist/harbor/reach.js +1 -1
  19. package/dist/ward/cells.d.ts +2 -0
  20. package/dist/ward/cells.js +60 -11
  21. package/dist/ward/door.d.ts +1 -0
  22. package/dist/ward/door.js +29 -10
  23. package/dist/ward/ground.d.ts +3 -1
  24. package/dist/ward/ground.js +1 -1
  25. package/dist/ward/heirs.js +8 -1
  26. package/dist/ward/index.d.ts +2 -2
  27. package/dist/ward/index.js +3 -3
  28. package/dist/ward/owner.js +49 -8
  29. package/dist/ward/seal.d.ts +1 -0
  30. package/dist/ward/seal.js +10 -2
  31. package/dist/ward/stance.d.ts +1 -0
  32. package/dist/ward/stance.js +62 -4
  33. package/dist/ward/ward.js +5 -0
  34. package/package.json +5 -3
  35. package/quo-kit.md +523 -0
  36. package/src/being/being.ts +8 -3
  37. package/src/being/index.ts +1 -1
  38. package/src/being/types.ts +34 -0
  39. package/src/conformance/beings.ts +25 -2
  40. package/src/conformance/estate.ts +9 -9
  41. package/src/conformance/index.ts +204 -7
  42. package/src/conformance/reach.ts +1 -1
  43. package/src/harbor/core.ts +26 -5
  44. package/src/harbor/index.ts +1 -1
  45. package/src/harbor/memory.ts +7 -4
  46. package/src/harbor/reach.ts +1 -1
  47. package/src/ward/cells.ts +59 -10
  48. package/src/ward/door.ts +27 -9
  49. package/src/ward/ground.ts +39 -11
  50. package/src/ward/heirs.ts +7 -1
  51. package/src/ward/index.ts +4 -4
  52. package/src/ward/owner.ts +45 -10
  53. package/src/ward/seal.ts +11 -2
  54. package/src/ward/stance.ts +60 -4
  55. package/src/ward/ward.ts +6 -1
@@ -3,7 +3,7 @@
3
3
  // shop, a customer on the base class, a maker, a member, and one raw being
4
4
  // with no base at all.
5
5
  import { Being } from '../being/being.ts';
6
- import { answered, isUnreached } from '../being/silence.ts';
6
+ import { answered, isSilence, isUnreached, isWord, wordOf } from '../being/silence.ts';
7
7
  import { invitationArgs } from '../being/types.ts';
8
8
  import type { Ask, Asker, Blueprint, Invitation, Json, JsonObject, OccupantRecord, Reply, Stance } from '../being/types.ts';
9
9
 
@@ -138,6 +138,7 @@ export class Maker extends Being {
138
138
  static override asks = {
139
139
  open: { input: { type: 'object', properties: { class: { type: 'string' }, key: { type: 'string' }, id: { type: 'string' } }, required: ['class', 'key'] } },
140
140
  say: { input: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] } },
141
+ borrow: { input: { type: 'object', properties: { name: { type: 'string' }, id: { type: 'string' } }, required: ['name', 'id'] } },
141
142
  };
142
143
  async open({ class: className, key, id }: JsonObject) {
143
144
  const made = await this.boot(className as string, key as string, id as string | undefined);
@@ -148,13 +149,35 @@ export class Maker extends Being {
148
149
  const standing = this.standings[id as string];
149
150
  return { said: standing === undefined ? null : ((await standing.ask('ping')) as Json) };
150
151
  }
152
+ // Making's other half: a standing at something the box already has, by the
153
+ // name the box knows it under. She is handed the id and never the value.
154
+ async borrow({ name, id }: JsonObject) {
155
+ return { lent: await this.lend(name as string, id as string) };
156
+ }
151
157
  }
152
158
 
153
159
  export class Member extends Being {
154
160
  static override cells = { heard: [] as JsonObject[] };
155
- static override asks = { ping: { input: { type: 'object' } } };
161
+ static override asks = {
162
+ ping: { input: { type: 'object' } },
163
+ relay: { input: { type: 'object', properties: { id: { type: 'string' }, method: { type: 'string' }, time: { type: 'number' } }, required: ['id'] } },
164
+ };
156
165
  ping(_args: JsonObject, asker: Asker) {
157
166
  (this.cells.heard as JsonObject[]).push({ from: asker.id ?? null });
158
167
  return { pong: asker.id ?? null };
159
168
  }
169
+ // She asks on one of her standings from inside her own answer, and says
170
+ // what came back as a word rather than as the word itself, since a word is
171
+ // her ward's value and never a thing that crosses. `time` is what she is
172
+ // willing to wait, which is the whole of what the third argument is for.
173
+ // Two members holding each other and relaying make a cycle, and a cycle is
174
+ // how a suite that owns no clock can see a wait end.
175
+ async relay({ id, method, args, time }: JsonObject) {
176
+ const standing = this.standings[id as string];
177
+ if (standing === undefined) return { said: 'no standing', out: null };
178
+ const out = await standing.ask(method as string | undefined, (args ?? {}) as JsonObject, time === undefined ? undefined : { time: time as number });
179
+ if (isSilence(out)) return { said: 'silence', out: null };
180
+ if (isWord(out)) return { said: wordOf(out), out: null };
181
+ return { said: null, out: out as Json };
182
+ }
160
183
  }
@@ -76,31 +76,31 @@ function ledger(c: Census, before: Census | undefined, where: string, g?: Graph)
76
76
  // is caught late, if the sweep happens to ask the affected arc, or never.
77
77
  if (g) {
78
78
  for (const s of g.seats) {
79
- const heir = c.wards[c.binds[s.host]!.ward]!.heirs[s.inv.heir!];
79
+ const heir = c.wards[c.binds[s.host].ward].heirs[s.inv.heir!];
80
80
  if (!s.live) {
81
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`));
82
+ assert.ok(!(s.id in c.binds[s.host].occupants), at(`${s.host} kicked ${s.id} and still holds the occupant`));
83
83
  continue;
84
84
  }
85
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}`));
86
+ assert.equal(heir.id, s.id, at(`the heir for ${s.host}/${s.id} is filed under ${heir.id}`));
87
87
  // A seat nobody has knocked is a heir that has never spoken: fresh, and
88
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}`));
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
91
  }
92
92
  for (const h of g.holds) {
93
- const standing = c.binds[h.owner]!.standings[h.id];
93
+ const standing = c.binds[h.owner].standings[h.id];
94
94
  if (!h.live) {
95
95
  assert.ok(!standing, at(`${h.owner} dropped ${h.id} and the standing is still there`));
96
96
  continue;
97
97
  }
98
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`));
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
100
  // The count she has spent, against the number the far door honoured.
101
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}`));
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
104
  }
105
105
  // Every seat and hold the model knows about, and no relation the ward
106
106
  // holds that the model never made.
@@ -3,7 +3,10 @@
3
3
  //
4
4
  // A ward is handed in as a probe:
5
5
  //
6
- // make() -> world
6
+ // make({ lends }) -> world. `lends` maps a name the box lends to the
7
+ // class of the being it lends under it; the probe
8
+ // stands one of each in a ward its harbor roots,
9
+ // and every ward it boots is grounded to lend them
7
10
  // world.boot(key, Class, cells?, { isPublic }) -> handle
8
11
  // world.heir(invitation) -> { current, announced, fresh } | undefined
9
12
  // what the inviting ward holds for that heir
@@ -19,7 +22,7 @@
19
22
  //
20
23
  // Every assertion here is the truth's. Nothing here knows how a ward is built.
21
24
  import { assert } from './assert.ts';
22
- import { silence, isSilence, isUnreached, told } from '../being/silence.ts';
25
+ import { silence, answered, isSilence, isUnreached, told } from '../being/silence.ts';
23
26
  import { digest } from '../being/digest.ts';
24
27
  import type { Asker, BeingClass, Cells, Invitation, Json, JsonObject, Reply, Stance } from '../being/types.ts';
25
28
  import { Printer, Shop, Customer, Echo, Maker, Member } from './beings.ts';
@@ -56,6 +59,25 @@ export type World = {
56
59
  // this one. `key` says which ward: a being of it, and never the ward's pk,
57
60
  // because a topology may put every being behind a ward of its own.
58
61
  owner(key: string): (method?: string, args?: JsonObject) => Promise<unknown>;
62
+ // The partition of the ward a being lives in, as values, and that ward
63
+ // booted again from one. Together they are the obligation "a ward can be
64
+ // written down and stood up again from what was written", which is what a
65
+ // restart is and what a migration rides on, and they are what lets this
66
+ // suite reach the count, the span and the mark without a hand to build
67
+ // bytes with: rewind a caller's own numbers in her partition, stand her
68
+ // ward up again, and her next ask carries a number the far door has
69
+ // already honoured. What `restart` is given it keeps; given nothing it
70
+ // keeps what the ward had.
71
+ // They are `Standing`, below, and optional here: required in prose and of
72
+ // any harness that says it can, which is the shape every obligation in this
73
+ // suite has. A harness that cannot says `canRestart: false` at the call and
74
+ // skips the chapters that need it, rather than writing a member that lies.
75
+ } & Partial<Standing>;
76
+
77
+ // What a harness that can stand a ward up again offers.
78
+ export type Standing = {
79
+ partition(key: string): JsonObject;
80
+ restart(key: string, partition?: JsonObject): Promise<void>;
59
81
  };
60
82
 
61
83
  export { Printer, Shop, Customer, Echo, Maker, Member };
@@ -70,14 +92,27 @@ export const HEX128 = /^[0-9a-f]{128}$/; // a ward pk: the signing pk then the p
70
92
  // Her ward's word, as she receives it.
71
93
  const said = (x: unknown, w: string): boolean => told(x) === w;
72
94
 
95
+ // A world whose harness stands a ward up again. The two members are optional
96
+ // on the probe and required of a harness that says it can, so a chapter that
97
+ // needs them says so here rather than reaching past a maybe at every line.
98
+ const able = (w: World): World & Standing => w as World & Standing;
99
+
73
100
  // The runner is handed in. Node passes `node:test`; a browser and an edge
74
101
  // worker pass a shim of their own. The suite is the ward's truth, and the
75
102
  // ward's truth must be checkable wherever a ward runs, not only here.
76
103
  export type Runner = (name: string, opts: { skip?: string | false }, fn: () => Promise<void> | void) => void;
77
104
 
78
- export function conform(label: string, make: () => Promise<World>, { canDown = true, test }: { canDown?: boolean; test: Runner }) {
105
+ // What the probe is asked to build. `lends` is the box's half of the ground's
106
+ // seventh member: a name the harbor lends, and the class of the being it
107
+ // lends under it. A harness that stands none is handed nothing and grounds
108
+ // its wards to lend nothing, which is what a harbor with nothing to lend is.
109
+ export type Made = { lends?: Record<string, string> };
110
+
111
+ export function conform(label: string, make: (made?: Made) => Promise<World>, { canDown = true, canLend = true, canRestart = true, test }: { canDown?: boolean; canLend?: boolean; canRestart?: boolean; test: Runner }) {
79
112
  const t = (name: string, fn: () => Promise<void> | void) => test(`[${label}] ${name}`, {}, fn);
80
113
  const down = (name: string, fn: () => Promise<void> | void) => test(`[${label}] ${name}`, { skip: !canDown && 'nothing to cut inside one ward' }, fn);
114
+ const lends = (name: string, fn: () => Promise<void> | void) => test(`[${label}] ${name}`, { skip: !canLend && 'this harness stands no ward of its harbor to lend from' }, fn);
115
+ const kept = (name: string, fn: () => Promise<void> | void) => test(`[${label}] ${name}`, { skip: !canRestart && 'this harness cannot hand a ward a partition and stand it up again' }, fn);
81
116
 
82
117
  async function world({ printerInvitesShop = true } = {}) {
83
118
  const w = await make();
@@ -220,8 +255,12 @@ export function conform(label: string, make: () => Promise<World>, { canDown = t
220
255
  // alice hosts here rather than the shop, because the shop's own `invite`
221
256
  // takes a tier and mints the id itself. This one is the stance's, which
222
257
  // is where the rule lives.
223
- const { alice, bob } = await world();
224
- for (const id of ['OWNER', 'PUBLIC', 'knock', 'take', 'remove']) {
258
+ const { w, alice, bob } = await world();
259
+ // `__proto__` is in the list because a partition cannot hold it. A kit
260
+ // that refuses it only where it writes her cells writes the ward's half
261
+ // first and then throws, which leaves one relation in two standings and a
262
+ // throw where this table promises a null.
263
+ for (const id of ['OWNER', 'PUBLIC', 'knock', 'take', 'remove', '__proto__']) {
225
264
  assert.equal(await alice.being.invite(id), null, `${id} is not hers to mint`);
226
265
  assert.equal(Object.hasOwn(alice.cells.occupants, id), false, `and nothing was written for ${id}`);
227
266
 
@@ -230,6 +269,9 @@ export function conform(label: string, make: () => Promise<World>, { canDown = t
230
269
  assert.deepEqual(await bob.being.knock(inv, 'hi'), { heard: 'hi' }, `the knock for ${id} was answered`);
231
270
  assert.equal(await bob.being.take(id, inv), null, `take refuses ${id} for the same reason`);
232
271
  assert.equal(Object.hasOwn(bob.cells.standings, id), false, `and nothing was written for ${id}`);
272
+ // The ward's half too, and not only hers: a standing filed here with no
273
+ // record of it in her cells is a lane nobody can see and nobody removes.
274
+ assert.equal(Object.hasOwn(w.census().binds.B!.standings, id), false, `and the ward wrote no keys for ${id}`);
233
275
  assert.equal(await bob.being.take(`as-${id}`, inv), `as-${id}`, 'and an ordinary id takes the same invitation');
234
276
  }
235
277
  // And the three calls are still the calls, not records that shadowed them.
@@ -245,12 +287,12 @@ export function conform(label: string, make: () => Promise<World>, { canDown = t
245
287
  const { w, shop, alice } = await world();
246
288
  const ask = w.owner('S');
247
289
  const bp = (await ask()) as { asks: { name: string; description?: string; input?: JsonObject }[]; notes: { pk: string; beings: JsonObject } };
248
- assert.deepEqual(bp.asks.map((a) => a.name), ['boot', 'public', 'invite', 'knock', 'remove', 'unboot'], 'six asks, in this order');
290
+ assert.deepEqual(bp.asks.map((a) => a.name), ['boot', 'public', 'invite', 'knock', 'remove', 'unboot', 'ask'], 'seven asks, in this order');
249
291
  for (const a of bp.asks) {
250
292
  assert.equal(typeof a.description, 'string', `${a.name} says what it is for`);
251
293
  assert.equal((a.input as { type: string }).type, 'object', `${a.name} takes one object`);
252
294
  }
253
- assert.match(bp.notes.pk as string, HEX128);
295
+ assert.match(bp.notes.pk, HEX128);
254
296
  assert.equal(typeof bp.notes.beings, 'object');
255
297
  assert.deepEqual(await ask('nosuch'), { error: 'unknown ask' });
256
298
 
@@ -289,6 +331,36 @@ export function conform(label: string, make: () => Promise<World>, { canDown = t
289
331
  assert.ok(said(await alice.being.standings['via-owner']!.ask('hello'), 'removed'), 'and her occupant hears removed at the door');
290
332
  });
291
333
 
334
+ // The owner's seventh ask: reaching into a being and asking her. It is the
335
+ // third asker of the ward-to-being edge, and the ward is the only one who
336
+ // fills it in: nothing outside a ward names OWNER, and no side holds a
337
+ // being's answer to judge it again. Strictly less than the unboot beside
338
+ // it, which takes her out with every relation she holds.
339
+ t('the owner asks a being of the ward, as owner or, with no being named, as nobody at the public being', async () => {
340
+ const { w } = await world();
341
+ const ask = w.owner('S');
342
+ const echo = w.owner('E');
343
+ await w.boot('E', Echo);
344
+
345
+ // Named: she is asked as the owner, and hears the owner's name.
346
+ assert.deepEqual(await echo('ask', { being: 'E', method: 'echo', args: { x: 1 } }), { from: 'OWNER', x: 1 }, 'the ward names the asker, and the name is the owner');
347
+ assert.deepEqual(bp(await ask('ask', { being: 'S' })).asks.map((a) => a.name), ['hello', 'buy'], 'no method is her describe, and a gate that reads an occupant record shows the owner nothing: refund is hers to nobody here');
348
+ assert.deepEqual(await ask('ask', { being: 'S', method: 'hello' }), { welcome: true });
349
+ assert.deepEqual(await ask('ask', { being: 'S', method: 'nosuch' }), { error: 'unknown ask' }, 'her own answer, as any asker gets it');
350
+
351
+ // Nobody named, and nobody home: one refusal, and never a fourth word.
352
+ assert.deepEqual(await echo('ask', {}), { error: 'no such being' }, 'a ward with no public being has nobody to ask as nobody');
353
+ assert.deepEqual(await echo('public', { key: 'E' }), { public: 'E' });
354
+ assert.deepEqual(await echo('ask', { method: 'echo', args: { x: 2 } }), { from: null, x: 2 }, 'with no being named it is the public being, asked as nobody');
355
+
356
+ // What it refuses, each in an object, because the owner hears objects.
357
+ const pk = (bp(await ask()) as unknown as { notes: { pk: string } }).notes.pk;
358
+ assert.deepEqual(await ask('ask', { being: 'nobody' }), { error: 'no such being' });
359
+ assert.deepEqual(await ask('ask', { being: pk }), { error: 'no such being' }, 'the ward is a being to nobody outside, here as at invite and unboot');
360
+ assert.deepEqual(await ask('ask', { being: 42 }), { error: 'no such being' }, 'a name is a string or it is nothing');
361
+ assert.deepEqual(await ask('ask', { being: 'S', method: 42 }), { error: 'an ask is named by a word' }, 'and so is an ask');
362
+ });
363
+
292
364
  // The one public being, marked and unmarked through the owner. A second
293
365
  // one would leave the first holding every relation she had, reachable by
294
366
  // nobody at the bare pk and told by nobody that she had been replaced.
@@ -341,6 +413,131 @@ export function conform(label: string, make: () => Promise<World>, { canDown = t
341
413
  assert.deepEqual(await maker.being.say({ id: 'nothing' }), { said: null });
342
414
  });
343
415
 
416
+ // The count, reached the one way a suite that owns no bytes can reach it:
417
+ // a caller's numbers are in her own partition, so winding them back and
418
+ // standing her ward up again makes her next ask carry a number the far
419
+ // door has already honoured. A kit with no once-only delivery answers it
420
+ // and passes every other chapter here clean.
421
+ kept('a number already honoured is repeated: the far being is not asked twice, and nothing is written for it', async () => {
422
+ const { w, shop, alice } = await world();
423
+ const inv = await shop.being.invite();
424
+ assert.deepEqual(await alice.being.join(inv), { welcome: true });
425
+ assert.deepEqual(await alice.being.buy('a hat'), { ok: true, receipt: 'receipt for a hat' });
426
+ const sold = (shop.cells.sales as JsonObject[]).length;
427
+
428
+ // Her keys for the shop, wound back to the number she has just spent.
429
+ const kept = able(w);
430
+ const p = kept.partition('A');
431
+ const keys = ((p.bind as JsonObject).A as JsonObject).standings as JsonObject;
432
+ const mine = keys.shop as JsonObject;
433
+ const back = (mine.seq as number) - 1;
434
+ mine.seq = back;
435
+ await kept.restart('A', p);
436
+
437
+ const again = await alice.being.buy('a second hat');
438
+ assert.ok(said(again, 'repeated'), `a number already honoured is repeated, not ${JSON.stringify(again)}`);
439
+ assert.equal((shop.cells.sales as JsonObject[]).length, sold, 'and she was never asked');
440
+ // The count carries on from where it was: the next number is fresh.
441
+ assert.deepEqual(await alice.being.buy('a third hat'), { ok: true, receipt: 'receipt for a third hat' });
442
+ });
443
+
444
+ // A restart is silent by construction: the ward comes back on the same
445
+ // seed with the same pk, and every relation it holds still points at it.
446
+ // What must survive with it is what the door has honoured, or a restart is
447
+ // a stretch of time in which every number can be replayed.
448
+ kept('a restart is silent: same pk, every relation still stands, and the far door still refuses what it spent', async () => {
449
+ const { w, shop, alice } = await world();
450
+ const inv = await shop.being.invite();
451
+ assert.deepEqual(await alice.being.join(inv), { welcome: true });
452
+ assert.deepEqual(await alice.being.buy('a hat'), { ok: true, receipt: 'receipt for a hat' });
453
+ const pk = shop.pk;
454
+ const able2 = able(w);
455
+ const mark = JSON.stringify(able2.partition('S').heirs);
456
+
457
+ await able2.restart('S');
458
+ assert.equal(shop.pk, pk, 'a ward that came back changed its pk');
459
+ assert.equal(JSON.stringify(able2.partition('S').heirs), mark, 'and it came back holding what it had honoured');
460
+ assert.deepEqual(await alice.being.buy('a second hat'), { ok: true, receipt: 'receipt for a second hat' }, 'the relation goes on, and she was never told');
461
+ assert.deepEqual(alice.cells.standings.shop!.id, 'shop', 'her side was not told either');
462
+ });
463
+
464
+ // Notes are the one thing on a record that a being seeds and a ward keeps
465
+ // without ever reading. A kit that files what it was handed rather than a
466
+ // copy of it leaves the seeder a hand inside her cells: it writes without
467
+ // the ward being told, so what a restart brings back is not what she read.
468
+ t('notes are values and are kept as a copy: what the seeder still holds is not what the record holds', async () => {
469
+ const { alice } = await world();
470
+ const seeded = { tier: { deep: 'gold' } } as unknown as JsonObject;
471
+ assert.ok((await alice.being.invite('noted', seeded)) !== null, 'she minted with notes');
472
+ (seeded.tier as JsonObject).deep = 'moved';
473
+ assert.deepEqual(alice.cells.occupants.noted!.notes, { tier: { deep: 'gold' } }, 'the record is the copy, all the way down');
474
+ // And notes no harbor could write back are no invitation, for the reason
475
+ // every other write is refused: a record it could not keep.
476
+ assert.equal(await alice.being.invite('bad-notes', { n: 0 / 0 } as unknown as JsonObject), null);
477
+ assert.equal(Object.hasOwn(alice.cells.occupants, 'bad-notes'), false, 'and nothing was written for it');
478
+ });
479
+
480
+ // The wait, and the one thing a kit can get wrong that nothing else here
481
+ // would notice. A ward bounds every ask itself, so a relation cycle ends:
482
+ // A asks B, B asking back holds A's lane, and the third ask can never be
483
+ // answered. A kit with no bound waits for ever and hangs, and every other
484
+ // chapter in this suite would still pass it. The word is `late`, which
485
+ // promises nothing either way, and the relation lives afterwards.
486
+ t('the wait is bounded, so a cycle ends: the ask that cannot be answered is late, and the relation lives', async () => {
487
+ const { w } = await world();
488
+ const a = await w.boot<Member>('MA', Member);
489
+ const b = await w.boot<Member>('MB', Member);
490
+ const toB = (await b.being.invite('from-a'))!;
491
+ const toA = (await a.being.invite('from-b'))!;
492
+ assert.ok(answered(await a.being.knock(toB, 'ping')), 'a knocks b');
493
+ assert.equal(await a.being.take('b', toB), 'b');
494
+ assert.ok(answered(await b.being.knock(toA, 'ping')), 'b knocks a');
495
+ assert.equal(await b.being.take('a', toA), 'a');
496
+
497
+ // A asks B; B, answering, asks A back; A, answering that, asks B on the
498
+ // lane the first ask still holds. Every link is a legal ask.
499
+ const out = (await a.being.relay({ id: 'b', method: 'relay', args: { id: 'a', method: 'relay', args: { id: 'b', method: 'ping' }, time: 200 } })) as { said: string | null; out: { said: string | null } | null };
500
+ assert.equal(out.said, null, 'the outer ask was answered');
501
+ assert.equal(out.out?.said, 'late', 'and what it carries is the ask that could not be, said as late');
502
+
503
+ // The relation is not the worse for it: the lane is free again and the
504
+ // count moved on, which is what a bound is for.
505
+ const after = (await a.being.relay({ id: 'b', method: 'ping' })) as { said: string | null };
506
+ assert.equal(after.said, null, 'and she asks again on the same standing');
507
+ });
508
+
509
+ // Lend is boot's other half: what the device already has, reached the one
510
+ // way anything is reached. The being lent stands in a ward the harbor
511
+ // roots, so nothing here can look at her from the outside; what the ward
512
+ // owes is what is asserted. She is handed an id, the standing is an
513
+ // ordinary one, and the invitation is nowhere she can read.
514
+ lends('what the device lends is a being: she is handed an id, asks on an ordinary standing, and never sees the invitation', async () => {
515
+ const w = await make({ lends: { pen: 'Member' } });
516
+ const maker = await w.boot<Maker>('MK', Maker);
517
+ assert.deepEqual(await maker.being.borrow({ name: 'pen', id: 'pen' }), { lent: 'pen' }, 'the id she gave is the id she gets');
518
+ // An ordinary standing from there on: she asks on it, and arrives at that
519
+ // being's door as somebody the far side named.
520
+ const said = (await maker.being.say({ id: 'pen' })) as { said: { pong: string | null } | null };
521
+ assert.equal(typeof said.said?.pong, 'string', 'she arrived under an id the far being minted');
522
+ // The keys are the ward's. Nothing of the relation is in her cells, which
523
+ // is the whole difference between this standing and one she gave away.
524
+ const c = w.census();
525
+ const heir = c.binds.MK!.standings.pen!.heir;
526
+ assert.equal(typeof heir, 'string', 'the ward bound a heir for it');
527
+ assert.equal(JSON.stringify(c.cells.MK).includes(heir!), false, 'and the invitation reached her nowhere');
528
+ // Null is every kind of no, and she cannot tell them apart.
529
+ assert.deepEqual(await maker.being.borrow({ name: 'gps', id: 'x' }), { lent: null }, 'a name this box does not lend');
530
+ assert.deepEqual(await maker.being.borrow({ name: 'pen', id: 'pen' }), { lent: null }, 'an id she already holds');
531
+ assert.deepEqual(await maker.being.borrow({ name: 'pen', id: 'OWNER' }), { lent: null }, 'a word the ward reserves');
532
+ });
533
+
534
+ lends('a harbor with nothing to lend lends nothing, and says so the same way', async () => {
535
+ const w = await make();
536
+ const maker = await w.boot<Maker>('MK', Maker);
537
+ assert.deepEqual(await maker.being.borrow({ name: 'pen', id: 'pen' }), { lent: null });
538
+ assert.equal(w.census().binds.MK!.standings.pen, undefined, 'and nothing was written for it');
539
+ });
540
+
344
541
  // The case the estate's walk reaches by weight and this reaches by hand: a
345
542
  // seat kicked after it answered and before it was taken. She holds a key
346
543
  // the door bound for her, so she is the one person who hears why, and she
@@ -47,7 +47,7 @@ export function conformReach(label: string, make: () => Promise<FarSide>, { test
47
47
  t('asks in flight at once each get their own answer', async () => {
48
48
  const far = await make();
49
49
  try {
50
- await far.hold(pk('aa'), async (b) => (await wait(b[0]! * 10), bytes(b[0]!)));
50
+ await far.hold(pk('aa'), async (b) => (await wait(b[0] * 10), bytes(b[0])));
51
51
  const out = await Promise.all([far.reach.carry(pk('aa'), bytes(3)), far.reach.carry(pk('aa'), bytes(1)), far.reach.carry(pk('aa'), bytes(2))]);
52
52
  assert.deepEqual(out, [bytes(3), bytes(1), bytes(2)]);
53
53
  } finally {
@@ -8,16 +8,18 @@
8
8
  // edge harbor is this over an object's storage. None of them is in this
9
9
  // tree, and every one of them passes the conformance suite untouched.
10
10
  //
11
- // The directory is filled three ways, in this order: the harbor's own
11
+ // The directory is filled four ways, in this order: the harbor's own
12
12
  // doors; a socket a dialer holds to it, bound once the door behind the
13
- // dialer's claim has proved it holds the key, and unbound at close; a hint
14
- // from a link. And one fallback: a dialer with nothing in its
13
+ // dialer's claim has proved it holds the key, and unbound at close; the
14
+ // claims of a listener this harbor dialed, proved the same way and reached
15
+ // through that line; and a hint, a pk at a URL, kept in the store. A hint
16
+ // never displaces a reach proved at a door. And one fallback: a dialer with nothing in its
15
17
  // directory for a pk sends down the socket it holds, because the listener
16
18
  // it dialed is the rendezvous and may hold that pk on another socket. Bytes
17
19
  // that arrive from the wire go to an own door or a held socket and never
18
20
  // onward by request or by fallback; that one rule is the rendezvous.
19
21
  import { Ward } from '../ward/ward.ts';
20
- import { maker, entropy as random, learnPk, type Ground, type WardPointers } from '../ward/ground.ts';
22
+ import { maker, entropy as random, learnPk, type Ground, type WardPointers, type Lend } from '../ward/ground.ts';
21
23
  import type { BeingClass, BeingLike } from '../being/types.ts';
22
24
  import { silence } from '../being/silence.ts';
23
25
  import { request, type Reach } from './reach.ts';
@@ -102,9 +104,22 @@ export class Harbor {
102
104
  for (const say of this.announcers) say();
103
105
  }
104
106
 
105
- constructor(store: Store, loader: Loader = async () => ({})) {
107
+ // What this device lends the beings of the wards it hosts, by name. One
108
+ // for the harbor, and `lendFor` says which ward may ask it: every ward,
109
+ // unless a terrain says a stranger's ward is lent nothing.
110
+ // Set at birth or once the harbor's own ward is standing, which is the
111
+ // usual order: a terrain puts that ward up first, then boots the rest on a
112
+ // ground that can reach it.
113
+ lend: Lend | undefined;
114
+
115
+ constructor(store: Store, loader: Loader = async () => ({}), lend?: Lend) {
106
116
  this.store = store;
107
117
  this.loader = loader;
118
+ this.lend = lend;
119
+ }
120
+
121
+ protected lendFor(_name: string, _record: WardRecord): Lend | undefined {
122
+ return this.lend;
108
123
  }
109
124
 
110
125
  // Boot every ward the store keeps, and learn the hints. One ward that will
@@ -112,6 +127,10 @@ export class Harbor {
112
127
  // named in `unbooted` and takes nobody else down with it.
113
128
  async boot(): Promise<void> {
114
129
  for (const name of await this.store.list()) {
130
+ // A ward this harbor already hosts is not booted twice. A terrain that
131
+ // keeps a ward of its own puts it up before this runs, so that a being
132
+ // born in any other ward can lend from her first line.
133
+ if (this.wards.has(name)) continue;
115
134
  try {
116
135
  const kept = await this.store.load(name);
117
136
  if (kept) await this.host(name, kept);
@@ -335,6 +354,8 @@ export class Harbor {
335
354
  random,
336
355
  wrote: () => this.#wrote(name),
337
356
  };
357
+ const lend = this.lendFor(name, kept.record);
358
+ if (lend) ground.lend = lend;
338
359
  const saving: Saving = { memory, dirty: false, running: null, timer: undefined, gone: false };
339
360
  this.#saving.set(name, saving);
340
361
  // A ward that will not be born leaves nothing behind: a partition of a
@@ -8,7 +8,7 @@ export { Harbor, DEFAULT_CODE, type Hosted, type Bound, type Loader } from './co
8
8
  export { dial, type Dialer } from './dial.ts';
9
9
  export { MemoryStore, values, type Store, type Kept, type WardRecord } from './store.ts';
10
10
  export { request, Socket, SUITE, REFUSED, type Reach, type Carry, type Line, type Announce } from './reach.ts';
11
- export type { Ground, WardPointers } from '../ward/ground.ts';
11
+ export type { Ground, WardPointers, Lend } from '../ward/ground.ts';
12
12
  // What a harbor builds a ground out of. Convenience, never contract: a kit
13
13
  // writing its own harbor may write these three again.
14
14
  export { maker, entropy, learnPk } from '../ward/ground.ts';
@@ -5,7 +5,7 @@
5
5
  // reads no partition, opens no byte. Several memory harbors may be linked,
6
6
  // which stands in for a wire, and cut, which stands in for weather.
7
7
  import type { BeingClass, BeingLike } from '../being/types.ts';
8
- import { maker, entropy, learnPk, type Ground, type WardPointers } from '../ward/ground.ts';
8
+ import { maker, entropy, learnPk, type Ground, type WardPointers, type Lend } from '../ward/ground.ts';
9
9
  import { hex } from '../ward/arithmetic.ts';
10
10
 
11
11
  export type Booted = WardPointers & { pk: string };
@@ -42,18 +42,21 @@ export class MemoryHarbor {
42
42
  }
43
43
 
44
44
  // boot by seed. the harbor gets a door and an ask, and learns the pk by asking.
45
- async boot(seed: string, Ward: WardFactory, classes: Record<string, BeingClass>): Promise<Booted> {
45
+ // `lend` is what this device lends the ward's beings, and a test's stand-in
46
+ // for a real terrain's: left out, every name answers null.
47
+ async boot(seed: string, Ward: WardFactory, classes: Record<string, BeingClass>, lend?: Lend): Promise<Booted> {
46
48
  this.partitions.set(seed, {});
47
- return this.reboot(seed, Ward, classes);
49
+ return this.reboot(seed, Ward, classes, lend);
48
50
  }
49
51
 
50
52
  // a restart: same seed, same partition, fresh ward. the ward's files are the harbor's to keep.
51
- async reboot(seed: string, Ward: WardFactory, classes: Record<string, BeingClass>): Promise<Booted> {
53
+ async reboot(seed: string, Ward: WardFactory, classes: Record<string, BeingClass>, lend?: Lend): Promise<Booted> {
52
54
  const memory = this.partitions.get(seed);
53
55
  if (!memory) throw new Error(`no partition for ${seed}`);
54
56
  const ground: Ground = {
55
57
  seed,
56
58
  memory,
59
+ ...(lend ? { lend } : {}),
57
60
  // One objects map for the whole harbor, not one per ward: every ward
58
61
  // here is in this process and a test reaches any being of any of them.
59
62
  instantiate: maker(this.objects, classes),
@@ -66,7 +66,7 @@ const ASK = 0,
66
66
  REPLY = 1,
67
67
  NONE = 2;
68
68
 
69
- // The wire suite: which frames these are and which four algorithms seal what
69
+ // The wire suite: which frames these are and which five algorithms seal what
70
70
  // they carry. One value, and it is not negotiated. It rides in the handshake
71
71
  // and never on an ask, so an ask stays bytes nobody can tell from noise, and
72
72
  // a line still learns before it carries anything whether the far side speaks
package/src/ward/cells.ts CHANGED
@@ -22,40 +22,82 @@ import type { Cells, Json } from '../being/types.ts';
22
22
  // reply from ending a walk in a stack overflow instead of a refusal.
23
23
  export const DEPTH = 64;
24
24
 
25
+ // How many values one written out may come to. Depth alone does not bound
26
+ // what a value costs to keep: one subvalue may sit under two keys, and
27
+ // nesting that forty levels deep is inside the depth bound and is a trillion
28
+ // values once written down. What is walked here is the object graph, which is
29
+ // small; what a harbor keeps is the tree it expands to, which is not. So the
30
+ // walk counts the tree while it proves the graph, and refuses a value nobody
31
+ // could write down. This is a bound on what a being holds, so it is this
32
+ // kit's number and no word of the spec.
33
+ export const BREADTH = 1 << 20;
34
+
35
+ // Text a harbor can write down: every surrogate in a pair. A lone one is a
36
+ // code unit with no character, which JSON escapes and UTF-8 cannot encode at
37
+ // all, so a harbor that keeps strings as bytes loses it and one that keeps
38
+ // the object does not. I-JSON says a string is text, and this is that rule
39
+ // where the value is written rather than where it is read.
40
+ const WELL_FORMED = /^(?:[^\uD800-\uDFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF])*$/;
41
+
25
42
  // I-JSON, all the way down. A number that JSON cannot write is not a number
26
43
  // a harbor can keep, a key on the prototype is not a key she wrote, and a
27
44
  // hole in a list, or a key named `__proto__`, is a thing JSON writes one way
28
45
  // and a runtime reads another.
29
- function fault(v: unknown, path: string, seen: Set<object>, depth: number): string | null {
30
- if (v === null || typeof v === 'boolean' || typeof v === 'string') return null;
31
- if (typeof v === 'number') return Number.isFinite(v) ? null : `${path} is ${String(v)}, which no harbor can write down`;
46
+ //
47
+ // A fault is a sentence; anything else is the count of values under this one,
48
+ // itself included. A value already counted is a value already proven, so a
49
+ // graph that shares is walked once per node rather than once per path.
50
+ function walk(v: unknown, path: string, seen: Set<object>, sized: Map<object, number>, depth: number): string | number {
51
+ if (v === null || typeof v === 'boolean') return 1;
52
+ if (typeof v === 'string') return WELL_FORMED.test(v) ? 1 : `${path} carries a lone surrogate, which is no text a harbor can write down`;
53
+ if (typeof v === 'number') {
54
+ if (!Number.isFinite(v)) return `${path} is ${String(v)}, which no harbor can write down`;
55
+ // Minus zero is a number JSON writes as `0` and reads back as `0`. A
56
+ // harbor that keeps values would hand her the sign she wrote and one that
57
+ // writes JSON would not, and she is never told which she is standing in.
58
+ return Object.is(v, -0) ? `${path} is minus zero, which comes back as zero from every harbor that writes JSON` : 1;
59
+ }
32
60
  if (typeof v !== 'object') return `${path} is a ${typeof v}, which is not a value`;
33
61
  if (seen.has(v)) return `${path} refers back to itself`;
62
+ const counted = sized.get(v);
63
+ if (counted !== undefined) return counted;
34
64
  if (depth >= DEPTH) return `${path} is nested past ${DEPTH} levels, which no harbor can keep`;
35
65
  seen.add(v);
36
66
  try {
67
+ let size = 1;
68
+ const under = (where: string, x: unknown): string | null => {
69
+ const r = walk(x, where, seen, sized, depth + 1);
70
+ if (typeof r === 'string') return r;
71
+ size += r;
72
+ return size > BREADTH ? `${path} writes out to more than ${BREADTH} values, which no harbor can keep` : null;
73
+ };
37
74
  if (Array.isArray(v)) {
38
75
  for (let i = 0; i < v.length; i += 1) {
39
76
  if (!(i in v)) return `${path}[${i}] is a hole, which no harbor can write down`;
40
- const f = fault(v[i], `${path}[${i}]`, seen, depth + 1);
77
+ const f = under(`${path}[${i}]`, v[i]);
41
78
  if (f) return f;
42
79
  }
43
- return null;
80
+ sized.set(v, size);
81
+ return size;
44
82
  }
45
83
  const proto = Object.getPrototypeOf(v);
46
84
  if (proto !== Object.prototype && proto !== null) return `${path} is a ${(v).constructor?.name ?? 'object'}, which is not a value`;
47
85
  for (const k of Object.keys(v)) {
48
86
  if (k === '__proto__') return `${path}.__proto__ is a key no harbor can keep`;
49
- const f = fault((v as Record<string, unknown>)[k], `${path}.${k}`, seen, depth + 1);
87
+ const f = under(`${path}.${k}`, (v as Record<string, unknown>)[k]);
50
88
  if (f) return f;
51
89
  }
52
- return null;
90
+ sized.set(v, size);
91
+ return size;
53
92
  } finally {
54
93
  seen.delete(v);
55
94
  }
56
95
  }
57
96
 
58
- export const cellFault = (v: unknown, path: string): string | null => fault(v, path, new Set(), 0);
97
+ export const cellFault = (v: unknown, path: string): string | null => {
98
+ const out = walk(v, path, new Set(), new Map(), 0);
99
+ return typeof out === 'string' ? out : null;
100
+ };
59
101
 
60
102
  // The three keys at the root of her cells that are the ward's: it writes
61
103
  // them, she reads them, and a write of hers there is refused like a non-value.
@@ -69,6 +111,13 @@ const wrapped = new WeakMap<object, object>();
69
111
  const targets = new WeakMap<object, object>();
70
112
  const guards = new WeakSet();
71
113
 
114
+ // A name no partition can hold, wherever it appears. It is here beside the
115
+ // guard that refuses it, because the ward refuses it too: a relation named
116
+ // this one would be written into the bind table and then throw on her cells,
117
+ // which is half a relation and a throw where a null is promised. One rule,
118
+ // one place, read by the guard and by the stance.
119
+ export const unkeepable = (k: string): boolean => k === '__proto__';
120
+
72
121
  // A key a write may land on: a string that is not `__proto__`, and not one
73
122
  // of the ward's at the root. A symbol key is a thing JSON never writes.
74
123
  const refuse: (why: string) => never = (why) => {
@@ -76,7 +125,7 @@ const refuse: (why: string) => never = (why) => {
76
125
  };
77
126
  function keyFault(t: object, k: string | symbol, v: unknown, path: string, root: boolean): void {
78
127
  if (typeof k !== 'string') refuse(`${path} takes no symbol key`);
79
- if (k === '__proto__') refuse(`${path}.__proto__ is a key no harbor can keep`);
128
+ if (unkeepable(k)) refuse(`${path}.__proto__ is a key no harbor can keep`);
80
129
  if (root && WARDS.has(k)) refuse(`${path}.${k} is the ward's to write`);
81
130
  // A list grows by one at its end, or it has holes JSON cannot write. A
82
131
  // push sets the slot at its length and then the length: both pass. A
@@ -138,4 +187,4 @@ function guard<T extends object>(target: T, path: string, wrote: () => void, roo
138
187
  export const guardCells = (cells: Cells, wrote: () => void = () => {}): Cells => guard(cells as unknown as Record<string, Json>, 'cells', wrote, true) as unknown as Cells;
139
188
  // The cells behind the guard, for the ward alone: the one writer of the
140
189
  // three keys that are its own.
141
- export const unguarded = (cells: Cells): Cells => (targets.get(cells as unknown as object) ?? cells) as Cells;
190
+ export const unguarded = (cells: Cells): Cells => (targets.get(cells) ?? cells) as Cells;