@quo-systems/quo 0.2.10 → 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.
Files changed (72) hide show
  1. package/SPEC.md +141 -22
  2. package/dist/being/being.js +7 -1
  3. package/dist/being/digest.js +15 -1
  4. package/dist/being/index.d.ts +2 -2
  5. package/dist/being/index.js +2 -2
  6. package/dist/being/silence.d.ts +2 -0
  7. package/dist/being/silence.js +12 -0
  8. package/dist/being/types.d.ts +2 -0
  9. package/dist/being/types.js +25 -0
  10. package/dist/conformance/assert.js +40 -6
  11. package/dist/conformance/beings.d.ts +48 -5
  12. package/dist/conformance/beings.js +39 -8
  13. package/dist/conformance/estate.js +109 -18
  14. package/dist/conformance/index.d.ts +5 -2
  15. package/dist/conformance/index.js +164 -6
  16. package/dist/harbor/core.d.ts +3 -2
  17. package/dist/harbor/core.js +86 -25
  18. package/dist/harbor/dial.js +28 -13
  19. package/dist/harbor/index.d.ts +1 -0
  20. package/dist/harbor/index.js +3 -0
  21. package/dist/harbor/memory.d.ts +3 -3
  22. package/dist/harbor/memory.js +6 -12
  23. package/dist/harbor/reach.js +10 -10
  24. package/dist/ward/allowance.js +7 -2
  25. package/dist/ward/arithmetic.d.ts +1 -0
  26. package/dist/ward/arithmetic.js +11 -3
  27. package/dist/ward/door.d.ts +2 -2
  28. package/dist/ward/door.js +14 -5
  29. package/dist/ward/ground.d.ts +4 -1
  30. package/dist/ward/ground.js +38 -1
  31. package/dist/ward/heirs.d.ts +1 -2
  32. package/dist/ward/heirs.js +13 -14
  33. package/dist/ward/index.d.ts +1 -0
  34. package/dist/ward/index.js +3 -0
  35. package/dist/ward/owner.d.ts +4 -4
  36. package/dist/ward/owner.js +36 -19
  37. package/dist/ward/partition.d.ts +3 -0
  38. package/dist/ward/partition.js +109 -4
  39. package/dist/ward/seal.d.ts +1 -0
  40. package/dist/ward/seal.js +38 -13
  41. package/dist/ward/stance.d.ts +3 -2
  42. package/dist/ward/stance.js +48 -20
  43. package/dist/ward/ward.d.ts +1 -1
  44. package/dist/ward/ward.js +40 -19
  45. package/package.json +1 -1
  46. package/src/being/being.ts +7 -1
  47. package/src/being/digest.ts +15 -2
  48. package/src/being/index.ts +2 -2
  49. package/src/being/silence.ts +14 -0
  50. package/src/being/types.ts +29 -1
  51. package/src/conformance/assert.ts +37 -4
  52. package/src/conformance/beings.ts +41 -10
  53. package/src/conformance/estate.ts +106 -17
  54. package/src/conformance/index.ts +187 -12
  55. package/src/harbor/core.ts +98 -25
  56. package/src/harbor/dial.ts +41 -14
  57. package/src/harbor/index.ts +3 -0
  58. package/src/harbor/memory.ts +8 -14
  59. package/src/harbor/reach.ts +10 -11
  60. package/src/ward/allowance.ts +7 -2
  61. package/src/ward/arithmetic.ts +14 -5
  62. package/src/ward/door.ts +16 -7
  63. package/src/ward/ground.ts +43 -1
  64. package/src/ward/heirs.ts +13 -14
  65. package/src/ward/index.ts +3 -0
  66. package/src/ward/owner.ts +45 -26
  67. package/src/ward/partition.ts +109 -5
  68. package/src/ward/seal.ts +39 -12
  69. package/src/ward/stance.ts +53 -23
  70. package/src/ward/ward.ts +46 -25
  71. package/vectors/framing.json +16 -10
  72. 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
- if (k === this.pk || !c.class)
55
+ const className = c.class;
56
+ if (k === this.pk || !className)
57
57
  continue;
58
58
  try {
59
- this.#boot(k, c.class);
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 { error: 'unknown ask' };
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: (key, className) => this.#instantiate(key, className),
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(key, className) {
117
- if (typeof key !== 'string' || typeof className !== 'string' || !key || this.doors.has(key) || at(this.p.beings, key))
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
- #boot(key, classNameOrSelf) {
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, announce, method, args, wanted) => this.#send(b, keys, announce, method, args, wanted),
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: (k, className) => {
185
- const made = this.#instantiate(k, className);
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 = classNameOrSelf === this ? this : this.g.instantiate(classNameOrSelf, stance);
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, announce, method, args, wanted) {
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
- if (announce && keys.next === null && keys.heir !== null)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quo-systems/quo",
3
- "version": "0.2.10",
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",
@@ -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 = typeof method === 'string' && Object.hasOwn(C.asks, method) ? C.asks[method] : undefined;
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);
@@ -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
- export const digest = async (blueprint: Json): Promise<string> =>
33
- hex(await globalThis.crypto.subtle.digest('SHA-256', new TextEncoder().encode(canonical(blueprint))));
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))));
@@ -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';
@@ -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.
@@ -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
- if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) return false;
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
- if (a instanceof Map || b instanceof Map || a instanceof Set || b instanceof Set) return false; // the suite has none, and a wrong yes is worse than a refusal
35
- const ka = Reflect.ownKeys(a), kb = Reflect.ownKeys(b);
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 with no base at all.
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 { isSilence, isUnreached, isWord } from '../being/silence.ts';
6
- import type { Asker, Blueprint, Invitation, JsonObject, OccupantRecord, Reply, Stance } from '../being/types.ts';
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
- return { asks: this.cells.asks as never, notes: { model: 'LX-2' } };
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 (isSilence(bp) || isWord(bp)) return false;
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 (!isSilence(back) && !isWord(back)) await this.take(`vip-${asker.id}`, invitation as Invitation);
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 (isSilence(out) || isWord(out)) return { error: 'printer refused' };
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
- const mine = (await this.invite('shop-back'))!;
90
- const out = await this.knock(invitation, 'hello', { invitation: mine as never });
91
- if (isSilence(out) || isWord(out)) return out; // nothing was born
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' } } };