@quo-systems/quo 0.2.10 → 0.2.12
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/SPEC.md +141 -22
- package/dist/being/being.js +7 -1
- package/dist/being/digest.js +15 -1
- 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 +2 -0
- 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 +109 -18
- package/dist/conformance/index.d.ts +5 -2
- package/dist/conformance/index.js +164 -6
- package/dist/harbor/core.d.ts +3 -2
- package/dist/harbor/core.js +86 -25
- package/dist/harbor/dial.js +28 -13
- 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 +6 -12
- package/dist/harbor/reach.js +10 -10
- package/dist/ward/allowance.js +7 -2
- package/dist/ward/arithmetic.d.ts +1 -0
- package/dist/ward/arithmetic.js +11 -3
- package/dist/ward/door.d.ts +2 -2
- package/dist/ward/door.js +14 -5
- package/dist/ward/ground.d.ts +4 -1
- package/dist/ward/ground.js +38 -1
- package/dist/ward/heirs.d.ts +1 -2
- package/dist/ward/heirs.js +13 -14
- package/dist/ward/index.d.ts +1 -0
- package/dist/ward/index.js +3 -0
- package/dist/ward/owner.d.ts +4 -4
- package/dist/ward/owner.js +36 -19
- 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 +38 -13
- package/dist/ward/stance.d.ts +3 -2
- package/dist/ward/stance.js +48 -20
- package/dist/ward/ward.d.ts +1 -1
- package/dist/ward/ward.js +40 -19
- package/package.json +1 -1
- package/src/being/being.ts +7 -1
- package/src/being/digest.ts +15 -2
- package/src/being/index.ts +2 -2
- package/src/being/silence.ts +14 -0
- package/src/being/types.ts +29 -1
- package/src/conformance/assert.ts +37 -4
- package/src/conformance/beings.ts +41 -10
- package/src/conformance/estate.ts +106 -17
- package/src/conformance/index.ts +187 -12
- package/src/harbor/core.ts +98 -25
- package/src/harbor/dial.ts +41 -14
- package/src/harbor/index.ts +3 -0
- package/src/harbor/memory.ts +8 -14
- package/src/harbor/reach.ts +10 -11
- package/src/ward/allowance.ts +7 -2
- package/src/ward/arithmetic.ts +14 -5
- package/src/ward/door.ts +16 -7
- package/src/ward/ground.ts +43 -1
- package/src/ward/heirs.ts +13 -14
- package/src/ward/index.ts +3 -0
- package/src/ward/owner.ts +45 -26
- package/src/ward/partition.ts +109 -5
- package/src/ward/seal.ts +39 -12
- package/src/ward/stance.ts +53 -23
- package/src/ward/ward.ts +46 -25
- package/vectors/framing.json +16 -10
- 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';
|
|
@@ -15,8 +15,6 @@ 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
17
|
import { guardCells, unguarded, cellFault } from './cells.js';
|
|
18
|
-
// How many minted pks she keeps behind her. Enough to name the keys in play.
|
|
19
|
-
const MINTED = 8;
|
|
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) {
|
|
@@ -41,7 +40,7 @@ class Self {
|
|
|
41
40
|
this.p = open(ground.memory); // the partition: the ward's own state, and every being's row
|
|
42
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
45
|
// A throw on a boot the owner asked for is that boot's error, and nothing
|
|
47
46
|
// half-lives. This boot nobody asked for, and there is nobody to hand an
|
|
@@ -53,10 +52,11 @@ class Self {
|
|
|
53
52
|
// read them. Her row stays hers: nobody boots another class over it, the
|
|
54
53
|
// owner sees her as absent, and the owner may unboot her.
|
|
55
54
|
for (const [k, c] of Object.entries(this.p.beings)) {
|
|
56
|
-
|
|
55
|
+
const className = c.class;
|
|
56
|
+
if (k === this.pk || !className)
|
|
57
57
|
continue;
|
|
58
58
|
try {
|
|
59
|
-
this.#boot(k,
|
|
59
|
+
this.#boot(k, (stance) => this.g.instantiate(className, stance));
|
|
60
60
|
}
|
|
61
61
|
catch {
|
|
62
62
|
/* she is not here this run. the ward is, and so is everyone else. */
|
|
@@ -78,14 +78,18 @@ class Self {
|
|
|
78
78
|
// can invite. A stranger cannot be a named asker here, because the door
|
|
79
79
|
// names nobody the ward did not bind.
|
|
80
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.
|
|
81
85
|
if (asker.id === undefined)
|
|
82
|
-
return
|
|
86
|
+
return silence;
|
|
83
87
|
return ownerAnswer({
|
|
84
88
|
pk: this.pk,
|
|
85
89
|
doors: this.doors,
|
|
86
90
|
absent: () => this.#absent(),
|
|
87
91
|
publicKey: () => this.p.public,
|
|
88
|
-
instantiate: (
|
|
92
|
+
instantiate: (className, key) => this.#instantiate(className, key),
|
|
89
93
|
unboot: (key) => this.#unboot(key),
|
|
90
94
|
setPublic: (key) => {
|
|
91
95
|
this.p.public = key;
|
|
@@ -113,16 +117,25 @@ class Self {
|
|
|
113
117
|
// row is hers, with every relation in it, and no class is booted over it.
|
|
114
118
|
// A throw at birth is that boot's refusal and nothing half-lives; the
|
|
115
119
|
// ward says which, since the owner hears objects.
|
|
116
|
-
#instantiate(
|
|
117
|
-
|
|
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))
|
|
118
127
|
return null;
|
|
119
128
|
let door;
|
|
129
|
+
this.#birthing.add(key);
|
|
120
130
|
try {
|
|
121
|
-
door = this.#boot(key, className);
|
|
131
|
+
door = this.#boot(key, (stance) => this.g.instantiate(className, stance));
|
|
122
132
|
}
|
|
123
133
|
catch {
|
|
124
134
|
return 'threw';
|
|
125
135
|
}
|
|
136
|
+
finally {
|
|
137
|
+
this.#birthing.delete(key);
|
|
138
|
+
}
|
|
126
139
|
if (!door)
|
|
127
140
|
return null;
|
|
128
141
|
unguarded(door.cells).class = className; // so a restart finds her. the ward's key, written behind her guard
|
|
@@ -170,19 +183,26 @@ class Self {
|
|
|
170
183
|
// Nothing is written until there is somebody to write it for: a class the
|
|
171
184
|
// harbor does not know makes no being, and a boot that made nobody must
|
|
172
185
|
// leave the partition as it found it.
|
|
173
|
-
|
|
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) {
|
|
174
191
|
const cells = guardCells(at(this.p.beings, key) ?? emptyCells(), () => this.#wrote());
|
|
175
192
|
const bind = at(this.p.bind, key) ?? emptyBind();
|
|
176
193
|
const stance = buildStance({
|
|
177
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,
|
|
178
198
|
mintKey: (b) => this.#mintKey(b),
|
|
179
199
|
openHeir: (heir, being, id) => this.heirs.open(heir, being, id),
|
|
180
200
|
closeHeir: (heir) => this.heirs.close(heir),
|
|
181
|
-
send: (b, keys,
|
|
201
|
+
send: (b, keys, method, args, wanted) => this.#send(b, keys, method, args, wanted),
|
|
182
202
|
// A throw at birth is null to her, not a throw in her method: she asked
|
|
183
203
|
// for a being and got none, and her own answer is still hers to give.
|
|
184
|
-
instantiate: (
|
|
185
|
-
const made = this.#instantiate(
|
|
204
|
+
instantiate: (className, k) => {
|
|
205
|
+
const made = this.#instantiate(className, k);
|
|
186
206
|
return made && made !== 'threw' ? made.key : null;
|
|
187
207
|
},
|
|
188
208
|
// The two halves of a maker's way back, each the ward's own call: the
|
|
@@ -192,7 +212,7 @@ class Self {
|
|
|
192
212
|
unmake: (k) => void this.#unboot(k),
|
|
193
213
|
wrote: () => this.#wrote(),
|
|
194
214
|
}, key, cells, bind);
|
|
195
|
-
const being =
|
|
215
|
+
const being = make(stance);
|
|
196
216
|
if (!being || typeof being.answer !== 'function')
|
|
197
217
|
return null; // a being is anything with an answer, and nothing without one
|
|
198
218
|
put(this.p.beings, key, cells);
|
|
@@ -219,7 +239,7 @@ class Self {
|
|
|
219
239
|
// next, seals to the far ward, opens the reply with the ephemeral secret,
|
|
220
240
|
// and rotates to the announced key once the far door has answered under
|
|
221
241
|
// the current one.
|
|
222
|
-
async #send(bind, keys,
|
|
242
|
+
async #send(bind, keys, method, args, wanted) {
|
|
223
243
|
// What she asked for, held to what this ward allows. Asking for nothing is
|
|
224
244
|
// the default, and asking for more than the ceiling is the ceiling: budget
|
|
225
245
|
// is granted by a ward, never minted by a being.
|
|
@@ -227,8 +247,9 @@ class Self {
|
|
|
227
247
|
const by = (await beingKey(unhex(keys.current))).pk;
|
|
228
248
|
// A public being is reached without a heir, and the door keeps nothing for
|
|
229
249
|
// whoever asked: no key it vouched for, so nothing to announce and nothing
|
|
230
|
-
// to rotate to. A standing on her signs with one key for life.
|
|
231
|
-
|
|
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)
|
|
232
253
|
keys.next = (await this.#mintKey(bind)).seed;
|
|
233
254
|
const next = keys.next === null ? null : (await beingKey(unhex(keys.next))).pk;
|
|
234
255
|
// Her count for this relation, one higher every call and never reused. The
|
package/package.json
CHANGED
package/src/being/being.ts
CHANGED
|
@@ -30,6 +30,12 @@ const method = (self: object, name: string): boolean => {
|
|
|
30
30
|
return false;
|
|
31
31
|
};
|
|
32
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: {} }`.
|
|
33
39
|
export class Being {
|
|
34
40
|
// Her cells' defaults. Merged in at birth, only where a key is missing, so
|
|
35
41
|
// a restart keeps what she wrote.
|
|
@@ -104,7 +110,7 @@ export class Being {
|
|
|
104
110
|
// would answer with her stance, `toString` with a string, and neither is
|
|
105
111
|
// an ask she wrote. Only her own keys are asks, which is what describe
|
|
106
112
|
// shows. What she shows is what she can be asked.
|
|
107
|
-
const spec =
|
|
113
|
+
const spec = Object.hasOwn(C.asks, method) ? C.asks[method] : undefined;
|
|
108
114
|
if (!spec || (spec.for && !spec.for(this.occupant(asker), asker))) return { error: 'unknown ask' };
|
|
109
115
|
const fn = (this as unknown as Record<string, (args: JsonObject, asker: Asker) => Reply | Promise<Reply>>)[method];
|
|
110
116
|
return fn.call(this, args ?? {}, asker);
|
package/src/being/digest.ts
CHANGED
|
@@ -26,8 +26,21 @@ export const canonical = (v: Json): string => {
|
|
|
26
26
|
return JSON.stringify(v);
|
|
27
27
|
};
|
|
28
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.
|
|
29
33
|
const hex = (bytes: ArrayBuffer): string =>
|
|
30
34
|
Array.from(new Uint8Array(bytes), (b) => b.toString(16).padStart(2, '0')).join('');
|
|
31
35
|
|
|
32
|
-
|
|
33
|
-
|
|
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;
|
|
@@ -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
|
}
|
|
@@ -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' } } };
|