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