nervur 0.19.2 → 0.20.0
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 +66 -101
- package/dist/being/being.d.ts +7 -15
- package/dist/being/being.js +16 -67
- package/dist/being/digest.d.ts +1 -1
- package/dist/being/digest.js +7 -27
- package/dist/being/faculty.d.ts +9 -0
- package/dist/being/faculty.js +68 -0
- package/dist/being/index.d.ts +4 -5
- package/dist/being/index.js +5 -5
- package/dist/being/types.d.ts +35 -62
- package/dist/being/types.js +3 -57
- package/dist/being/words.d.ts +13 -0
- package/dist/being/words.js +14 -0
- package/dist/contract/index.d.ts +26 -0
- package/dist/contract/index.js +19 -0
- package/dist/crypto/aes.d.ts +3 -0
- package/dist/crypto/aes.js +24 -0
- package/dist/crypto/bytes.d.ts +6 -0
- package/dist/crypto/bytes.js +33 -0
- package/dist/crypto/ed25519.d.ts +3 -0
- package/dist/crypto/ed25519.js +85 -0
- package/dist/crypto/hash.d.ts +2 -0
- package/dist/crypto/hash.js +11 -0
- package/dist/crypto/index.d.ts +7 -0
- package/dist/crypto/index.js +10 -0
- package/dist/crypto/json.d.ts +8 -0
- package/dist/crypto/json.js +250 -0
- package/dist/crypto/mlkem.d.ts +12 -0
- package/dist/crypto/mlkem.js +36 -0
- package/dist/crypto/subtle.d.ts +10 -0
- package/dist/crypto/subtle.js +29 -0
- package/dist/crypto/x25519.d.ts +2 -0
- package/dist/crypto/x25519.js +21 -0
- package/dist/folder/index.d.ts +13 -0
- package/dist/folder/index.js +125 -0
- package/dist/harbor/dock.d.ts +19 -0
- package/dist/harbor/dock.js +92 -0
- package/dist/harbor/harbor.d.ts +22 -0
- package/dist/harbor/harbor.js +84 -0
- package/dist/harbor/index.d.ts +4 -10
- package/dist/harbor/index.js +5 -18
- package/dist/harbor/registry.d.ts +7 -0
- package/dist/harbor/registry.js +13 -0
- package/dist/harbor/terrain.d.ts +8 -0
- package/dist/harbor/terrain.js +2 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +14 -0
- package/dist/pointer/bodies.d.ts +20 -0
- package/dist/pointer/bodies.js +45 -0
- package/dist/pointer/index.d.ts +2 -0
- package/dist/pointer/index.js +4 -0
- package/dist/pointer/world.d.ts +34 -0
- package/dist/pointer/world.js +76 -0
- package/dist/quo/door.d.ts +34 -0
- package/dist/quo/door.js +172 -0
- package/dist/quo/index.d.ts +8 -0
- package/dist/quo/index.js +11 -0
- package/dist/quo/invitation.d.ts +7 -0
- package/dist/quo/invitation.js +15 -0
- package/dist/quo/keys.d.ts +40 -0
- package/dist/quo/keys.js +79 -0
- package/dist/quo/payload.d.ts +15 -0
- package/dist/quo/payload.js +55 -0
- package/dist/quo/relations.d.ts +40 -0
- package/dist/quo/relations.js +33 -0
- package/dist/quo/reply.d.ts +13 -0
- package/dist/quo/reply.js +35 -0
- package/dist/quo/seal.d.ts +42 -0
- package/dist/quo/seal.js +78 -0
- package/dist/quo/standing.d.ts +39 -0
- package/dist/quo/standing.js +90 -0
- package/dist/ward/allowance.d.ts +13 -9
- package/dist/ward/allowance.js +28 -67
- package/dist/ward/cells.d.ts +6 -6
- package/dist/ward/cells.js +67 -179
- package/dist/ward/index.d.ts +5 -10
- package/dist/ward/index.js +6 -13
- package/dist/ward/partition.d.ts +44 -60
- package/dist/ward/partition.js +56 -285
- package/dist/ward/stance.d.ts +29 -20
- package/dist/ward/stance.js +173 -406
- package/dist/ward/ward-being.d.ts +33 -0
- package/dist/ward/ward-being.js +76 -0
- package/dist/ward/ward.d.ts +43 -11
- package/dist/ward/ward.js +215 -363
- package/package.json +19 -33
- package/src/being/being.ts +31 -78
- package/src/being/digest.ts +7 -35
- package/src/being/faculty.ts +66 -0
- package/src/being/index.ts +5 -6
- package/src/being/types.ts +47 -145
- package/src/being/words.ts +31 -0
- package/src/contract/index.ts +44 -0
- package/src/crypto/aes.ts +26 -0
- package/src/crypto/bytes.ts +37 -0
- package/src/crypto/ed25519.ts +84 -0
- package/src/crypto/hash.ts +14 -0
- package/src/crypto/index.ts +10 -0
- package/src/crypto/json.ts +241 -0
- package/src/crypto/mlkem.ts +38 -0
- package/src/crypto/subtle.ts +33 -0
- package/src/crypto/x25519.ts +21 -0
- package/src/folder/index.ts +134 -0
- package/src/harbor/dock.ts +101 -0
- package/src/harbor/harbor.ts +105 -0
- package/src/harbor/index.ts +5 -19
- package/src/harbor/registry.ts +20 -0
- package/src/harbor/terrain.ts +13 -0
- package/src/index.ts +20 -0
- package/src/pointer/bodies.ts +47 -0
- package/src/pointer/index.ts +4 -0
- package/src/pointer/world.ts +91 -0
- package/src/quo/door.ts +178 -0
- package/src/quo/index.ts +11 -0
- package/src/quo/invitation.ts +15 -0
- package/src/quo/keys.ts +91 -0
- package/src/quo/payload.ts +61 -0
- package/src/quo/relations.ts +62 -0
- package/src/quo/reply.ts +38 -0
- package/src/quo/seal.ts +101 -0
- package/src/quo/standing.ts +111 -0
- package/src/stand/main.ts +19 -0
- package/src/stand/stand.ts +178 -0
- package/src/ward/allowance.ts +37 -75
- package/src/ward/cells.ts +63 -176
- package/src/ward/index.ts +6 -17
- package/src/ward/partition.ts +86 -326
- package/src/ward/stance.ts +185 -420
- package/src/ward/ward-being.ts +97 -0
- package/src/ward/ward.ts +229 -363
- package/dist/being/lent.d.ts +0 -32
- package/dist/being/lent.js +0 -72
- package/dist/being/silence.d.ts +0 -12
- package/dist/being/silence.js +0 -41
- package/dist/conformance/assert.d.ts +0 -11
- package/dist/conformance/assert.js +0 -106
- package/dist/conformance/beings.d.ts +0 -199
- package/dist/conformance/beings.js +0 -188
- package/dist/conformance/estate.d.ts +0 -5
- package/dist/conformance/estate.js +0 -388
- package/dist/conformance/index.d.ts +0 -80
- package/dist/conformance/index.js +0 -819
- package/dist/conformance/reach.d.ts +0 -10
- package/dist/conformance/reach.js +0 -72
- package/dist/conformance/store.d.ts +0 -5
- package/dist/conformance/store.js +0 -113
- package/dist/harbor/box.d.ts +0 -121
- package/dist/harbor/box.js +0 -121
- package/dist/harbor/core.d.ts +0 -55
- package/dist/harbor/core.js +0 -662
- package/dist/harbor/dial.d.ts +0 -9
- package/dist/harbor/dial.js +0 -81
- package/dist/harbor/memory.d.ts +0 -29
- package/dist/harbor/memory.js +0 -104
- package/dist/harbor/reach.d.ts +0 -36
- package/dist/harbor/reach.js +0 -199
- package/dist/harbor/store.d.ts +0 -35
- package/dist/harbor/store.js +0 -62
- package/dist/vector/cases.d.ts +0 -42
- package/dist/vector/cases.js +0 -223
- package/dist/vector/index.d.ts +0 -6
- package/dist/vector/index.js +0 -8
- package/dist/vector/stand.d.ts +0 -9
- package/dist/vector/stand.js +0 -77
- package/dist/vector/world.d.ts +0 -144
- package/dist/vector/world.js +0 -209
- package/dist/ward/arithmetic.d.ts +0 -31
- package/dist/ward/arithmetic.js +0 -249
- package/dist/ward/door.d.ts +0 -18
- package/dist/ward/door.js +0 -186
- package/dist/ward/ground.d.ts +0 -29
- package/dist/ward/ground.js +0 -56
- package/dist/ward/heirs.d.ts +0 -13
- package/dist/ward/heirs.js +0 -117
- package/dist/ward/json.d.ts +0 -2
- package/dist/ward/json.js +0 -163
- package/dist/ward/owner.d.ts +0 -13
- package/dist/ward/owner.js +0 -220
- package/dist/ward/seal.d.ts +0 -52
- package/dist/ward/seal.js +0 -150
- package/src/being/lent.ts +0 -72
- package/src/being/silence.ts +0 -46
- package/src/conformance/assert.ts +0 -100
- package/src/conformance/beings.ts +0 -188
- package/src/conformance/estate.ts +0 -412
- package/src/conformance/index.ts +0 -965
- package/src/conformance/reach.ts +0 -83
- package/src/conformance/store.ts +0 -125
- package/src/harbor/box.ts +0 -131
- package/src/harbor/core.ts +0 -699
- package/src/harbor/dial.ts +0 -112
- package/src/harbor/memory.ts +0 -123
- package/src/harbor/reach.ts +0 -221
- package/src/harbor/store.ts +0 -91
- package/src/vector/cases.ts +0 -257
- package/src/vector/index.ts +0 -11
- package/src/vector/stand.ts +0 -76
- package/src/vector/world.ts +0 -232
- package/src/ward/arithmetic.ts +0 -251
- package/src/ward/door.ts +0 -186
- package/src/ward/ground.ts +0 -142
- package/src/ward/heirs.ts +0 -116
- package/src/ward/json.ts +0 -144
- package/src/ward/owner.ts +0 -214
- package/src/ward/seal.ts +0 -178
|
@@ -1,412 +0,0 @@
|
|
|
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 two 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
|
-
//
|
|
21
|
-
// The second is asked under one ward, under a ward per being, and under two
|
|
22
|
-
// harbors, and that is where the promise of Quo is held: a being never learns
|
|
23
|
-
// where the other one is, so every answer, after every move, under every
|
|
24
|
-
// topology, is the answer the model wrote without knowing the topology
|
|
25
|
-
// either. Held after every one of sixty moves, it is the promise at a scale a scene
|
|
26
|
-
// cannot reach, and it is what makes a migration honest: if no answer can
|
|
27
|
-
// tell the topology apart, moving a ward between harbors cannot be felt.
|
|
28
|
-
// The model itself is the script's own bookkeeping and takes nothing from a
|
|
29
|
-
// ward, so it is never compared with itself across topologies: that would
|
|
30
|
-
// hold by construction and prove nothing.
|
|
31
|
-
//
|
|
32
|
-
// A relation is two arcs and never one edge. The occupant arc is the host's,
|
|
33
|
-
// born at invite; the standing arc is the guest's, born at take, and only
|
|
34
|
-
// after an answer. `SPEC.md`: take binds B's side only, the knock bound hers.
|
|
35
|
-
// A model that kept one symmetric edge would be a wrong model, and would
|
|
36
|
-
// agree happily with a ward that had drifted.
|
|
37
|
-
import { assert } from './assert.ts';
|
|
38
|
-
import { isSilence, isUnreached, told } from '../being/silence.ts';
|
|
39
|
-
import type { Invitation } from '../being/types.ts';
|
|
40
|
-
import { Member } from './beings.ts';
|
|
41
|
-
import { HEX64 } from './index.ts';
|
|
42
|
-
import type { Census, Handle, Runner, World } from './index.ts';
|
|
43
|
-
|
|
44
|
-
const said = (x: unknown, w: string): boolean => told(x) === w;
|
|
45
|
-
|
|
46
|
-
// One occupant arc, as the host holds it. `guest` is null until somebody
|
|
47
|
-
// knocks; `live` goes false when she is kicked. Both are the host's business
|
|
48
|
-
// alone -- the guest is never told either one.
|
|
49
|
-
type Seat = { host: string; id: string; inv: Invitation; guest: string | null; answered: boolean; live: boolean; taken: boolean };
|
|
50
|
-
// One standing arc, as the guest holds it. It carries the seat it was born
|
|
51
|
-
// on, because what an ask on it should come back with is the host's name for
|
|
52
|
-
// her, and whether the host has since kicked her.
|
|
53
|
-
type Hold = { owner: string; id: string; seat: Seat; live: boolean };
|
|
54
|
-
type Graph = { seats: Seat[]; holds: Hold[] };
|
|
55
|
-
|
|
56
|
-
// A pseudo-random source in the language alone: the estate must replay, on
|
|
57
|
-
// every terrain, from a number a failure can print.
|
|
58
|
-
const rolls = (seed: number) => () => {
|
|
59
|
-
seed = (seed + 0x6d2b79f5) | 0;
|
|
60
|
-
let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
|
|
61
|
-
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
62
|
-
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
// ---- the ledger is a graph
|
|
66
|
-
|
|
67
|
-
// Held after every move. Each line is a way the bookkeeping could stop being
|
|
68
|
-
// a graph: an arc with one end, an arc with two owners, a relation filed in
|
|
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, g?: Graph) {
|
|
71
|
-
const at = (why: string) => `${where}: ${why}`;
|
|
72
|
-
const heirsSeen = new Set<string>();
|
|
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
|
-
|
|
111
|
-
for (const [pk, w] of Object.entries(c.wards)) {
|
|
112
|
-
for (const [heir, r] of Object.entries(w.heirs)) {
|
|
113
|
-
assert.ok(!heirsSeen.has(heir), at(`heir ${heir} is held by two wards`));
|
|
114
|
-
heirsSeen.add(heir);
|
|
115
|
-
assert.match(heir, HEX64, at(`heir ${heir} is not a key`));
|
|
116
|
-
// the far end of the arc: the being the ward says minted it holds it back, under the same id
|
|
117
|
-
const b = c.binds[r.being];
|
|
118
|
-
assert.ok(b, at(`heir ${heir} names ${r.being}, who lives in no ward`));
|
|
119
|
-
assert.equal(b.ward, pk, at(`heir ${heir} is kept by ${pk}, but ${r.being} lives behind ${b.ward}`));
|
|
120
|
-
assert.equal(b.occupants[r.id], heir, at(`heir ${heir} says it is ${r.being}'s ${r.id}, who does not hold it`));
|
|
121
|
-
assert.ok(r.spent.every((n) => n <= r.mark), at(`heir ${heir} spent a number above its mark`));
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
for (const [key, b] of Object.entries(c.binds)) {
|
|
126
|
-
// every occupant id the being holds is a heir her own ward keeps
|
|
127
|
-
for (const [id, heir] of Object.entries(b.occupants)) {
|
|
128
|
-
const r = c.wards[b.ward].heirs[heir];
|
|
129
|
-
assert.ok(r, at(`${key}'s occupant ${id} names heir ${heir}, which her ward has forgotten`));
|
|
130
|
-
assert.equal(r.being, key, at(`${key}'s occupant ${id} names a heir of ${r.being}'s`));
|
|
131
|
-
assert.equal(r.id, id, at(`${key}'s occupant ${id} names a heir filed under ${r.id}`));
|
|
132
|
-
}
|
|
133
|
-
// a taken relation lives in the standing and never also in a knock record
|
|
134
|
-
for (const [id, s] of Object.entries(b.standings)) {
|
|
135
|
-
const name = s.heir === null ? `public:${s.ward}` : `${s.ward}:${s.heir}`;
|
|
136
|
-
assert.ok(!b.knocks.includes(name), at(`${key}'s standing ${id} still has a knock record`));
|
|
137
|
-
assert.ok(!b.answered.includes(name), at(`${key}'s standing ${id} is still marked answered`));
|
|
138
|
-
}
|
|
139
|
-
// no key her side holds is anywhere in anyone's cells
|
|
140
|
-
const cells = JSON.stringify(c.cells);
|
|
141
|
-
for (const word of b.secrets) assert.ok(!cells.includes(word), at(`a key of ${key}'s is in the cells`));
|
|
142
|
-
// counts climb and never fall. a door that forgot what it honoured honours it again
|
|
143
|
-
const was = before?.binds[key];
|
|
144
|
-
if (was)
|
|
145
|
-
for (const [id, s] of Object.entries(b.standings)) assert.ok(!was.standings[id] || s.seq >= was.standings[id].seq, at(`${key}'s standing ${id} counted backwards`));
|
|
146
|
-
}
|
|
147
|
-
if (before)
|
|
148
|
-
for (const [pk, w] of Object.entries(c.wards))
|
|
149
|
-
for (const [heir, r] of Object.entries(w.heirs)) {
|
|
150
|
-
const was = before.wards[pk]?.heirs[heir];
|
|
151
|
-
if (was) assert.ok(r.mark >= was.mark, at(`heir ${heir} marked backwards`));
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// ---- the chapter
|
|
156
|
-
|
|
157
|
-
export function estate(label: string, make: () => Promise<World>, { canDown, test }: { canDown: boolean; test: Runner }) {
|
|
158
|
-
const t = (name: string, fn: () => Promise<void> | void) => test(`[${label}] ${name}`, {}, fn);
|
|
159
|
-
|
|
160
|
-
// The story, told once, by hand, so that the next person can read what the
|
|
161
|
-
// model is for. A partner is invited into a harbor she is borrowing, works,
|
|
162
|
-
// moves to a harbor of her own, is kicked, and is invited back as somebody
|
|
163
|
-
// else. Nothing here is random and nothing here is generated.
|
|
164
|
-
t('estate: a partner is invited into a borrowed harbor, works, moves to her own, is kicked, and comes back as somebody else', async () => {
|
|
165
|
-
const w = await make();
|
|
166
|
-
const acme = await w.boot<Member>('ACME', Member);
|
|
167
|
-
const partner = await w.boot<Member>('PARTNER', Member);
|
|
168
|
-
|
|
169
|
-
// 1. acme mints a name for her and asks her ward for an invitation. the
|
|
170
|
-
// occupant exists from that moment, and the partner knows nothing yet.
|
|
171
|
-
const first = (await acme.being.invite('partner-1'))!;
|
|
172
|
-
assert.deepEqual(Object.keys(acme.cells.occupants), ['partner-1']);
|
|
173
|
-
assert.deepEqual(partner.cells.standings, {});
|
|
174
|
-
|
|
175
|
-
// 2. she knocks. from now on every ask of hers arrives as that name.
|
|
176
|
-
assert.deepEqual(await partner.being.knock(first, 'ping'), { pong: 'partner-1' });
|
|
177
|
-
// 3. and only then does she keep acme, under a name of her own minting.
|
|
178
|
-
assert.equal(await partner.being.take('the-client', first), 'the-client');
|
|
179
|
-
assert.deepEqual(await partner.being.standings['the-client']!.ask('ping'), { pong: 'partner-1' });
|
|
180
|
-
|
|
181
|
-
// 4. she gets her own harbor. the same seed, the same pk, so the arc acme
|
|
182
|
-
// holds still points at her and the arc she holds still points at acme.
|
|
183
|
-
await w.migrate('PARTNER');
|
|
184
|
-
await w.migrate('ACME');
|
|
185
|
-
assert.deepEqual(await partner.being.standings['the-client']!.ask('ping'), { pong: 'partner-1' });
|
|
186
|
-
const back = (await partner.being.invite('acme'))!;
|
|
187
|
-
assert.deepEqual(await acme.being.knock(back, 'ping'), { pong: 'acme' });
|
|
188
|
-
assert.equal(await acme.being.take('them', back), 'them');
|
|
189
|
-
|
|
190
|
-
// 5. acme kicks her. her standing is still hers to hold, and she is not
|
|
191
|
-
// told until she asks: then the door, under the key it bound for her,
|
|
192
|
-
// says removed, and only to her.
|
|
193
|
-
acme.being.occupants.remove('partner-1');
|
|
194
|
-
assert.ok(said(await partner.being.standings['the-client']!.ask('ping'), 'removed'));
|
|
195
|
-
assert.deepEqual(Object.keys(partner.cells.standings), ['the-client']); // she never learns
|
|
196
|
-
assert.deepEqual(await acme.being.standings.them!.ask('ping'), { pong: 'acme' }); // the other arc never moved
|
|
197
|
-
|
|
198
|
-
// 6. and back, as somebody else: a second invitation is a second relation,
|
|
199
|
-
// sharing nothing with the first. the old one stays dead.
|
|
200
|
-
const second = (await acme.being.invite('partner-2'))!;
|
|
201
|
-
assert.notEqual(second.heir, first.heir);
|
|
202
|
-
assert.deepEqual(await partner.being.knock(second, 'ping'), { pong: 'partner-2' });
|
|
203
|
-
assert.equal(await partner.being.take('the-client-again', second), 'the-client-again');
|
|
204
|
-
assert.ok(said(await partner.being.standings['the-client']!.ask('ping'), 'removed'));
|
|
205
|
-
ledger(w.census(), undefined, 'the story');
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
for (const seed of [1, 2, 3]) {
|
|
209
|
-
t(`estate: sixty moves from seed ${seed}, the ledger a graph and every arc asked after each one`, async () => {
|
|
210
|
-
const w = await make();
|
|
211
|
-
const roll = rolls(seed);
|
|
212
|
-
const pick = <T>(xs: T[]): T => xs[Math.floor(roll() * xs.length)];
|
|
213
|
-
const hands: Record<string, Handle<Member>> = {};
|
|
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
|
-
];
|
|
227
|
-
let minted = 0;
|
|
228
|
-
const born = async (key: string) => (hands[key] = await w.boot<Member>(key, Member));
|
|
229
|
-
for (const key of ['M1', 'M2', 'M3']) await born(key);
|
|
230
|
-
let before: Census | undefined;
|
|
231
|
-
|
|
232
|
-
// Every move that is legal right now, as a thunk. A move that would not
|
|
233
|
-
// be legal is not written down, so the chapter never asserts on a
|
|
234
|
-
// question the spec does not answer.
|
|
235
|
-
const moves = (): (() => Promise<string>)[] => {
|
|
236
|
-
const keys = Object.keys(hands);
|
|
237
|
-
const out: (() => Promise<string>)[] = [];
|
|
238
|
-
|
|
239
|
-
// invite: a fresh occupant arc, and nothing of the guest's in it
|
|
240
|
-
out.push(async () => {
|
|
241
|
-
const host = pick(keys);
|
|
242
|
-
const id = `o${++minted}`;
|
|
243
|
-
const inv = (await hands[host].being.invite(id))!;
|
|
244
|
-
assert.equal(inv.ward, hands[host].pk);
|
|
245
|
-
assert.match(inv.heir!, HEX64);
|
|
246
|
-
g.seats.push({ host, id, inv, guest: null, answered: false, live: true, taken: false });
|
|
247
|
-
return `${host} invites ${id}`;
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
// knock: the guest an unspent seat gets is anyone but the host; a
|
|
251
|
-
// spent one only ever answers the one who spent it. Once she has
|
|
252
|
-
// taken it the invitation is not a knock any more -- it joins the
|
|
253
|
-
// standing's lane, and is exactly as alive as the standing is.
|
|
254
|
-
for (const s of g.seats) {
|
|
255
|
-
const knock = async () => {
|
|
256
|
-
const guest = s.guest ?? pick(keys.filter((k) => k !== s.host));
|
|
257
|
-
const hold = g.holds.find((h) => h.seat === s);
|
|
258
|
-
const back = await hands[guest].being.knock(s.inv, 'ping');
|
|
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) {
|
|
268
|
-
assert.deepEqual(back, { pong: s.id }, `${guest} knocking ${s.host}/${s.id}`);
|
|
269
|
-
s.guest = guest;
|
|
270
|
-
s.answered = true;
|
|
271
|
-
} else {
|
|
272
|
-
assert.ok(said(back, 'removed'), `${guest} knocking kicked ${s.host}/${s.id} should hear removed`);
|
|
273
|
-
reached.add('a kicked seat knocked by the guest it bound, who hears removed');
|
|
274
|
-
}
|
|
275
|
-
return `${guest} knocks ${s.host}/${s.id}`;
|
|
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);
|
|
286
|
-
// a stranger on a spent seat: silence, and nothing rebinds
|
|
287
|
-
if (s.guest !== null)
|
|
288
|
-
out.push(async () => {
|
|
289
|
-
const other = keys.filter((k) => k !== s.host && k !== s.guest);
|
|
290
|
-
if (!other.length) return 'no stranger to try';
|
|
291
|
-
const who = pick(other);
|
|
292
|
-
assert.ok(isSilence(await hands[who].being.knock(s.inv, 'ping')), `${who} should not get in on ${s.host}/${s.id}`);
|
|
293
|
-
return `${who} is refused at ${s.host}/${s.id}`;
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// take: only after an answer, only once. the far side may have kicked
|
|
298
|
-
// her in between -- take reads the knock record, and still births one.
|
|
299
|
-
for (const s of g.seats.filter((s) => s.answered && !s.taken))
|
|
300
|
-
out.push(async () => {
|
|
301
|
-
const id = `s${++minted}`;
|
|
302
|
-
assert.equal(await hands[s.guest!].being.take(id, s.inv), id);
|
|
303
|
-
s.taken = true;
|
|
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');
|
|
306
|
-
assert.equal(await hands[s.guest!].being.take(`${id}-again`, s.inv), null); // spent for her
|
|
307
|
-
return `${s.guest} takes ${s.host}/${s.id} as ${id}`;
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
// drop: hers to drop, and the host is never told
|
|
311
|
-
for (const h of g.holds.filter((h) => h.live))
|
|
312
|
-
out.push(async () => {
|
|
313
|
-
hands[h.owner].being.standings.remove(h.id);
|
|
314
|
-
h.live = false;
|
|
315
|
-
reached.add('a standing dropped by its owner, whose host is never told');
|
|
316
|
-
assert.equal(hands[h.owner].being.standings[h.id], undefined);
|
|
317
|
-
return `${h.owner} drops ${h.id}`;
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
// kick: the host's, and the guest is never told either
|
|
321
|
-
for (const s of g.seats.filter((s) => s.live))
|
|
322
|
-
out.push(async () => {
|
|
323
|
-
hands[s.host].being.occupants.remove(s.id);
|
|
324
|
-
s.live = false;
|
|
325
|
-
reached.add('a seat kicked by its host, whose guest is never told');
|
|
326
|
-
return `${s.host} kicks ${s.id}`;
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
// a new being: an estate grows
|
|
330
|
-
if (keys.length < 6) out.push(async () => (await born(`M${keys.length + 1}`), `M${keys.length + 1} is booted`));
|
|
331
|
-
|
|
332
|
-
// a ward moves harbor, under everyone standing on it
|
|
333
|
-
out.push(async () => {
|
|
334
|
-
const who = pick(keys);
|
|
335
|
-
await w.migrate(who);
|
|
336
|
-
reached.add('a ward moved to another harbor under everyone standing on it');
|
|
337
|
-
return `${who}'s ward moves harbor`;
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
// weather: the host's door is gone. unreached, never silence, and the
|
|
341
|
-
// relation is exactly where it was when it comes back. The move is on
|
|
342
|
-
// the list under every topology even where there is nothing to cut,
|
|
343
|
-
// because the list is the script: a move missing in one world would
|
|
344
|
-
// make it a different script, and the three could not be compared.
|
|
345
|
-
for (const h of g.holds.filter((h) => h.live && h.seat.live))
|
|
346
|
-
out.push(async () => {
|
|
347
|
-
if (!canDown) return 'nothing to cut inside one ward';
|
|
348
|
-
hands[h.seat.host].down = true;
|
|
349
|
-
assert.ok(isUnreached(await hands[h.owner].being.standings[h.id]!.ask('ping')), `${h.owner}/${h.id} should be unreached`);
|
|
350
|
-
hands[h.seat.host].down = false;
|
|
351
|
-
return `${h.seat.host} is cut and comes back`;
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
return out;
|
|
355
|
-
};
|
|
356
|
-
|
|
357
|
-
// Every arc in the model, asked. This is where the ledger stops being
|
|
358
|
-
// bookkeeping and becomes what the door actually does.
|
|
359
|
-
const sweep = async (step: number) => {
|
|
360
|
-
for (const h of g.holds) {
|
|
361
|
-
const standing = hands[h.owner].being.standings[h.id];
|
|
362
|
-
if (!h.live) {
|
|
363
|
-
assert.equal(standing, undefined, `step ${step}: ${h.owner}'s dropped ${h.id} is still there`);
|
|
364
|
-
continue;
|
|
365
|
-
}
|
|
366
|
-
const out = await standing!.ask('ping');
|
|
367
|
-
if (h.seat.live) assert.deepEqual(out, { pong: h.seat.id }, `step ${step}: ${h.owner}/${h.id} -> ${h.seat.host}/${h.seat.id}`);
|
|
368
|
-
else assert.ok(said(out, 'removed'), `step ${step}: ${h.owner}/${h.id} is kicked and should hear removed`);
|
|
369
|
-
}
|
|
370
|
-
};
|
|
371
|
-
|
|
372
|
-
// The trace is the seed's own account of itself: a red run prints the
|
|
373
|
-
// moves that got there, which is the only thing that makes a generated
|
|
374
|
-
// sequence debuggable at all.
|
|
375
|
-
const trace: string[] = [];
|
|
376
|
-
const at = (step: number) => `seed ${seed} step ${step}\n ${trace.join('\n ')}`;
|
|
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> => {
|
|
381
|
-
try {
|
|
382
|
-
return await work();
|
|
383
|
-
} catch (e) {
|
|
384
|
-
throw new Error(`${at(step)}\n -> ${(e as Error).message}`, { cause: e });
|
|
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;
|
|
398
|
-
const c = w.census();
|
|
399
|
-
ledger(c, before, at(step), g);
|
|
400
|
-
before = c;
|
|
401
|
-
await inStep(step, () => sweep(step));
|
|
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 ')}`);
|
|
410
|
-
});
|
|
411
|
-
}
|
|
412
|
-
}
|