@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.
Files changed (78) hide show
  1. package/README.md +19 -10
  2. package/SPEC.md +301 -110
  3. package/dist/being/being.d.ts +2 -2
  4. package/dist/being/being.js +34 -12
  5. package/dist/being/digest.js +26 -8
  6. package/dist/being/index.d.ts +2 -2
  7. package/dist/being/index.js +2 -2
  8. package/dist/being/silence.d.ts +2 -0
  9. package/dist/being/silence.js +12 -0
  10. package/dist/being/types.d.ts +4 -2
  11. package/dist/being/types.js +25 -0
  12. package/dist/conformance/assert.js +40 -6
  13. package/dist/conformance/beings.d.ts +48 -5
  14. package/dist/conformance/beings.js +39 -8
  15. package/dist/conformance/estate.js +110 -21
  16. package/dist/conformance/index.d.ts +5 -2
  17. package/dist/conformance/index.js +165 -7
  18. package/dist/harbor/core.d.ts +5 -2
  19. package/dist/harbor/core.js +195 -45
  20. package/dist/harbor/dial.js +32 -15
  21. package/dist/harbor/index.d.ts +1 -0
  22. package/dist/harbor/index.js +3 -0
  23. package/dist/harbor/memory.d.ts +3 -3
  24. package/dist/harbor/memory.js +7 -12
  25. package/dist/harbor/reach.js +42 -17
  26. package/dist/ward/allowance.js +15 -4
  27. package/dist/ward/arithmetic.d.ts +1 -0
  28. package/dist/ward/arithmetic.js +22 -6
  29. package/dist/ward/cells.d.ts +3 -1
  30. package/dist/ward/cells.js +79 -21
  31. package/dist/ward/door.d.ts +3 -2
  32. package/dist/ward/door.js +38 -10
  33. package/dist/ward/ground.d.ts +5 -1
  34. package/dist/ward/ground.js +38 -1
  35. package/dist/ward/heirs.d.ts +2 -3
  36. package/dist/ward/heirs.js +19 -13
  37. package/dist/ward/index.d.ts +1 -0
  38. package/dist/ward/index.js +3 -0
  39. package/dist/ward/owner.d.ts +6 -27
  40. package/dist/ward/owner.js +59 -33
  41. package/dist/ward/partition.d.ts +3 -0
  42. package/dist/ward/partition.js +109 -4
  43. package/dist/ward/seal.d.ts +1 -0
  44. package/dist/ward/seal.js +41 -13
  45. package/dist/ward/stance.d.ts +7 -3
  46. package/dist/ward/stance.js +156 -66
  47. package/dist/ward/ward.d.ts +10 -0
  48. package/dist/ward/ward.js +123 -51
  49. package/package.json +4 -2
  50. package/src/being/being.ts +33 -11
  51. package/src/being/digest.ts +28 -13
  52. package/src/being/index.ts +2 -2
  53. package/src/being/silence.ts +14 -0
  54. package/src/being/types.ts +39 -5
  55. package/src/conformance/assert.ts +37 -4
  56. package/src/conformance/beings.ts +41 -10
  57. package/src/conformance/estate.ts +107 -20
  58. package/src/conformance/index.ts +188 -13
  59. package/src/harbor/core.ts +203 -46
  60. package/src/harbor/dial.ts +46 -17
  61. package/src/harbor/index.ts +3 -0
  62. package/src/harbor/memory.ts +8 -13
  63. package/src/harbor/reach.ts +47 -21
  64. package/src/ward/allowance.ts +15 -4
  65. package/src/ward/arithmetic.ts +25 -8
  66. package/src/ward/cells.ts +76 -25
  67. package/src/ward/door.ts +38 -12
  68. package/src/ward/ground.ts +52 -3
  69. package/src/ward/heirs.ts +19 -13
  70. package/src/ward/index.ts +3 -0
  71. package/src/ward/owner.ts +65 -46
  72. package/src/ward/partition.ts +109 -5
  73. package/src/ward/seal.ts +41 -12
  74. package/src/ward/stance.ts +163 -64
  75. package/src/ward/ward.ts +124 -52
  76. package/vectors/arithmetic.json +7 -0
  77. package/vectors/framing.json +30 -15
  78. 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 IS the ward's cells
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 answer: silence,
47
- // and nothing half-lives. This boot nobody asked for, and there is nobody
48
- // to hand a silence to. Any constructor may throw a default that is not
49
- // a value, an author's mistake, a shape she no longer recognises after an
50
- // upgrade — and let through here it is one being taking down the ward and
51
- // every other being in it, on a restart. She is absent instead: no door,
52
- // so silence at the door, and her cells sit untouched, waiting for the
53
- // run that can read them.
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
- if (k === this.pk || !c.class)
55
+ const className = c.class;
56
+ if (k === this.pk || !className)
56
57
  continue;
