@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
package/dist/ward/ward.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// leaves, opens every one that arrives.
|
|
7
7
|
import { silence, isSilence, unreached, isWord, word } from '../being/silence.js';
|
|
8
8
|
import { OWNER } from '../being/types.js';
|
|
9
|
-
import { at, drop, open, put, emptyBind, emptyCells } from './partition.js';
|
|
9
|
+
import { at, drop, open, put, emptyBind, emptyCells, MINTED } from './partition.js';
|
|
10
10
|
import { Heirs } from './heirs.js';
|
|
11
11
|
import { makeDoor } from './door.js';
|
|
12
12
|
import { buildStance } from './stance.js';
|
|
@@ -14,9 +14,7 @@ import { ownerAnswer } from './owner.js';
|
|
|
14
14
|
import { allow, within, LATE } from './allowance.js';
|
|
15
15
|
import { beingKey, openReply, sealAsk, wardKey, wardPadlock, wardSignPk } from './seal.js';
|
|
16
16
|
import { unhex } from './arithmetic.js';
|
|
17
|
-
import { guardCells, cellFault } from './cells.js';
|
|
18
|
-
// How many minted pks she keeps behind her. Enough to name the keys in play.
|
|
19
|
-
const MINTED = 8;
|
|
17
|
+
import { guardCells, unguarded, cellFault } from './cells.js';
|
|
20
18
|
// The ward. One call in, two pointers out. Async because keys are derived.
|
|
21
19
|
export async function Ward(ground) {
|
|
22
20
|
const w = await Self.boot(ground);
|
|
@@ -29,6 +27,7 @@ class Self {
|
|
|
29
27
|
p;
|
|
30
28
|
heirs;
|
|
31
29
|
doors = new Map();
|
|
30
|
+
#birthing = new Set(); // keys whose constructor is running: taken, though no door and no row say so yet
|
|
32
31
|
door;
|
|
33
32
|
ask;
|
|
34
33
|
static async boot(ground) {
|
|
@@ -38,24 +37,26 @@ class Self {
|
|
|
38
37
|
this.g = ground;
|
|
39
38
|
this.key = key;
|
|
40
39
|
this.pk = key.pk;
|
|
41
|
-
this.p = open(ground.memory); // the partition
|
|
42
|
-
this.heirs = new Heirs(this.p);
|
|
40
|
+
this.p = open(ground.memory); // the partition: the ward's own state, and every being's row
|
|
41
|
+
this.heirs = new Heirs(this.p, () => this.#wrote());
|
|
43
42
|
this.door = makeDoor(key, this.heirs, this.doors, () => this.p.public, (n) => ground.random(n)); // pointer one
|
|
44
|
-
this.#boot(this.pk, this); // the ward's ward is itself
|
|
43
|
+
this.#boot(this.pk, () => this); // the ward's ward is itself
|
|
45
44
|
// a restart is silent: every being in the cells is constructed again, unasked.
|
|
46
|
-
// A throw on a boot the owner asked for is that boot's
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
45
|
+
// A throw on a boot the owner asked for is that boot's error, and nothing
|
|
46
|
+
// half-lives. This boot nobody asked for, and there is nobody to hand an
|
|
47
|
+
// error to. Any constructor may throw, a default that is not a value, an
|
|
48
|
+
// author's mistake, a shape she does not recognise after an upgrade, and
|
|
49
|
+
// let through here it is one being taking down the ward and every other
|
|
50
|
+
// being in it, on a restart. She is absent instead: no door, so silence
|
|
51
|
+
// at the door, and her cells sit untouched, waiting for the run that can
|
|
52
|
+
// read them. Her row stays hers: nobody boots another class over it, the
|
|
53
|
+
// owner sees her as absent, and the owner may unboot her.
|
|
54
54
|
for (const [k, c] of Object.entries(this.p.beings)) {
|
|
55
|
-
|
|
55
|
+
const className = c.class;
|
|
56
|
+
if (k === this.pk || !className)
|
|
56
57
|
continue;
|
|
57
58
|
try {
|
|
58
|
-
this.#
|
|
59
|
+
this.#boot(k, (stance) => this.g.instantiate(className, stance));
|
|
59
60
|
}
|
|
60
61
|
catch {
|
|
61
62
|
/* she is not here this run. the ward is, and so is everyone else. */
|
|
@@ -77,25 +78,68 @@ class Self {
|
|
|
77
78
|
// can invite. A stranger cannot be a named asker here, because the door
|
|
78
79
|
// names nobody the ward did not bind.
|
|
79
80
|
async answer(asker, method, args = {}) {
|
|
81
|
+
// Nobody is not an owner. The door names nobody the ward did not bind, so
|
|
82
|
+
// the only way here unnamed is a partition that says the ward is its own
|
|
83
|
+
// public being, which no owner ask will write. Silence, not an error
|
|
84
|
+
// object: an unnamed asker is a stranger, and a stranger hears one thing.
|
|
80
85
|
if (asker.id === undefined)
|
|
81
|
-
return
|
|
86
|
+
return silence;
|
|
82
87
|
return ownerAnswer({
|
|
83
88
|
pk: this.pk,
|
|
84
89
|
doors: this.doors,
|
|
90
|
+
absent: () => this.#absent(),
|
|
85
91
|
publicKey: () => this.p.public,
|
|
86
|
-
instantiate: (
|
|
92
|
+
instantiate: (className, key) => this.#instantiate(className, key),
|
|
87
93
|
unboot: (key) => this.#unboot(key),
|
|
88
|
-
setPublic: (key) =>
|
|
94
|
+
setPublic: (key) => {
|
|
95
|
+
this.p.public = key;
|
|
96
|
+
this.#wrote();
|
|
97
|
+
},
|
|
89
98
|
}, asker, method, args);
|
|
90
99
|
}
|
|
91
100
|
// ---- ward functions. on the object. no stance reaches them.
|
|
92
|
-
|
|
93
|
-
|
|
101
|
+
// The partition was written. The harbor is told, and nothing more: what it
|
|
102
|
+
// does with the word is its own, and the ward never learns.
|
|
103
|
+
#wrote() {
|
|
104
|
+
this.g.wrote?.();
|
|
105
|
+
}
|
|
106
|
+
// The rows no door holds this run: beings whose class threw at birth or
|
|
107
|
+
// is not the harbor's to give. Key to class.
|
|
108
|
+
#absent() {
|
|
109
|
+
const out = {};
|
|
110
|
+
for (const [k, c] of Object.entries(this.p.beings))
|
|
111
|
+
if (k !== this.pk && !this.doors.has(k))
|
|
112
|
+
put(out, k, c.class ?? null);
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
// A new being under a key nobody holds. A key with a row in the partition
|
|
116
|
+
// is booted, whether a door holds it this run or not: an absent being's
|
|
117
|
+
// row is hers, with every relation in it, and no class is booted over it.
|
|
118
|
+
// A throw at birth is that boot's refusal and nothing half-lives; the
|
|
119
|
+
// ward says which, since the owner hears objects.
|
|
120
|
+
#instantiate(className, key) {
|
|
121
|
+
// The three refusals are read before the constructor runs, and the
|
|
122
|
+
// constructor can reach the stance and boot: a class that boots its own
|
|
123
|
+
// key from inside itself would pass all three, since neither its door nor
|
|
124
|
+
// its row exists yet, and the two objects would then share one row. A key
|
|
125
|
+
// being born is a key taken, for as long as the birth lasts.
|
|
126
|
+
if (typeof key !== 'string' || typeof className !== 'string' || !key || this.doors.has(key) || at(this.p.beings, key) || this.#birthing.has(key))
|
|
94
127
|
return null;
|
|
95
|
-
|
|
128
|
+
let door;
|
|
129
|
+
this.#birthing.add(key);
|
|
130
|
+
try {
|
|
131
|
+
door = this.#boot(key, (stance) => this.g.instantiate(className, stance));
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return 'threw';
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
this.#birthing.delete(key);
|
|
138
|
+
}
|
|
96
139
|
if (!door)
|
|
97
140
|
return null;
|
|
98
|
-
door.cells.class = className; // so a restart finds her
|
|
141
|
+
unguarded(door.cells).class = className; // so a restart finds her. the ward's key, written behind her guard
|
|
142
|
+
this.#wrote();
|
|
99
143
|
return door;
|
|
100
144
|
}
|
|
101
145
|
// The inverse of boot, and the only way a being leaves a ward. Her
|
|
@@ -104,16 +148,28 @@ class Self {
|
|
|
104
148
|
// rather than meeting a being who is simply not there any more. What is
|
|
105
149
|
// left after that is her cells and her bind table, and they go with her:
|
|
106
150
|
// a row naming a being no door holds would be read on the next restart
|
|
107
|
-
// and boot her again.
|
|
151
|
+
// and boot her again. An absent being leaves the same way: she has no
|
|
152
|
+
// stance to speak for her, so the ward closes her heirs itself.
|
|
108
153
|
#unboot(key) {
|
|
154
|
+
if (key === this.pk)
|
|
155
|
+
return null;
|
|
109
156
|
const door = this.doors.get(key);
|
|
110
|
-
|
|
157
|
+
const cells = door?.cells ?? at(this.p.beings, key);
|
|
158
|
+
if (!cells)
|
|
111
159
|
return null;
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
160
|
+
const ids = (r) => (r !== null && typeof r === 'object' ? Object.keys(r) : []);
|
|
161
|
+
const occupants = ids(cells.occupants), standings = ids(cells.standings);
|
|
162
|
+
if (door) {
|
|
163
|
+
for (const id of occupants)
|
|
164
|
+
door.stance.occupants.remove(id);
|
|
165
|
+
for (const id of standings)
|
|
166
|
+
door.stance.standings.remove(id);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
const bind = at(this.p.bind, key);
|
|
170
|
+
for (const heir of Object.values(bind?.occupants ?? {}))
|
|
171
|
+
this.heirs.close(heir);
|
|
172
|
+
}
|
|
117
173
|
// A ward whose public being is gone answers arrivals for no heir with
|
|
118
174
|
// silence, as a ward that never had one does.
|
|
119
175
|
if (this.p.public === key)
|
|
@@ -121,38 +177,49 @@ class Self {
|
|
|
121
177
|
drop(this.p.beings, key);
|
|
122
178
|
drop(this.p.bind, key);
|
|
123
179
|
this.doors.delete(key);
|
|
180
|
+
this.#wrote();
|
|
124
181
|
return [...occupants, ...standings];
|
|
125
182
|
}
|
|
126
183
|
// Nothing is written until there is somebody to write it for: a class the
|
|
127
184
|
// harbor does not know makes no being, and a boot that made nobody must
|
|
128
185
|
// leave the partition as it found it.
|
|
129
|
-
|
|
130
|
-
|
|
186
|
+
//
|
|
187
|
+
// `make` is who to build with the stance this raises. Two callers and two
|
|
188
|
+
// makers: the ground's `instantiate` for a being of the ward, and the ward
|
|
189
|
+
// itself, which is the first being in its own map and is already made.
|
|
190
|
+
#boot(key, make) {
|
|
191
|
+
const cells = guardCells(at(this.p.beings, key) ?? emptyCells(), () => this.#wrote());
|
|
131
192
|
const bind = at(this.p.bind, key) ?? emptyBind();
|
|
132
193
|
const stance = buildStance({
|
|
133
194
|
pk: this.pk,
|
|
195
|
+
// This stance, not merely this key: unboot and boot again under the
|
|
196
|
+
// same key makes a new being, and the old stance is not hers.
|
|
197
|
+
live: () => this.doors.get(key)?.stance === stance,
|
|
134
198
|
mintKey: (b) => this.#mintKey(b),
|
|
135
199
|
openHeir: (heir, being, id) => this.heirs.open(heir, being, id),
|
|
136
200
|
closeHeir: (heir) => this.heirs.close(heir),
|
|
137
|
-
send: (b, keys,
|
|
201
|
+
send: (b, keys, method, args, wanted) => this.#send(b, keys, method, args, wanted),
|
|
138
202
|
// A throw at birth is null to her, not a throw in her method: she asked
|
|
139
203
|
// for a being and got none, and her own answer is still hers to give.
|
|
140
|
-
instantiate: (
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
catch {
|
|
145
|
-
return null;
|
|
146
|
-
}
|
|
204
|
+
instantiate: (className, k) => {
|
|
205
|
+
const made = this.#instantiate(className, k);
|
|
206
|
+
return made && made !== 'threw' ? made.key : null;
|
|
147
207
|
},
|
|
208
|
+
// The two halves of a maker's way back, each the ward's own call: the
|
|
209
|
+
// being made mints an occupant for her maker, and a relation that
|
|
210
|
+
// never came about takes her out again.
|
|
211
|
+
relate: async (k, id) => (await this.doors.get(k)?.stance.occupants.invite(id)) ?? null,
|
|
212
|
+
unmake: (k) => void this.#unboot(k),
|
|
213
|
+
wrote: () => this.#wrote(),
|
|
148
214
|
}, key, cells, bind);
|
|
149
|
-
const being =
|
|
150
|
-
if (!being)
|
|
151
|
-
return null;
|
|
215
|
+
const being = make(stance);
|
|
216
|
+
if (!being || typeof being.answer !== 'function')
|
|
217
|
+
return null; // a being is anything with an answer, and nothing without one
|
|
152
218
|
put(this.p.beings, key, cells);
|
|
153
219
|
put(this.p.bind, key, bind);
|
|
154
220
|
const door = { key, cells, bind, stance, being };
|
|
155
221
|
this.doors.set(key, door);
|
|
222
|
+
this.#wrote();
|
|
156
223
|
return door;
|
|
157
224
|
}
|
|
158
225
|
async #mintKey(bind) {
|
|
@@ -164,6 +231,7 @@ class Self {
|
|
|
164
231
|
bind.minted.push(k.pk);
|
|
165
232
|
if (bind.minted.length > MINTED)
|
|
166
233
|
bind.minted.splice(0, bind.minted.length - MINTED);
|
|
234
|
+
this.#wrote();
|
|
167
235
|
return k;
|
|
168
236
|
}
|
|
169
237
|
// One send for every door, the ward's own included. Mine: never leaves.
|
|
@@ -171,7 +239,7 @@ class Self {
|
|
|
171
239
|
// next, seals to the far ward, opens the reply with the ephemeral secret,
|
|
172
240
|
// and rotates to the announced key once the far door has answered under
|
|
173
241
|
// the current one.
|
|
174
|
-
async #send(bind, keys,
|
|
242
|
+
async #send(bind, keys, method, args, wanted) {
|
|
175
243
|
// What she asked for, held to what this ward allows. Asking for nothing is
|
|
176
244
|
// the default, and asking for more than the ceiling is the ceiling: budget
|
|
177
245
|
// is granted by a ward, never minted by a being.
|
|
@@ -179,8 +247,9 @@ class Self {
|
|
|
179
247
|
const by = (await beingKey(unhex(keys.current))).pk;
|
|
180
248
|
// A public being is reached without a heir, and the door keeps nothing for
|
|
181
249
|
// whoever asked: no key it vouched for, so nothing to announce and nothing
|
|
182
|
-
// to rotate to. A standing on her signs with one key for life.
|
|
183
|
-
|
|
250
|
+
// to rotate to. A standing on her signs with one key for life. Every other
|
|
251
|
+
// ask announces its next: there is no send that does not.
|
|
252
|
+
if (keys.next === null && keys.heir !== null)
|
|
184
253
|
keys.next = (await this.#mintKey(bind)).seed;
|
|
185
254
|
const next = keys.next === null ? null : (await beingKey(unhex(keys.next))).pk;
|
|
186
255
|
// Her count for this relation, one higher every call and never reused. The
|
|
@@ -190,10 +259,12 @@ class Self {
|
|
|
190
259
|
// Args that will not go through the seal are args that never left. A
|
|
191
260
|
// being holds three answers and a throw is not one of them, so an ask
|
|
192
261
|
// that cannot be sealed is told what is true of it: no door was reached.
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
|
|
262
|
+
// One object of values, by the rule her cells keep and the far door
|
|
263
|
+
// reads: a Date would arrive as a string, a function would not arrive at
|
|
264
|
+
// all, a list or a string would be refused as a stranger's bytes, and
|
|
265
|
+
// she would never know the far being was asked something other than
|
|
266
|
+
// what she said.
|
|
267
|
+
if (args === null || typeof args !== 'object' || Array.isArray(args) || cellFault(args, 'args') !== null)
|
|
197
268
|
return unreached();
|
|
198
269
|
let sealed;
|
|
199
270
|
try {
|
|
@@ -227,6 +298,7 @@ class Self {
|
|
|
227
298
|
keys.current = keys.next; // the far door holds `next` as announced. move to it.
|
|
228
299
|
keys.next = null;
|
|
229
300
|
}
|
|
301
|
+
this.#wrote(); // the count moved, and the keys may have
|
|
230
302
|
return reply;
|
|
231
303
|
}
|
|
232
304
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quo-systems/quo",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.11",
|
|
4
4
|
"description": "Quo: an object asks another object and gets an answer, without knowing where it is. Being, Ward, Harbor.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"quo",
|
|
@@ -33,7 +33,9 @@
|
|
|
33
33
|
"./conformance": {
|
|
34
34
|
"types": "./dist/conformance/index.d.ts",
|
|
35
35
|
"default": "./dist/conformance/index.js"
|
|
36
|
-
}
|
|
36
|
+
},
|
|
37
|
+
"./vectors/*": "./vectors/*",
|
|
38
|
+
"./package.json": "./package.json"
|
|
37
39
|
},
|
|
38
40
|
"scripts": {
|
|
39
41
|
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
package/src/being/being.ts
CHANGED
|
@@ -18,11 +18,30 @@ export type AskSpec = {
|
|
|
18
18
|
// Names a subclass may not use for an ask, because they are the base's own.
|
|
19
19
|
const RESERVED = new Set(['answer', 'describe', 'stance', 'cells', 'standings', 'occupants', 'occupant', 'invite', 'knock', 'take', 'boot', 'constructor']);
|
|
20
20
|
|
|
21
|
+
// Whether she has a method of that name, written on her own prototype chain
|
|
22
|
+
// below Object's. A name Object lends every object, `hasOwnProperty` or
|
|
23
|
+
// `toString`, is not a method she wrote; and a field she assigns in her own
|
|
24
|
+
// constructor is not there yet when the base checks, so an ask is a method
|
|
25
|
+
// on the prototype and nothing else.
|
|
26
|
+
const method = (self: object, name: string): boolean => {
|
|
27
|
+
for (let p = Object.getPrototypeOf(self); p !== null && p !== Object.prototype; p = Object.getPrototypeOf(p)) {
|
|
28
|
+
if (Object.hasOwn(p, name)) return typeof (p as Record<string, unknown>)[name] === 'function';
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Both statics below are read off the class the object was made from, so a
|
|
34
|
+
// subclass declaring either replaces its parent's rather than adding to it.
|
|
35
|
+
// That is the rule: her blueprint is exactly what the class in front of you
|
|
36
|
+
// declares, in the order she chose, and merging down a chain would hand her
|
|
37
|
+
// asks she may mean to drop and an order she did not write. A subclass that
|
|
38
|
+
// means to extend says so, `static override asks = { ...Parent.asks, mine: {} }`.
|
|
21
39
|
export class Being {
|
|
22
40
|
// Her cells' defaults. Merged in at birth, only where a key is missing, so
|
|
23
41
|
// a restart keeps what she wrote.
|
|
24
42
|
static cells: JsonObject = {};
|
|
25
|
-
// What she can be asked. Declaration order is blueprint order
|
|
43
|
+
// What she can be asked. Declaration order is blueprint order, except a
|
|
44
|
+
// name that reads as an array index, which the language lists first.
|
|
26
45
|
static asks: Record<string, AskSpec> = {};
|
|
27
46
|
|
|
28
47
|
readonly stance: Stance;
|
|
@@ -32,9 +51,11 @@ export class Being {
|
|
|
32
51
|
const C = this.constructor as typeof Being;
|
|
33
52
|
for (const name of Object.keys(C.asks)) {
|
|
34
53
|
if (RESERVED.has(name)) throw new Error(`ask '${name}' is a reserved name`);
|
|
35
|
-
if (
|
|
54
|
+
if (!method(this, name)) throw new Error(`ask '${name}' has no method on the prototype`);
|
|
36
55
|
}
|
|
37
|
-
|
|
56
|
+
// Own keys only: a default named after a member of Object's prototype is
|
|
57
|
+
// still hers, and still missing until she writes it.
|
|
58
|
+
for (const [k, v] of Object.entries(C.cells)) if (!Object.hasOwn(stance.cells, k)) stance.cells[k] = structuredClone(v);
|
|
38
59
|
}
|
|
39
60
|
|
|
40
61
|
get cells(): Cells {
|
|
@@ -46,8 +67,8 @@ export class Being {
|
|
|
46
67
|
get occupants(): Occupants {
|
|
47
68
|
return this.stance.occupants;
|
|
48
69
|
}
|
|
49
|
-
invite(id: string): Promise<Invitation | null> {
|
|
50
|
-
return this.stance.occupants.invite(id);
|
|
70
|
+
invite(id: string, notes?: JsonObject): Promise<Invitation | null> {
|
|
71
|
+
return this.stance.occupants.invite(id, notes);
|
|
51
72
|
}
|
|
52
73
|
knock(invitation: Invitation, method?: string, args?: JsonObject, wanted?: Wanted): Promise<Answer> {
|
|
53
74
|
return this.stance.standings.knock(invitation, method, args, wanted);
|
|
@@ -55,13 +76,14 @@ export class Being {
|
|
|
55
76
|
take(id: string, invitation: Invitation): Promise<string | null> {
|
|
56
77
|
return this.stance.standings.take(id, invitation);
|
|
57
78
|
}
|
|
58
|
-
// A new being of her ward, by class name, under a key she chooses.
|
|
59
|
-
|
|
60
|
-
|
|
79
|
+
// A new being of her ward, by class name, under a key she chooses. With an
|
|
80
|
+
// id, she holds a standing to the being she made, who knows her by her key.
|
|
81
|
+
boot(className: string, key: string, id?: string): Promise<string | null> {
|
|
82
|
+
return this.stance.boot(className, key, id);
|
|
61
83
|
}
|
|
62
84
|
// The occupant record for whoever is at the door. Undefined at a public being.
|
|
63
85
|
occupant(asker: Asker): OccupantRecord | undefined {
|
|
64
|
-
return asker.id
|
|
86
|
+
return asker.id !== undefined && Object.hasOwn(this.cells.occupants, asker.id) ? this.cells.occupants[asker.id] : undefined;
|
|
65
87
|
}
|
|
66
88
|
|
|
67
89
|
// Her blueprint for this asker. Override to shape it by hand.
|
|
@@ -87,8 +109,8 @@ export class Being {
|
|
|
87
109
|
// lookup would also find every name on Object's prototype: `valueOf`
|
|
88
110
|
// would answer with her stance, `toString` with a string, and neither is
|
|
89
111
|
// an ask she wrote. Only her own keys are asks, which is what describe
|
|
90
|
-
//
|
|
91
|
-
const spec =
|
|
112
|
+
// shows. What she shows is what she can be asked.
|
|
113
|
+
const spec = Object.hasOwn(C.asks, method) ? C.asks[method] : undefined;
|
|
92
114
|
if (!spec || (spec.for && !spec.for(this.occupant(asker), asker))) return { error: 'unknown ask' };
|
|
93
115
|
const fn = (this as unknown as Record<string, (args: JsonObject, asker: Asker) => Reply | Promise<Reply>>)[method];
|
|
94
116
|
return fn.call(this, args ?? {}, asker);
|
package/src/being/digest.ts
CHANGED
|
@@ -12,20 +12,35 @@ import type { Json } from './types.ts';
|
|
|
12
12
|
const absent = (v: unknown): boolean => v === undefined || typeof v === 'function' || typeof v === 'symbol';
|
|
13
13
|
|
|
14
14
|
// JCS for I-JSON values: sorted keys, no whitespace, JSON escaping. Numbers
|
|
15
|
-
// are serialized as ES does, which is what RFC 8785 specifies.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
15
|
+
// are serialized as ES does, which is what RFC 8785 specifies. A hole in a
|
|
16
|
+
// list is null, as JSON writes it.
|
|
17
|
+
export const canonical = (v: Json): string => {
|
|
18
|
+
if (Array.isArray(v)) return `[${Array.from(v, (slot) => (absent(slot) ? 'null' : canonical(slot))).join(',')}]`;
|
|
19
|
+
if (v !== null && typeof v === 'object') {
|
|
20
|
+
return `{${Object.keys(v)
|
|
21
|
+
.filter((k) => !absent(v[k]))
|
|
22
|
+
.sort()
|
|
23
|
+
.map((k) => `${JSON.stringify(k)}:${canonical(v[k])}`)
|
|
24
|
+
.join(',')}}`;
|
|
25
|
+
}
|
|
26
|
+
return JSON.stringify(v);
|
|
27
|
+
};
|
|
26
28
|
|
|
29
|
+
// Hex and the guard are spelled here and again in `src/ward/arithmetic.ts`,
|
|
30
|
+
// which is the price of the boundary: nothing under `src/being` imports
|
|
31
|
+
// anything above it, because this is the whole world a being's own code sees
|
|
32
|
+
// and a being reaching the ward is the thing the shape is against.
|
|
27
33
|
const hex = (bytes: ArrayBuffer): string =>
|
|
28
34
|
Array.from(new Uint8Array(bytes), (b) => b.toString(16).padStart(2, '0')).join('');
|
|
29
35
|
|
|
30
|
-
|
|
31
|
-
|
|
36
|
+
// `crypto.subtle` is read at the call and never captured at load. A browser
|
|
37
|
+
// on a plain http:// origin has `crypto` without `subtle`, and a terrain may
|
|
38
|
+
// install one after this module is first imported; either way the failure is
|
|
39
|
+
// one sentence and not a TypeError from inside a digest nobody can read.
|
|
40
|
+
const subtle = (): SubtleCrypto => {
|
|
41
|
+
const s = globalThis.crypto?.subtle;
|
|
42
|
+
if (!s) throw new Error('this terrain has no crypto.subtle: a being needs a secure context to be described');
|
|
43
|
+
return s;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const digest = async (blueprint: Json): Promise<string> => hex(await subtle().digest('SHA-256', new TextEncoder().encode(canonical(blueprint))));
|
package/src/being/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// @quo-systems/quo — the Being side. What a being author imports, if anything.
|
|
3
3
|
export { Being, type AskSpec } from './being.ts';
|
|
4
|
-
export { silence, isSilence, unreached, isUnreached, word, isWord, wordOf, DOOR_WORDS, isDoorWord } from './silence.ts';
|
|
4
|
+
export { silence, isSilence, answered, unreached, isUnreached, word, isWord, wordOf, told, DOOR_WORDS, isDoorWord } from './silence.ts';
|
|
5
5
|
export { digest, canonical } from './digest.ts';
|
|
6
|
-
export { OWNER, PUBLIC, RESERVED_IDS } from './types.ts';
|
|
6
|
+
export { OWNER, PUBLIC, RESERVED_IDS, isBlueprint, invitationArgs } from './types.ts';
|
|
7
7
|
export type * from './types.ts';
|
package/src/being/silence.ts
CHANGED
|
@@ -25,6 +25,20 @@ export const word = <W extends WordName>(name: W): Word<W> => {
|
|
|
25
25
|
export const isWord = (x: unknown): x is Word => x !== null && typeof x === 'object' && typeof (x as Record<symbol, unknown>)[WORD_KEY] === 'string';
|
|
26
26
|
export const wordOf = (x: Word): WordName => x[WORD_KEY];
|
|
27
27
|
|
|
28
|
+
// What came back is an answer and not one of the two things that are not
|
|
29
|
+
// answers. A being who only wants the object writes one test instead of
|
|
30
|
+
// three, and the three shapes stay three shapes: silence is no answer, a word
|
|
31
|
+
// is her ward telling her why there is none.
|
|
32
|
+
export const answered = <T>(x: T): x is Exclude<T, typeof silence | Word> => !isSilence(x) && !isWord(x);
|
|
33
|
+
|
|
34
|
+
// What she was told, with a word given its name and everything else left
|
|
35
|
+
// exactly as it came. One value to compare against, so asking which word came
|
|
36
|
+
// back is one test and not two joined by an and: `told(out) === 'late'` says
|
|
37
|
+
// what `isWord(out) && wordOf(out) === 'late'` says, and says which answer
|
|
38
|
+
// arrived instead when it is wrong, where the pair collapses to false and
|
|
39
|
+
// names nothing. Silence stays the symbol it is, and an object stays itself.
|
|
40
|
+
export const told = (x: unknown): unknown => (isWord(x) ? wordOf(x) : x);
|
|
41
|
+
|
|
28
42
|
// Unreached: no far door was reached. Nothing is known to have been
|
|
29
43
|
// delivered, so asking again is safe. A being cannot produce it: a ward that
|
|
30
44
|
// sees a word come out of a being reads it as her having thrown.
|
package/src/being/types.ts
CHANGED
|
@@ -30,11 +30,36 @@ export const RESERVED_IDS: readonly string[] = [OWNER, PUBLIC];
|
|
|
30
30
|
// and the heir's secret. Without a heir it addresses a ward's public being.
|
|
31
31
|
export type Invitation = { ward: string; heir?: string; secret?: string };
|
|
32
32
|
|
|
33
|
+
// An invitation as one object of values, which is what it already is. A being
|
|
34
|
+
// who hands one on sends it as args, and args are values; this is the one
|
|
35
|
+
// place that is spelled, so nobody spells it with a cast. Absent stays absent:
|
|
36
|
+
// an invitation to a public being carries no heir and no secret, and a key
|
|
37
|
+
// present and undefined is not the same object once JSON has been through it.
|
|
38
|
+
export const invitationArgs = (inv: Invitation): JsonObject => ({
|
|
39
|
+
ward: inv.ward,
|
|
40
|
+
...(inv.heir !== undefined ? { heir: inv.heir } : {}),
|
|
41
|
+
...(inv.secret !== undefined ? { secret: inv.secret } : {}),
|
|
42
|
+
});
|
|
43
|
+
|
|
33
44
|
// A blueprint is an MCP tool list plus notes.
|
|
34
45
|
export type Schema = JsonObject;
|
|
35
46
|
export type Ask = { name: string; description?: string; input: Schema; output?: Schema };
|
|
36
47
|
export type Blueprint = { asks: Ask[]; notes: Json };
|
|
37
48
|
|
|
49
|
+
// Whether what came back from an empty ask is a blueprint. Every describe on
|
|
50
|
+
// the far side of a door is somebody else's code, so nothing may be written
|
|
51
|
+
// into a standing's record as a blueprint without being read as one first: a
|
|
52
|
+
// side walks `asks` by name, and a list of anything else, or no list at all,
|
|
53
|
+
// is a side that breaks on a far ward's answer. The asks are checked to the
|
|
54
|
+
// depth a side reads them, a name and an input each, and no further: what a
|
|
55
|
+
// far being puts beside those is hers.
|
|
56
|
+
export const isBlueprint = (v: unknown): v is Blueprint => {
|
|
57
|
+
if (v === null || typeof v !== 'object' || Array.isArray(v)) return false;
|
|
58
|
+
const { asks, notes } = v as { asks?: unknown; notes?: unknown };
|
|
59
|
+
if (!Array.isArray(asks) || notes === undefined) return false;
|
|
60
|
+
return asks.every((a) => a !== null && typeof a === 'object' && !Array.isArray(a) && typeof (a as Ask).name === 'string' && (a as Ask).input !== null && typeof (a as Ask).input === 'object' && !Array.isArray((a as Ask).input));
|
|
61
|
+
};
|
|
62
|
+
|
|
38
63
|
export type StandingRecord = { id: string; digest: string | null; blueprint: Blueprint | null; seen: string | null };
|
|
39
64
|
export type OccupantRecord = { id: string; notes: JsonObject };
|
|
40
65
|
export type Cells = {
|
|
@@ -49,7 +74,10 @@ export type Silence = typeof silence;
|
|
|
49
74
|
// why. A door says the first five to a key it has bound, and rides them on
|
|
50
75
|
// the wire as `{ quo: word }`; her own ward says the last four to her and
|
|
51
76
|
// they never leave the ward. Each is one frozen object under one symbol key,
|
|
52
|
-
// so no being makes one and none crosses an edge as a value.
|
|
77
|
+
// so no being makes one by accident and none crosses an edge as a value. A
|
|
78
|
+
// being who reaches for the symbol on purpose makes an object her ward reads
|
|
79
|
+
// as her having thrown, which is what a being who returns nonsense gets: the
|
|
80
|
+
// key is a guard against collision, never against her.
|
|
53
81
|
export type DoorWord = 'removed' | 'absent' | 'unannounced' | 'repeated' | 'threw';
|
|
54
82
|
export type WardWord = 'unreached' | 'late' | 'invitation' | 'dropped';
|
|
55
83
|
export type WordName = DoorWord | WardWord;
|
|
@@ -79,7 +107,9 @@ export type Standings = {
|
|
|
79
107
|
} & { readonly [id: string]: Standing | undefined };
|
|
80
108
|
|
|
81
109
|
export type Occupants = {
|
|
82
|
-
|
|
110
|
+
// awaitable: a key is minted. The notes are the terms it is minted under,
|
|
111
|
+
// written on the occupant for her gate to read, and hers alone after that.
|
|
112
|
+
invite(id: string, notes?: JsonObject): Promise<Invitation | null>;
|
|
83
113
|
|
|
84
114
|
remove(id: string): void;
|
|
85
115
|
};
|
|
@@ -90,9 +120,13 @@ export type Stance = {
|
|
|
90
120
|
readonly occupants: Occupants;
|
|
91
121
|
readonly standings: Standings;
|
|
92
122
|
// A new being of her ward, by class name, under a key she chooses. The key
|
|
93
|
-
// back, or null: the key is taken
|
|
94
|
-
// being may make; only the owner reaches into
|
|
95
|
-
|
|
123
|
+
// back, or null: the key is taken, the harbor holds no such class, or the
|
|
124
|
+
// class threw at birth. A being may make; only the owner reaches into
|
|
125
|
+
// another. Naming an id asks for a way back too: the being made holds an
|
|
126
|
+
// occupant for her maker under the maker's own key, and the maker holds
|
|
127
|
+
// the standing under that id. A relation that could not be made is a boot
|
|
128
|
+
// that made nobody, and the being made goes with it.
|
|
129
|
+
boot(className: string, key: string, id?: string): Promise<string | null>;
|
|
96
130
|
};
|
|
97
131
|
|
|
98
132
|
// The raw shape of a being. Anything with these two is a being.
|
|
@@ -11,6 +11,14 @@ const show = (v: unknown): string => {
|
|
|
11
11
|
if (typeof v === 'bigint') return `${v}n`;
|
|
12
12
|
if (v === undefined) return 'undefined';
|
|
13
13
|
if (typeof v === 'function') return `[function ${v.name}]`;
|
|
14
|
+
// A ward's word is a frozen object under one symbol key, and JSON shows it
|
|
15
|
+
// as `{}`. A red estate chapter saying `{} is not {}` names nothing, so a
|
|
16
|
+
// value whose own keys are all symbols is shown by its keys.
|
|
17
|
+
if (v !== null && typeof v === 'object') {
|
|
18
|
+
const keys = Object.keys(v);
|
|
19
|
+
const syms = Object.getOwnPropertySymbols(v).filter((s) => Object.getOwnPropertyDescriptor(v, s)!.enumerable);
|
|
20
|
+
if (keys.length === 0 && syms.length > 0) return `{ ${syms.map((s) => `${s.toString()}: ${show((v as Record<symbol, unknown>)[s])}`).join(', ')} }`;
|
|
21
|
+
}
|
|
14
22
|
try {
|
|
15
23
|
return JSON.stringify(v) ?? Object.prototype.toString.call(v);
|
|
16
24
|
} catch {
|
|
@@ -24,15 +32,40 @@ const fail = (why: string, message?: string): never => {
|
|
|
24
32
|
// Structural, and strict about shape the way `node:assert/strict` is: a value
|
|
25
33
|
// is not equal to one of another kind, and an array is not equal to an object
|
|
26
34
|
// that happens to hold the same keys.
|
|
35
|
+
// The four kinds this compares, and every other object is refused rather than
|
|
36
|
+
// guessed at. A Date, a RegExp, a boxed primitive and a class instance each
|
|
37
|
+
// carry state no key walk can see, so two of them with the same keys are
|
|
38
|
+
// equal here and unequal in node, which is a wrong yes: the one answer a
|
|
39
|
+
// suite must never give. A refusal is a red test that says so.
|
|
40
|
+
const KNOWN: readonly (object | null)[] = [Object.prototype, null, Array.prototype, Uint8Array.prototype];
|
|
41
|
+
|
|
42
|
+
// What node compares: own enumerable keys, strings then enumerable symbols.
|
|
43
|
+
// Not `Reflect.ownKeys`, which also lists what a definition hid.
|
|
44
|
+
const keysOf = (v: object): PropertyKey[] => [...Object.keys(v), ...Object.getOwnPropertySymbols(v).filter((s) => Object.getOwnPropertyDescriptor(v, s)!.enumerable)];
|
|
45
|
+
|
|
27
46
|
export function same(a: unknown, b: unknown): boolean {
|
|
28
47
|
if (Object.is(a, b)) return true;
|
|
29
48
|
if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) return false;
|
|
30
|
-
|
|
49
|
+
const proto = Object.getPrototypeOf(a) as object | null;
|
|
50
|
+
if (proto !== (Object.getPrototypeOf(b) as object | null)) return false;
|
|
51
|
+
if (!KNOWN.includes(proto)) return false; // a Date, a RegExp, a boxed number, a class of hers
|
|
31
52
|
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
32
|
-
if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((x, i) => same(x, b[i]));
|
|
33
53
|
if (a instanceof Uint8Array && b instanceof Uint8Array) return a.length === b.length && a.every((x, i) => x === b[i]);
|
|
34
|
-
|
|
35
|
-
|
|
54
|
+
// An array is its indices and whatever else was hung on it, and a hole is
|
|
55
|
+
// not a present undefined: `[, 1]` and `[undefined, 1]` differ in node and
|
|
56
|
+
// must differ here. `every` skips holes, so the loop is written out.
|
|
57
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
58
|
+
if (a.length !== b.length) return false;
|
|
59
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
60
|
+
if (i in a !== i in b) return false;
|
|
61
|
+
if (i in a && !same(a[i], b[i])) return false;
|
|
62
|
+
}
|
|
63
|
+
const extra = (v: unknown[]) => keysOf(v).filter((k) => typeof k !== 'string' || !/^(?:0|[1-9]\d*)$/.test(k));
|
|
64
|
+
const xa = extra(a), xb = extra(b);
|
|
65
|
+
const at = (v: unknown[], k: PropertyKey) => (v as unknown as Record<PropertyKey, unknown>)[k];
|
|
66
|
+
return xa.length === xb.length && xa.every((k) => xb.includes(k) && same(at(a, k), at(b, k)));
|
|
67
|
+
}
|
|
68
|
+
const ka = keysOf(a), kb = keysOf(b);
|
|
36
69
|
if (ka.length !== kb.length) return false;
|
|
37
70
|
return ka.every((k) => kb.includes(k) && same((a as Record<PropertyKey, unknown>)[k], (b as Record<PropertyKey, unknown>)[k]));
|
|
38
71
|
}
|