@quo-systems/quo 0.2.9 → 0.2.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -10
- package/SPEC.md +301 -110
- package/dist/being/being.d.ts +2 -2
- package/dist/being/being.js +34 -12
- package/dist/being/digest.js +26 -8
- package/dist/being/index.d.ts +2 -2
- package/dist/being/index.js +2 -2
- package/dist/being/silence.d.ts +2 -0
- package/dist/being/silence.js +12 -0
- package/dist/being/types.d.ts +4 -2
- package/dist/being/types.js +25 -0
- package/dist/conformance/assert.js +40 -6
- package/dist/conformance/beings.d.ts +48 -5
- package/dist/conformance/beings.js +39 -8
- package/dist/conformance/estate.js +110 -21
- package/dist/conformance/index.d.ts +5 -2
- package/dist/conformance/index.js +165 -7
- package/dist/harbor/core.d.ts +5 -2
- package/dist/harbor/core.js +195 -45
- package/dist/harbor/dial.js +32 -15
- package/dist/harbor/index.d.ts +1 -0
- package/dist/harbor/index.js +3 -0
- package/dist/harbor/memory.d.ts +3 -3
- package/dist/harbor/memory.js +7 -12
- package/dist/harbor/reach.js +42 -17
- package/dist/ward/allowance.js +15 -4
- package/dist/ward/arithmetic.d.ts +1 -0
- package/dist/ward/arithmetic.js +22 -6
- package/dist/ward/cells.d.ts +3 -1
- package/dist/ward/cells.js +79 -21
- package/dist/ward/door.d.ts +3 -2
- package/dist/ward/door.js +38 -10
- package/dist/ward/ground.d.ts +5 -1
- package/dist/ward/ground.js +38 -1
- package/dist/ward/heirs.d.ts +2 -3
- package/dist/ward/heirs.js +19 -13
- package/dist/ward/index.d.ts +1 -0
- package/dist/ward/index.js +3 -0
- package/dist/ward/owner.d.ts +6 -27
- package/dist/ward/owner.js +59 -33
- package/dist/ward/partition.d.ts +3 -0
- package/dist/ward/partition.js +109 -4
- package/dist/ward/seal.d.ts +1 -0
- package/dist/ward/seal.js +41 -13
- package/dist/ward/stance.d.ts +7 -3
- package/dist/ward/stance.js +156 -66
- package/dist/ward/ward.d.ts +10 -0
- package/dist/ward/ward.js +123 -51
- package/package.json +4 -2
- package/src/being/being.ts +33 -11
- package/src/being/digest.ts +28 -13
- package/src/being/index.ts +2 -2
- package/src/being/silence.ts +14 -0
- package/src/being/types.ts +39 -5
- package/src/conformance/assert.ts +37 -4
- package/src/conformance/beings.ts +41 -10
- package/src/conformance/estate.ts +107 -20
- package/src/conformance/index.ts +188 -13
- package/src/harbor/core.ts +203 -46
- package/src/harbor/dial.ts +46 -17
- package/src/harbor/index.ts +3 -0
- package/src/harbor/memory.ts +8 -13
- package/src/harbor/reach.ts +47 -21
- package/src/ward/allowance.ts +15 -4
- package/src/ward/arithmetic.ts +25 -8
- package/src/ward/cells.ts +76 -25
- package/src/ward/door.ts +38 -12
- package/src/ward/ground.ts +52 -3
- package/src/ward/heirs.ts +19 -13
- package/src/ward/index.ts +3 -0
- package/src/ward/owner.ts +65 -46
- package/src/ward/partition.ts +109 -5
- package/src/ward/seal.ts +41 -12
- package/src/ward/stance.ts +163 -64
- package/src/ward/ward.ts +124 -52
- package/vectors/arithmetic.json +7 -0
- package/vectors/framing.json +30 -15
- package/vectors/wire.json +4 -4
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
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
|
|
3
|
+
// shop, a customer on the base class, a maker, a member, and one raw being
|
|
4
|
+
// with no base at all.
|
|
4
5
|
import { Being } from '../being/being.ts';
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
6
|
+
import { answered, isUnreached } from '../being/silence.ts';
|
|
7
|
+
import { invitationArgs } from '../being/types.ts';
|
|
8
|
+
import type { Ask, Asker, Blueprint, Invitation, Json, JsonObject, OccupantRecord, Reply, Stance } from '../being/types.ts';
|
|
7
9
|
|
|
8
10
|
// A printer. Invites whom she is told to, prints for her occupants. Her
|
|
9
11
|
// blueprint lives in her cells so a test can change her shape.
|
|
@@ -11,7 +13,9 @@ export class Printer extends Being {
|
|
|
11
13
|
static override cells = { jobs: [] as JsonObject[], asks: [{ name: 'print', input: { type: 'object' } }] as JsonObject[] };
|
|
12
14
|
static override asks = { print: { input: { type: 'object' } } };
|
|
13
15
|
override describe(): Blueprint {
|
|
14
|
-
|
|
16
|
+
// Her own cells, which a test writes to change her shape. Cells are Json
|
|
17
|
+
// to the ward, so the asks are named for what she keeps in them.
|
|
18
|
+
return { asks: this.cells.asks as Ask[], notes: { model: 'LX-2' } };
|
|
15
19
|
}
|
|
16
20
|
print({ doc }: JsonObject, asker: Asker) {
|
|
17
21
|
(this.cells.jobs as JsonObject[]).push({ by: asker.id ?? null, doc: doc ?? null });
|
|
@@ -46,7 +50,7 @@ export class Shop extends Being {
|
|
|
46
50
|
|
|
47
51
|
async keepPrinter(invitation: Invitation): Promise<boolean> {
|
|
48
52
|
const bp = await this.knock(invitation);
|
|
49
|
-
if (
|
|
53
|
+
if (!answered(bp)) return false;
|
|
50
54
|
this.cells.printerId = await this.take('prn', invitation);
|
|
51
55
|
return true;
|
|
52
56
|
}
|
|
@@ -55,7 +59,7 @@ export class Shop extends Being {
|
|
|
55
59
|
const rec = this.occupant(asker);
|
|
56
60
|
if (rec?.notes.tier === 'gold' && invitation) {
|
|
57
61
|
const back = await this.knock(invitation as Invitation, 'hi');
|
|
58
|
-
if (
|
|
62
|
+
if (answered(back)) await this.take(`vip-${asker.id}`, invitation as Invitation);
|
|
59
63
|
}
|
|
60
64
|
return { welcome: true };
|
|
61
65
|
}
|
|
@@ -67,7 +71,7 @@ export class Shop extends Being {
|
|
|
67
71
|
if (!printer) return { error: 'no printer' };
|
|
68
72
|
const out = await printer.ask('print', { doc: `receipt for ${item}` });
|
|
69
73
|
if (isUnreached(out)) return { error: 'printer unreachable', retry: true };
|
|
70
|
-
if (
|
|
74
|
+
if (!answered(out)) return { error: 'printer refused' };
|
|
71
75
|
const st = this.cells.standings[id!];
|
|
72
76
|
if (st.seen !== st.digest) this.occupant(asker)!.notes.printerChanged = true;
|
|
73
77
|
(this.cells.sales as JsonObject[]).push({ item: item ?? null, by: asker.id ?? null });
|
|
@@ -86,9 +90,13 @@ export class Customer extends Being {
|
|
|
86
90
|
static override asks = { hi: { input: { type: 'object' } } };
|
|
87
91
|
|
|
88
92
|
async join(invitation: Invitation) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
93
|
+
// A way back for the shop, if she can mint one. She may not: two joins on
|
|
94
|
+
// one invitation race, and the second finds the id taken. Then she offers
|
|
95
|
+
// nothing and knocks all the same, which is what the shop is asked to
|
|
96
|
+
// survive. The old shape said `!` here and was wrong about it.
|
|
97
|
+
const mine = await this.invite('shop-back');
|
|
98
|
+
const out = await this.knock(invitation, 'hello', { invitation: mine === null ? null : invitationArgs(mine) });
|
|
99
|
+
if (!answered(out)) return out; // nothing was born
|
|
92
100
|
await this.take('shop', invitation); // now, and only now
|
|
93
101
|
return out;
|
|
94
102
|
}
|
|
@@ -119,6 +127,29 @@ export class Echo {
|
|
|
119
127
|
// A member of an estate. Nothing scripted: she invites whom she is told to,
|
|
120
128
|
// knocks where she is told to, and answers `ping` with the asker's own name.
|
|
121
129
|
// The estate chapter drives her, and what it drives is the graph, not her.
|
|
130
|
+
// A being who makes beings. `boot` is on every stance, so it is the ward's
|
|
131
|
+
// promise and not this kit's, and a second kit that never wired it hands its
|
|
132
|
+
// beings a call that answers null for ever. She says what came of it: the key
|
|
133
|
+
// back, or null for a key already taken, a class the harbor does not hold, or
|
|
134
|
+
// a constructor that threw. With an id she also asks for a way back, and then
|
|
135
|
+
// she holds a standing to what she made like any other.
|
|
136
|
+
export class Maker extends Being {
|
|
137
|
+
static override cells = { made: [] as string[] };
|
|
138
|
+
static override asks = {
|
|
139
|
+
open: { input: { type: 'object', properties: { class: { type: 'string' }, key: { type: 'string' }, id: { type: 'string' } }, required: ['class', 'key'] } },
|
|
140
|
+
say: { input: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] } },
|
|
141
|
+
};
|
|
142
|
+
async open({ class: className, key, id }: JsonObject) {
|
|
143
|
+
const made = await this.boot(className as string, key as string, id as string | undefined);
|
|
144
|
+
if (made !== null) (this.cells.made as string[]).push(made);
|
|
145
|
+
return { made };
|
|
146
|
+
}
|
|
147
|
+
async say({ id }: JsonObject) {
|
|
148
|
+
const standing = this.standings[id as string];
|
|
149
|
+
return { said: standing === undefined ? null : ((await standing.ask('ping')) as Json) };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
122
153
|
export class Member extends Being {
|
|
123
154
|
static override cells = { heard: [] as JsonObject[] };
|
|
124
155
|
static override asks = { ping: { input: { type: 'object' } } };
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
// harbors, and that is where the promise of Quo is held: a being never learns
|
|
23
23
|
// where the other one is, so every answer, after every move, under every
|
|
24
24
|
// topology, is the answer the model wrote without knowing the topology
|
|
25
|
-
// either. Held
|
|
25
|
+
// either. Held after every one of sixty moves, it is the promise at a scale a scene
|
|
26
26
|
// cannot reach, and it is what makes a migration honest: if no answer can
|
|
27
27
|
// tell the topology apart, moving a ward between harbors cannot be felt.
|
|
28
28
|
// The model itself is the script's own bookkeeping and takes nothing from a
|
|
@@ -35,13 +35,13 @@
|
|
|
35
35
|
// A model that kept one symmetric edge would be a wrong model, and would
|
|
36
36
|
// agree happily with a ward that had drifted.
|
|
37
37
|
import { assert } from './assert.ts';
|
|
38
|
-
import { isSilence, isUnreached,
|
|
38
|
+
import { isSilence, isUnreached, told } from '../being/silence.ts';
|
|
39
39
|
import type { Invitation } from '../being/types.ts';
|
|
40
40
|
import { Member } from './beings.ts';
|
|
41
|
+
import { HEX64 } from './index.ts';
|
|
41
42
|
import type { Census, Handle, Runner, World } from './index.ts';
|
|
42
43
|
|
|
43
|
-
const
|
|
44
|
-
const said = (x: unknown, w: string): boolean => isWord(x) && wordOf(x) === w;
|
|
44
|
+
const said = (x: unknown, w: string): boolean => told(x) === w;
|
|
45
45
|
|
|
46
46
|
// One occupant arc, as the host holds it. `guest` is null until somebody
|
|
47
47
|
// knocks; `live` goes false when she is kicked. Both are the host's business
|
|
@@ -67,10 +67,47 @@ const rolls = (seed: number) => () => {
|
|
|
67
67
|
// Held after every move. Each line is a way the bookkeeping could stop being
|
|
68
68
|
// a graph: an arc with one end, an arc with two owners, a relation filed in
|
|
69
69
|
// two places at once, a count that went backwards, a secret in the cells.
|
|
70
|
-
function ledger(c: Census, before: Census | undefined, where: string) {
|
|
70
|
+
function ledger(c: Census, before: Census | undefined, where: string, g?: Graph) {
|
|
71
71
|
const at = (why: string) => `${where}: ${why}`;
|
|
72
72
|
const heirsSeen = new Set<string>();
|
|
73
73
|
|
|
74
|
+
// What the model knows, which the ward's own bookkeeping cannot tell it.
|
|
75
|
+
// Without this the ledger only checks the ward against itself, and a drift
|
|
76
|
+
// is caught late, if the sweep happens to ask the affected arc, or never.
|
|
77
|
+
if (g) {
|
|
78
|
+
for (const s of g.seats) {
|
|
79
|
+
const heir = c.wards[c.binds[s.host]!.ward]!.heirs[s.inv.heir!];
|
|
80
|
+
if (!s.live) {
|
|
81
|
+
assert.ok(!heir, at(`${s.host} kicked ${s.id} and the heir is still held`));
|
|
82
|
+
assert.ok(!(s.id in c.binds[s.host]!.occupants), at(`${s.host} kicked ${s.id} and still holds the occupant`));
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
assert.ok(heir, at(`${s.host}'s live seat ${s.id} has no heir`));
|
|
86
|
+
assert.equal(heir!.id, s.id, at(`the heir for ${s.host}/${s.id} is filed under ${heir!.id}`));
|
|
87
|
+
// A seat nobody has knocked is a heir that has never spoken: fresh, and
|
|
88
|
+
// at zero. One that answered has spoken, so it is neither.
|
|
89
|
+
assert.equal(heir!.fresh, !s.answered, at(`${s.host}/${s.id} is ${s.answered ? 'answered' : 'unknocked'} and its heir says fresh=${heir!.fresh}`));
|
|
90
|
+
assert.equal(heir!.mark === 0, !s.answered, at(`${s.host}/${s.id} is ${s.answered ? 'answered' : 'unknocked'} and its heir is at ${heir!.mark}`));
|
|
91
|
+
}
|
|
92
|
+
for (const h of g.holds) {
|
|
93
|
+
const standing = c.binds[h.owner]!.standings[h.id];
|
|
94
|
+
if (!h.live) {
|
|
95
|
+
assert.ok(!standing, at(`${h.owner} dropped ${h.id} and the standing is still there`));
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
assert.ok(standing, at(`${h.owner}'s live standing ${h.id} is gone from the bind table`));
|
|
99
|
+
assert.equal(standing!.heir, h.seat.inv.heir, at(`${h.owner}/${h.id} names a heir that is not the seat it was born on`));
|
|
100
|
+
// The count she has spent, against the number the far door honoured.
|
|
101
|
+
// Her side is never behind: she takes a number, then it crosses.
|
|
102
|
+
const far = c.wards[c.binds[h.seat.host]!.ward]!.heirs[h.seat.inv.heir!];
|
|
103
|
+
if (far) assert.ok(standing!.seq >= far.mark, at(`${h.owner}/${h.id} counted ${standing!.seq} and the far door has honoured ${far.mark}`));
|
|
104
|
+
}
|
|
105
|
+
// Every seat and hold the model knows about, and no relation the ward
|
|
106
|
+
// holds that the model never made.
|
|
107
|
+
const mine = new Set(g.seats.filter((s) => s.live).map((s) => `${s.host}/${s.id}`));
|
|
108
|
+
for (const [key, b] of Object.entries(c.binds)) for (const id of Object.keys(b.occupants)) assert.ok(mine.has(`${key}/${id}`), at(`${key} holds an occupant ${id} the model never made`));
|
|
109
|
+
}
|
|
110
|
+
|
|
74
111
|
for (const [pk, w] of Object.entries(c.wards)) {
|
|
75
112
|
for (const [heir, r] of Object.entries(w.heirs)) {
|
|
76
113
|
assert.ok(!heirsSeen.has(heir), at(`heir ${heir} is held by two wards`));
|
|
@@ -175,6 +212,18 @@ export function estate(label: string, make: () => Promise<World>, { canDown, tes
|
|
|
175
212
|
const pick = <T>(xs: T[]): T => xs[Math.floor(roll() * xs.length)];
|
|
176
213
|
const hands: Record<string, Handle<Member>> = {};
|
|
177
214
|
const g: Graph = { seats: [], holds: [] };
|
|
215
|
+
// What this run actually got to. A generated walk protects only the
|
|
216
|
+
// states it reaches, and a list of assertions nobody ran is a list of
|
|
217
|
+
// assertions nobody has. Every case named here is checked off as it
|
|
218
|
+
// fires and the run fails at the end if one never did.
|
|
219
|
+
const reached = new Set<string>();
|
|
220
|
+
const MUST = [
|
|
221
|
+
'a kicked seat knocked by the guest it bound, who hears removed',
|
|
222
|
+
'a standing dropped by its owner, whose host is never told',
|
|
223
|
+
'a seat kicked by its host, whose guest is never told',
|
|
224
|
+
'a relation taken, so it lives in the standing and not the knock',
|
|
225
|
+
'a ward moved to another harbor under everyone standing on it',
|
|
226
|
+
];
|
|
178
227
|
let minted = 0;
|
|
179
228
|
const born = async (key: string) => (hands[key] = await w.boot<Member>(key, Member));
|
|
180
229
|
for (const key of ['M1', 'M2', 'M3']) await born(key);
|
|
@@ -203,20 +252,37 @@ export function estate(label: string, make: () => Promise<World>, { canDown, tes
|
|
|
203
252
|
// taken it the invitation is not a knock any more -- it joins the
|
|
204
253
|
// standing's lane, and is exactly as alive as the standing is.
|
|
205
254
|
for (const s of g.seats) {
|
|
206
|
-
|
|
255
|
+
const knock = async () => {
|
|
207
256
|
const guest = s.guest ?? pick(keys.filter((k) => k !== s.host));
|
|
208
257
|
const hold = g.holds.find((h) => h.seat === s);
|
|
209
258
|
const back = await hands[guest].being.knock(s.inv, 'ping');
|
|
210
|
-
|
|
259
|
+
// Whether this knock is heard at all: the seat has no guest yet,
|
|
260
|
+
// or its guest is this one, and nothing of hers has dropped the
|
|
261
|
+
// key the door would bind her by. A hold exists only where a
|
|
262
|
+
// guest does, so the two tests are one condition and not two.
|
|
263
|
+
// Then the seat says which answer: alive is the pong, kicked is
|
|
264
|
+
// `removed`, and unheard is silence whichever it is.
|
|
265
|
+
const speaks = (s.guest === null || s.guest === guest) && (hold === undefined || hold.live);
|
|
266
|
+
if (!speaks) assert.ok(isSilence(back), `${guest} knocking ${s.host}/${s.id} should be silence`);
|
|
267
|
+
else if (s.live) {
|
|
211
268
|
assert.deepEqual(back, { pong: s.id }, `${guest} knocking ${s.host}/${s.id}`);
|
|
212
269
|
s.guest = guest;
|
|
213
270
|
s.answered = true;
|
|
214
|
-
} else
|
|
215
|
-
// kicked, and she speaks under a key the door bound for this seat: she hears why
|
|
271
|
+
} else {
|
|
216
272
|
assert.ok(said(back, 'removed'), `${guest} knocking kicked ${s.host}/${s.id} should hear removed`);
|
|
217
|
-
|
|
273
|
+
reached.add('a kicked seat knocked by the guest it bound, who hears removed');
|
|
274
|
+
}
|
|
218
275
|
return `${guest} knocks ${s.host}/${s.id}`;
|
|
219
|
-
}
|
|
276
|
+
};
|
|
277
|
+
out.push(knock);
|
|
278
|
+
// Weighted, not written twice: the case this move exists to protect
|
|
279
|
+
// is a seat kicked after it answered and before it was taken, and a
|
|
280
|
+
// uniform walk over a list that grows with every invitation reaches
|
|
281
|
+
// it by luck if at all. Three entries make it likely by
|
|
282
|
+
// construction, and the count at the end of the run makes it
|
|
283
|
+
// certain: a walk that never got there fails rather than passing
|
|
284
|
+
// quietly on an assertion that never ran.
|
|
285
|
+
if (!s.live && s.answered && !s.taken) out.push(knock, knock);
|
|
220
286
|
// a stranger on a spent seat: silence, and nothing rebinds
|
|
221
287
|
if (s.guest !== null)
|
|
222
288
|
out.push(async () => {
|
|
@@ -236,6 +302,7 @@ export function estate(label: string, make: () => Promise<World>, { canDown, tes
|
|
|
236
302
|
assert.equal(await hands[s.guest!].being.take(id, s.inv), id);
|
|
237
303
|
s.taken = true;
|
|
238
304
|
g.holds.push({ owner: s.guest!, id, seat: s, live: true });
|
|
305
|
+
reached.add('a relation taken, so it lives in the standing and not the knock');
|
|
239
306
|
assert.equal(await hands[s.guest!].being.take(`${id}-again`, s.inv), null); // spent for her
|
|
240
307
|
return `${s.guest} takes ${s.host}/${s.id} as ${id}`;
|
|
241
308
|
});
|
|
@@ -245,6 +312,7 @@ export function estate(label: string, make: () => Promise<World>, { canDown, tes
|
|
|
245
312
|
out.push(async () => {
|
|
246
313
|
hands[h.owner].being.standings.remove(h.id);
|
|
247
314
|
h.live = false;
|
|
315
|
+
reached.add('a standing dropped by its owner, whose host is never told');
|
|
248
316
|
assert.equal(hands[h.owner].being.standings[h.id], undefined);
|
|
249
317
|
return `${h.owner} drops ${h.id}`;
|
|
250
318
|
});
|
|
@@ -254,6 +322,7 @@ export function estate(label: string, make: () => Promise<World>, { canDown, tes
|
|
|
254
322
|
out.push(async () => {
|
|
255
323
|
hands[s.host].being.occupants.remove(s.id);
|
|
256
324
|
s.live = false;
|
|
325
|
+
reached.add('a seat kicked by its host, whose guest is never told');
|
|
257
326
|
return `${s.host} kicks ${s.id}`;
|
|
258
327
|
});
|
|
259
328
|
|
|
@@ -264,11 +333,10 @@ export function estate(label: string, make: () => Promise<World>, { canDown, tes
|
|
|
264
333
|
out.push(async () => {
|
|
265
334
|
const who = pick(keys);
|
|
266
335
|
await w.migrate(who);
|
|
336
|
+
reached.add('a ward moved to another harbor under everyone standing on it');
|
|
267
337
|
return `${who}'s ward moves harbor`;
|
|
268
338
|
});
|
|
269
339
|
|
|
270
|
-
// weather: the host's door is gone. unreached, never silence, and the
|
|
271
|
-
// relation is exactly where it was when it comes back.
|
|
272
340
|
// weather: the host's door is gone. unreached, never silence, and the
|
|
273
341
|
// relation is exactly where it was when it comes back. The move is on
|
|
274
342
|
// the list under every topology even where there is nothing to cut,
|
|
@@ -306,20 +374,39 @@ export function estate(label: string, make: () => Promise<World>, { canDown, tes
|
|
|
306
374
|
// sequence debuggable at all.
|
|
307
375
|
const trace: string[] = [];
|
|
308
376
|
const at = (step: number) => `seed ${seed} step ${step}\n ${trace.join('\n ')}`;
|
|
309
|
-
|
|
310
|
-
|
|
377
|
+
// Whatever fails inside a step carries the trace that got there. One
|
|
378
|
+
// wrapper, so the move and the sweep are decorated the same way and a
|
|
379
|
+
// third thing added to a step cannot be decorated a fourth.
|
|
380
|
+
const inStep = async <T>(step: number, work: () => Promise<T>): Promise<T> => {
|
|
311
381
|
try {
|
|
312
|
-
|
|
382
|
+
return await work();
|
|
313
383
|
} catch (e) {
|
|
314
384
|
throw new Error(`${at(step)}\n -> ${(e as Error).message}`, { cause: e });
|
|
315
385
|
}
|
|
386
|
+
};
|
|
387
|
+
for (let step = 1; step <= 60; step++) {
|
|
388
|
+
const move = pick(moves());
|
|
389
|
+
// The move that failed goes into the trace before it runs, not after
|
|
390
|
+
// it returns: a move names itself as it finishes, so a throw left the
|
|
391
|
+
// trace ending at the last move that worked and saying nothing about
|
|
392
|
+
// the one that did not. It is written in twice, provisionally and
|
|
393
|
+
// then as it named itself, and the provisional line goes when the
|
|
394
|
+
// real one lands.
|
|
395
|
+
trace.push(`step ${step}: a move that has not said what it did`);
|
|
396
|
+
const did = await inStep(step, move);
|
|
397
|
+
trace[trace.length - 1] = did;
|
|
316
398
|
const c = w.census();
|
|
317
|
-
ledger(c, before, at(step));
|
|
399
|
+
ledger(c, before, at(step), g);
|
|
318
400
|
before = c;
|
|
319
|
-
await
|
|
320
|
-
throw new Error(`${at(step)}\n -> ${e.message}`, { cause: e });
|
|
321
|
-
});
|
|
401
|
+
await inStep(step, () => sweep(step));
|
|
322
402
|
}
|
|
403
|
+
// And the walk is held to having gone somewhere. Sixty moves under one
|
|
404
|
+
// seed is a sample, not a proof, so the states the chapter is written
|
|
405
|
+
// to protect are named and counted: a run that never reached one has
|
|
406
|
+
// not tested it, and says so here rather than passing on an assertion
|
|
407
|
+
// that never ran.
|
|
408
|
+
const missed = MUST.filter((what) => !reached.has(what));
|
|
409
|
+
assert.deepEqual(missed, [], `seed ${seed} never reached: ${missed.join('; ')}\n ${trace.join('\n ')}`);
|
|
323
410
|
});
|
|
324
411
|
}
|
|
325
412
|
}
|
package/src/conformance/index.ts
CHANGED
|
@@ -12,17 +12,17 @@
|
|
|
12
12
|
// handle.being the object, as the test drives it
|
|
13
13
|
// handle.cells her cells, as the ward keeps them
|
|
14
14
|
// handle.down = true | false her door is unreachable (needs canDown)
|
|
15
|
-
// handle.minted.has(pk)
|
|
15
|
+
// handle.minted.has(pk) the last eight pks her side minted
|
|
16
16
|
// handle.forgeKnock(invitation) make her side knock with a key of its own, unannounced
|
|
17
17
|
// world.census() every partition in the world, as values
|
|
18
18
|
// world.migrate(key) her ward moves to another harbor, same seed, same pk
|
|
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,
|
|
22
|
+
import { silence, isSilence, isUnreached, told } from '../being/silence.ts';
|
|
23
23
|
import { digest } from '../being/digest.ts';
|
|
24
|
-
import type { Asker, BeingClass, Cells, Invitation, Json, JsonObject } from '../being/types.ts';
|
|
25
|
-
import { Printer, Shop, Customer, Echo, Member } from './beings.ts';
|
|
24
|
+
import type { Asker, BeingClass, Cells, Invitation, Json, JsonObject, Reply, Stance } from '../being/types.ts';
|
|
25
|
+
import { Printer, Shop, Customer, Echo, Maker, Member } from './beings.ts';
|
|
26
26
|
import { estate } from './estate.ts';
|
|
27
27
|
|
|
28
28
|
export type Handle<B = unknown> = {
|
|
@@ -49,13 +49,26 @@ export type World = {
|
|
|
49
49
|
heir(inv: Invitation): HeirView | undefined;
|
|
50
50
|
census(): Census;
|
|
51
51
|
migrate(key: string): Promise<void>;
|
|
52
|
+
// The ask pointer of the ward a being lives behind, which is the owner's
|
|
53
|
+
// half and the second of the two a ward hands back. The harness already
|
|
54
|
+
// speaks it -- `boot` is the owner's boot ask -- and it is named here so the
|
|
55
|
+
// owner's chapter can be run against a second kit rather than only against
|
|
56
|
+
// this one. `key` says which ward: a being of it, and never the ward's pk,
|
|
57
|
+
// because a topology may put every being behind a ward of its own.
|
|
58
|
+
owner(key: string): (method?: string, args?: JsonObject) => Promise<unknown>;
|
|
52
59
|
};
|
|
53
60
|
|
|
54
|
-
export { Printer, Shop, Customer, Echo, Member };
|
|
61
|
+
export { Printer, Shop, Customer, Echo, Maker, Member };
|
|
55
62
|
|
|
56
|
-
|
|
63
|
+
// The two shapes the suite asserts, spelled here for the whole folder. They
|
|
64
|
+
// are spelled again in `src/ward/seal.ts`, and that is the point: this suite
|
|
65
|
+
// is what a second kit runs against its own ward, so it states the shapes it
|
|
66
|
+
// expects instead of importing ours, and nothing under `src/conformance`
|
|
67
|
+
// reaches into `src/ward` for anything.
|
|
68
|
+
export const HEX64 = /^[0-9a-f]{64}$/; // one key, one secret, one digest
|
|
69
|
+
export const HEX128 = /^[0-9a-f]{128}$/; // a ward pk: the signing pk then the padlock
|
|
57
70
|
// Her ward's word, as she receives it.
|
|
58
|
-
const said = (x: unknown, w: string): boolean =>
|
|
71
|
+
const said = (x: unknown, w: string): boolean => told(x) === w;
|
|
59
72
|
|
|
60
73
|
// The runner is handed in. Node passes `node:test`; a browser and an edge
|
|
61
74
|
// worker pass a shim of their own. The suite is the ward's truth, and the
|
|
@@ -84,7 +97,7 @@ export function conform(label: string, make: () => Promise<World>, { canDown = t
|
|
|
84
97
|
const inv = await shop.being.invite('gold');
|
|
85
98
|
assert.deepEqual(Object.keys(shop.cells.occupants), ['cust1']);
|
|
86
99
|
assert.deepEqual(Object.keys(inv).sort(), ['heir', 'secret', 'ward']);
|
|
87
|
-
assert.match(inv.ward,
|
|
100
|
+
assert.match(inv.ward, HEX128);
|
|
88
101
|
assert.match(inv.heir!, HEX64);
|
|
89
102
|
assert.match(inv.secret!, HEX64);
|
|
90
103
|
assert.equal(inv.ward, shop.pk);
|
|
@@ -195,6 +208,166 @@ export function conform(label: string, make: () => Promise<World>, { canDown = t
|
|
|
195
208
|
assert.deepEqual(Object.keys(alice.cells.standings), ['shop']);
|
|
196
209
|
});
|
|
197
210
|
|
|
211
|
+
// ---- the names that are nobody's to take
|
|
212
|
+
//
|
|
213
|
+
// Two lists, refused at both ends of the same namespace. The ward speaks
|
|
214
|
+
// under its own words, so a being who could mint one would be a being whose
|
|
215
|
+
// ward is indistinguishable from an occupant of hers. And standings are
|
|
216
|
+
// reached by name, so a relation taken as one of the calls on them would be
|
|
217
|
+
// a relation nothing can reach. A kit that ported invite without either
|
|
218
|
+
// list passes every other scene in this file.
|
|
219
|
+
t('the reserved ids and the calls on standings are refused at invite and at take, and nothing is written for them', async () => {
|
|
220
|
+
// alice hosts here rather than the shop, because the shop's own `invite`
|
|
221
|
+
// takes a tier and mints the id itself. This one is the stance's, which
|
|
222
|
+
// is where the rule lives.
|
|
223
|
+
const { alice, bob } = await world();
|
|
224
|
+
for (const id of ['OWNER', 'PUBLIC', 'knock', 'take', 'remove']) {
|
|
225
|
+
assert.equal(await alice.being.invite(id), null, `${id} is not hers to mint`);
|
|
226
|
+
assert.equal(Object.hasOwn(alice.cells.occupants, id), false, `and nothing was written for ${id}`);
|
|
227
|
+
|
|
228
|
+
// The other end: a guest is answered and then refused the name.
|
|
229
|
+
const inv = (await alice.being.invite(`for-${id}`))!;
|
|
230
|
+
assert.deepEqual(await bob.being.knock(inv, 'hi'), { heard: 'hi' }, `the knock for ${id} was answered`);
|
|
231
|
+
assert.equal(await bob.being.take(id, inv), null, `take refuses ${id} for the same reason`);
|
|
232
|
+
assert.equal(Object.hasOwn(bob.cells.standings, id), false, `and nothing was written for ${id}`);
|
|
233
|
+
assert.equal(await bob.being.take(`as-${id}`, inv), `as-${id}`, 'and an ordinary id takes the same invitation');
|
|
234
|
+
}
|
|
235
|
+
// And the three calls are still the calls, not records that shadowed them.
|
|
236
|
+
for (const call of ['knock', 'take', 'remove']) assert.equal(typeof (bob.being.standings as unknown as Record<string, unknown>)[call], 'function', `standings.${call} is the call`);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// ---- the owner
|
|
240
|
+
//
|
|
241
|
+
// The second pointer a ward hands back. Every ask below answers an object,
|
|
242
|
+
// because the owner hears objects and never a word: a refusal is `{ error }`
|
|
243
|
+
// with its reason, which is what a side renders.
|
|
244
|
+
t('the owner boots, marks one public being, invites, knocks, removes and unboots, and refuses each in an object', async () => {
|
|
245
|
+
const { w, shop, alice } = await world();
|
|
246
|
+
const ask = w.owner('S');
|
|
247
|
+
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');
|
|
249
|
+
for (const a of bp.asks) {
|
|
250
|
+
assert.equal(typeof a.description, 'string', `${a.name} says what it is for`);
|
|
251
|
+
assert.equal((a.input as { type: string }).type, 'object', `${a.name} takes one object`);
|
|
252
|
+
}
|
|
253
|
+
assert.match(bp.notes.pk as string, HEX128);
|
|
254
|
+
assert.equal(typeof bp.notes.beings, 'object');
|
|
255
|
+
assert.deepEqual(await ask('nosuch'), { error: 'unknown ask' });
|
|
256
|
+
|
|
257
|
+
// boot: a key already taken is refused, and so is a class nobody holds.
|
|
258
|
+
assert.deepEqual(await ask('boot', { key: 'S', class: 'Shop' }), { error: 'no such class, or key taken' });
|
|
259
|
+
assert.deepEqual(await ask('boot', { key: 'fresh', class: 'NoSuchClass' }), { error: 'no such class, or key taken' });
|
|
260
|
+
assert.deepEqual(await ask('boot', { key: 42, class: 'Shop' }), { error: 'no such class, or key taken' }, 'a name is a string or it is nothing');
|
|
261
|
+
|
|
262
|
+
// public: one per ward, null takes the mark off, and the ward is nobody's
|
|
263
|
+
// public being.
|
|
264
|
+
assert.deepEqual(await ask('public', { key: 'nobody' }), { error: 'no such being' });
|
|
265
|
+
assert.deepEqual(await ask('public', { key: bp.notes.pk }), { error: 'no such being' }, 'the ward is a being to nobody outside');
|
|
266
|
+
|
|
267
|
+
// invite and remove, on a being of the ward, by id.
|
|
268
|
+
assert.deepEqual(await ask('invite', { being: 'nobody', id: 'x' }), { error: 'no such being' });
|
|
269
|
+
const inv = (await ask('invite', { being: 'S', id: 'by-owner', notes: { tier: 'gold' } })) as Invitation;
|
|
270
|
+
assert.match(inv.heir!, HEX64);
|
|
271
|
+
assert.deepEqual((shop.cells.occupants as Record<string, { notes: JsonObject }>)['by-owner'].notes, { tier: 'gold' }, 'the notes she minted under are hers to read');
|
|
272
|
+
assert.deepEqual(await ask('remove', { being: 'S', id: 'nothing-of-hers' }), { error: 'nothing to remove' });
|
|
273
|
+
assert.deepEqual(await ask('remove', { being: 'S', id: 'by-owner' }), { removed: 'by-owner' });
|
|
274
|
+
assert.equal(Object.hasOwn(shop.cells.occupants, 'by-owner'), false);
|
|
275
|
+
|
|
276
|
+
// knock: the owner knocks for a being of hers and takes the standing.
|
|
277
|
+
const forA = (await ask('invite', { being: 'S', id: 'for-alice' })) as Invitation;
|
|
278
|
+
const knocked = (await w.owner('A'))('knock', { being: 'A', id: 'via-owner', invitation: forA, method: 'hello' });
|
|
279
|
+
assert.deepEqual(await knocked, { taken: 'via-owner', answer: { welcome: true } });
|
|
280
|
+
assert.deepEqual(Object.keys(alice.cells.standings), ['via-owner']);
|
|
281
|
+
assert.deepEqual(await alice.being.standings['via-owner']!.ask('hello'), { welcome: true }, 'and it is an ordinary standing');
|
|
282
|
+
|
|
283
|
+
// unboot: the inverse of boot, and the ward refuses itself.
|
|
284
|
+
assert.deepEqual(await ask('unboot', { being: bp.notes.pk }), { error: 'no such being' });
|
|
285
|
+
assert.deepEqual(await ask('unboot', { being: 'nobody' }), { error: 'no such being' });
|
|
286
|
+
const gone = (await ask('unboot', { being: 'S' })) as { unbooted: string; removed: string[] };
|
|
287
|
+
assert.equal(gone.unbooted, 'S');
|
|
288
|
+
assert.ok(gone.removed.includes('for-alice'), 'her relations went with her, and the owner is told which');
|
|
289
|
+
assert.ok(said(await alice.being.standings['via-owner']!.ask('hello'), 'removed'), 'and her occupant hears removed at the door');
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// The one public being, marked and unmarked through the owner. A second
|
|
293
|
+
// one would leave the first holding every relation she had, reachable by
|
|
294
|
+
// nobody at the bare pk and told by nobody that she had been replaced.
|
|
295
|
+
t('a ward has one public being: a second is refused, null takes the mark off, and she keeps everything she held', async () => {
|
|
296
|
+
const { w, alice } = await world();
|
|
297
|
+
const ask = w.owner('A');
|
|
298
|
+
assert.deepEqual(await ask('public', { key: 'A' }), { public: 'A' });
|
|
299
|
+
assert.deepEqual(await ask('public', { key: 'A' }), { public: 'A' }, 'marking the same one again is not a second');
|
|
300
|
+
const bp = (await ask()) as { notes: { beings: Record<string, { public: boolean }> } };
|
|
301
|
+
assert.equal(bp.notes.beings.A.public, true, 'and the owner sees which one she is');
|
|
302
|
+
const inv = (await alice.being.invite('kept'))!;
|
|
303
|
+
assert.deepEqual(await ask('public', { key: null }), { public: null }, 'null takes the mark off');
|
|
304
|
+
const off = (await ask()) as { notes: { beings: Record<string, { public: boolean }> } };
|
|
305
|
+
assert.equal(off.notes.beings.A.public, false);
|
|
306
|
+
assert.ok(Object.hasOwn(alice.cells.occupants, 'kept'), 'retiring her is not destroying her: every relation she held is still hers');
|
|
307
|
+
assert.equal(inv.ward, alice.pk);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// A relation she dropped is nobody's. The word is her own ward's, said to
|
|
311
|
+
// her and never crossing a door, because there is no door left to ask.
|
|
312
|
+
t('a standing she dropped is dropped: the word is hers, nothing crosses, and the far side is never told', async () => {
|
|
313
|
+
const { shop, alice } = await world();
|
|
314
|
+
await alice.being.join(await shop.being.invite());
|
|
315
|
+
const standing = alice.being.standings.shop!;
|
|
316
|
+
alice.being.standings.remove('shop');
|
|
317
|
+
assert.deepEqual(alice.cells.standings, {}, 'gone from her cells');
|
|
318
|
+
assert.equal(alice.being.standings.shop, undefined, 'and gone from her stance');
|
|
319
|
+
assert.ok(said(await standing.ask('hello'), 'dropped'), 'a handle she kept over the drop reaches nothing, and says so');
|
|
320
|
+
assert.ok(Object.hasOwn(shop.cells.occupants, 'cust1'), 'the far side holds her seat still: dropping is hers alone and she tells nobody');
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// ---- a being may make
|
|
324
|
+
//
|
|
325
|
+
// `boot` is on every stance, so a kit that never wired it hands its beings a
|
|
326
|
+
// call that answers null for ever and nothing else here would notice.
|
|
327
|
+
t('a being boots one of her ward s classes from inside her own answer, and may ask for a way back to what she made', async () => {
|
|
328
|
+
const { w } = await world();
|
|
329
|
+
const maker = await w.boot<Maker>('MK', Maker);
|
|
330
|
+
// A harbor holds the classes its device gave it, so one Member is booted
|
|
331
|
+
// to put the class there. What the maker does after that is the ward's.
|
|
332
|
+
await w.boot<Member>('MEM', Member);
|
|
333
|
+
assert.deepEqual(await maker.being.open({ class: 'Member', key: 'made-1' }), { made: 'made-1' });
|
|
334
|
+
assert.deepEqual(await maker.being.open({ class: 'Member', key: 'made-1' }), { made: null }, 'a key already taken makes nobody');
|
|
335
|
+
assert.deepEqual(await maker.being.open({ class: 'NoSuchClass', key: 'made-2' }), { made: null }, 'a class the harbor does not hold makes nobody');
|
|
336
|
+
assert.deepEqual(maker.cells.made, ['made-1']);
|
|
337
|
+
// With an id she holds a standing to what she made, and the being made
|
|
338
|
+
// knows her by her own key and by nothing else.
|
|
339
|
+
assert.deepEqual(await maker.being.open({ class: 'Member', key: 'made-3', id: 'mine' }), { made: 'made-3' });
|
|
340
|
+
assert.deepEqual(await maker.being.say({ id: 'mine' }), { said: { pong: 'MK' } }, 'she asks through the relation, and arrives under her own key');
|
|
341
|
+
assert.deepEqual(await maker.being.say({ id: 'nothing' }), { said: null });
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// The case the estate's walk reaches by weight and this reaches by hand: a
|
|
345
|
+
// seat kicked after it answered and before it was taken. She holds a key
|
|
346
|
+
// the door bound for her, so she is the one person who hears why, and she
|
|
347
|
+
// hears it every time. A kit that forgot to keep the last keys of a
|
|
348
|
+
// forgotten heir answers her silence, which is what it says to a stranger,
|
|
349
|
+
// and she cannot tell being kicked from never having been let in.
|
|
350
|
+
t('a relation kicked between the knock and the take: she hears removed, every time, and the take births nothing', async () => {
|
|
351
|
+
const { shop, alice, bob } = await world();
|
|
352
|
+
const inv = await shop.being.invite();
|
|
353
|
+
assert.deepEqual(await alice.being.knock(inv, 'hello'), { welcome: true }, 'she knocked and was answered');
|
|
354
|
+
shop.being.occupants.remove('cust1'); // kicked, and she is not told
|
|
355
|
+
for (let i = 0; i < 3; i++) assert.ok(said(await alice.being.knock(inv, 'hello'), 'removed'), `knock ${i}: a spent heir stays spent, and she hears why every time`);
|
|
356
|
+
// And the take still births one: it reads the knock record, which says
|
|
357
|
+
// she was answered, and the far side kicking her afterwards is not
|
|
358
|
+
// something her side can know or should guess at. The standing is hers,
|
|
359
|
+
// and it is the standing that hears removed from then on. A kit that
|
|
360
|
+
// refused the take here would leave her with no record to read the
|
|
361
|
+
// refusal through, and nothing to drop.
|
|
362
|
+
assert.equal(await alice.being.take('shop', inv), 'shop', 'the take reads the knock record, and the knock was answered');
|
|
363
|
+
assert.ok(said(await alice.being.standings.shop!.ask('hello'), 'removed'), 'and the standing is where she hears it now');
|
|
364
|
+
alice.being.standings.remove('shop');
|
|
365
|
+
assert.deepEqual(alice.cells.standings, {}, 'hers to drop, like any other');
|
|
366
|
+
// And only to her: the same invitation in anyone else's hand is silence,
|
|
367
|
+
// because the keys the door kept are hers and nobody else's.
|
|
368
|
+
assert.ok(isSilence(await bob.being.knock(inv, 'hello')), 'a stranger holding the same invitation learns nothing at all');
|
|
369
|
+
});
|
|
370
|
+
|
|
198
371
|
t('take without a knock, or after a knock that was not answered, births nothing', async () => {
|
|
199
372
|
const { shop, alice } = await world();
|
|
200
373
|
const inv = await shop.being.invite();
|
|
@@ -452,20 +625,22 @@ export function conform(label: string, make: () => Promise<World>, { canDown = t
|
|
|
452
625
|
|
|
453
626
|
t('a public being is asked by anyone as {}; a knock on her needs no heir and may be taken; a ward with none is silence; an unknown pk is unreached', async () => {
|
|
454
627
|
const w = await make();
|
|
628
|
+
// A being written by hand: no base class, one answer, and the stance is
|
|
629
|
+
// all she is handed. She is a being to the ward on those terms alone.
|
|
455
630
|
class Pub {
|
|
456
|
-
s:
|
|
457
|
-
constructor(s:
|
|
631
|
+
s: Stance;
|
|
632
|
+
constructor(s: Stance) {
|
|
458
633
|
this.s = s;
|
|
459
634
|
this.s.cells.askers ??= [];
|
|
460
635
|
}
|
|
461
|
-
answer(asker: Asker, m?: string, args?: JsonObject) {
|
|
636
|
+
answer(asker: Asker, m?: string, args?: JsonObject): Reply {
|
|
462
637
|
if (m === undefined) return { asks: [{ name: 'time', input: {} }], notes: {} };
|
|
463
638
|
(this.s.cells.askers as Json[]).push(asker as Json);
|
|
464
639
|
return { time: 12, got: args ?? {} };
|
|
465
640
|
}
|
|
466
641
|
}
|
|
467
|
-
const pub = await w.boot<Pub>('PUB', Pub
|
|
468
|
-
const priv = await w.boot<Pub>('PRIV', Pub
|
|
642
|
+
const pub = await w.boot<Pub>('PUB', Pub, {}, { isPublic: true });
|
|
643
|
+
const priv = await w.boot<Pub>('PRIV', Pub); // same class, not public
|
|
469
644
|
const a = await w.boot<Customer>('A', Customer);
|
|
470
645
|
const b = await w.boot<Customer>('B', Customer);
|
|
471
646
|
const at = (h: { pk: string }) => ({ ward: h.pk });
|