57
58
  try {
58
- this.#instantiate(k, c.class);
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 { error: 'unknown ask' };
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: (key, className) => this.#instantiate(key, className),
92
+ instantiate: (className, key) => this.#instantiate(className, key),
87
93
  unboot: (key) => this.#unboot(key),
88
- setPublic: (key) => void (this.p.public = 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
- #instantiate(key, className) {
93
- if (!key || this.doors.has(key))
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
- const door = this.#boot(key, className);
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
- if (!door || key === this.pk)
157
+ const cells = door?.cells ?? at(this.p.beings, key);
158
+ if (!cells)
111
159
  return null;
112
- const occupants = Object.keys(door.cells.occupants), standings = Object.keys(door.cells.standings);
113
- for (const id of occupants)
114
- door.stance.occupants.remove(id);
115
- for (const id of standings)
116
- door.stance.standings.remove(id);
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
- #boot(key, classNameOrSelf) {
130
- const cells = guardCells(at(this.p.beings, key) ?? emptyCells());
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, 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),
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: (k, className) => {
141
- try {
142
- return this.#instantiate(k, className)?.key ?? null;
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 = classNameOrSelf === this ? this : this.g.instantiate(classNameOrSelf, stance);
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, announce, method, args, wanted) {
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
- 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)
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
- // Values only, by the same rule her cells keep: a Date would arrive as a
194
- // string, a function would not arrive at all, and she would never know
195
- // the far being was asked something other than what she said.
196
- if (cellFault(args, 'args') !== null)
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.9",
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",
@@ -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 (typeof (this as unknown as Record<string, unknown>)[name] !== 'function') throw new Error(`ask '${name}' has no method`);
54
+ if (!method(this, name)) throw new Error(`ask '${name}' has no method on the prototype`);
36
55
  }
37
- for (const [k, v] of Object.entries(C.cells)) if (!(k in stance.cells)) stance.cells[k] = structuredClone(v);
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
- boot(className: string, key: string): string | null {
60
- return this.stance.boot(className, key);
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 === undefined ? undefined : this.cells.occupants[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
- // has always shown. What she shows is what she can be asked.
91
- const spec = typeof method === 'string' && Object.hasOwn(C.asks, method) ? C.asks[method] : undefined;
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);
@@ -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
- export const canonical = (v: Json): string =>
17
- Array.isArray(v)
18
- ? `[${v.map((slot) => (absent(slot) ? 'null' : canonical(slot))).join(',')}]`
19
- : v !== null && typeof v === 'object'
20
- ? `{${Object.keys(v)
21
- .filter((k) => !absent(v[k]))
22
- .sort()
23
- .map((k) => `${JSON.stringify(k)}:${canonical(v[k])}`)
24
- .join(',')}}`
25
- : JSON.stringify(v);
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
- export const digest = async (blueprint: Json): Promise<string> =>
31
- 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;
@@ -79,7 +107,9 @@ export type Standings = {
79
107
  } & { readonly [id: string]: Standing | undefined };
80
108
 
81
109
  export type Occupants = {
82
- invite(id: string): Promise<Invitation | null>; // awaitable: a key is minted
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 or the harbor holds no such class. A
94
- // being may make; only the owner reaches into another.
95
- boot(className: string, key: string): string | null;
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
- 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
  }