@quo-systems/quo 0.2.13 → 0.2.15

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 (55) hide show
  1. package/README.md +10 -4
  2. package/SPEC.md +354 -536
  3. package/dist/being/being.d.ts +1 -0
  4. package/dist/being/being.js +8 -3
  5. package/dist/being/index.d.ts +1 -1
  6. package/dist/being/index.js +1 -1
  7. package/dist/being/types.d.ts +2 -0
  8. package/dist/being/types.js +17 -0
  9. package/dist/conformance/beings.d.ts +41 -0
  10. package/dist/conformance/beings.js +28 -2
  11. package/dist/conformance/index.d.ts +10 -1
  12. package/dist/conformance/index.js +162 -6
  13. package/dist/harbor/core.d.ts +4 -2
  14. package/dist/harbor/core.js +25 -4
  15. package/dist/harbor/index.d.ts +1 -1
  16. package/dist/harbor/memory.d.ts +3 -3
  17. package/dist/harbor/memory.js +6 -3
  18. package/dist/harbor/reach.js +1 -1
  19. package/dist/ward/cells.d.ts +2 -0
  20. package/dist/ward/cells.js +60 -11
  21. package/dist/ward/door.d.ts +1 -0
  22. package/dist/ward/door.js +29 -10
  23. package/dist/ward/ground.d.ts +3 -1
  24. package/dist/ward/ground.js +1 -1
  25. package/dist/ward/heirs.js +8 -1
  26. package/dist/ward/index.d.ts +2 -2
  27. package/dist/ward/index.js +3 -3
  28. package/dist/ward/owner.js +49 -8
  29. package/dist/ward/seal.d.ts +1 -0
  30. package/dist/ward/seal.js +10 -2
  31. package/dist/ward/stance.d.ts +1 -0
  32. package/dist/ward/stance.js +62 -4
  33. package/dist/ward/ward.js +5 -0
  34. package/package.json +5 -3
  35. package/quo-kit.md +523 -0
  36. package/src/being/being.ts +8 -3
  37. package/src/being/index.ts +1 -1
  38. package/src/being/types.ts +34 -0
  39. package/src/conformance/beings.ts +25 -2
  40. package/src/conformance/estate.ts +9 -9
  41. package/src/conformance/index.ts +204 -7
  42. package/src/conformance/reach.ts +1 -1
  43. package/src/harbor/core.ts +26 -5
  44. package/src/harbor/index.ts +1 -1
  45. package/src/harbor/memory.ts +7 -4
  46. package/src/harbor/reach.ts +1 -1
  47. package/src/ward/cells.ts +59 -10
  48. package/src/ward/door.ts +27 -9
  49. package/src/ward/ground.ts +39 -11
  50. package/src/ward/heirs.ts +7 -1
  51. package/src/ward/index.ts +4 -4
  52. package/src/ward/owner.ts +45 -10
  53. package/src/ward/seal.ts +11 -2
  54. package/src/ward/stance.ts +60 -4
  55. package/src/ward/ward.ts +6 -1
package/quo-kit.md ADDED
@@ -0,0 +1,523 @@
1
+ # quo-kit
2
+
3
+ This is the kit: one TypeScript implementation of Quo, run directly on Node
4
+ with no dependencies, published as `@quo-systems/quo`. It assumes
5
+ `papers/SPEC.md`, which is the protocol and assumes nothing, and it adds
6
+ nothing to it. Everything here is a choice this kit made and another kit may
7
+ refuse: a base class, a spelling for silence, a file layout, a store, a
8
+ bundle, a gate. Where this paper and the protocol disagree the protocol wins,
9
+ because a sentence that a kit in another language needs to interoperate is
10
+ not this paper's to hold.
11
+
12
+ It carries no version and keeps no history, and it is rewritten in place to
13
+ say what the tree says.
14
+
15
+ ## The tree
16
+
17
+ The repository is one TypeScript implementation of the protocol, run
18
+ directly on Node's type stripping, no dependencies. Node 22.18
19
+ or later. The package is `@quo-systems/quo`, published from 0.1.0, the
20
+ first version under this name, and it starts over: nothing before it under
21
+ any other name is this protocol. Before 1.0.0 every version may change the
22
+ words, and nothing is kept for a holder of an earlier one.
23
+
24
+ The tree runs as TypeScript and the package ships as JavaScript. Node
25
+ strips types nowhere under `node_modules`, so a consumer cannot load the
26
+ source. `npm run build` emits `dist/` from `src`
27
+ alone, one JavaScript file and one declaration file per source file with
28
+ every relative import rewritten, and the exports map points there, types
29
+ beside default. The
30
+ build runs before every gate and every publish and is never tracked;
31
+ nothing in this repository imports it except through the package name,
32
+ which is how a consumer does. `test/package.test.ts` is the one
33
+ suite that meets the package as a stranger: it packs the tarball,
34
+ installs it into an empty folder, and imports every entry point under
35
+ plain Node. `npm pack --dry-run` shows what ships: `dist/`, the four
36
+ source folders, the vectors, the papers, the README, the licence and
37
+ the notice, and nothing else. Two papers ship, `SPEC.md` and `quo-kit.md`,
38
+ both copied in by the build: the protocol is the truth the source and the
39
+ vectors are read against, and a kit in another language is written against it
40
+ and nothing else, while this paper says which of what it sees here was a
41
+ choice. Publishing runs both
42
+ gates first, `npm run check` and `npm run check:terrain`, and refuses on a
43
+ failure.
44
+
45
+ ```
46
+ src/being/ the Being side. types, the base class, silence, the digest.
47
+ src/ward/ the ward. the Ground contract, door, seal, arithmetic, heirs, stance, owner, partition, cells, allowance.
48
+ src/harbor/ the memory harbor, the store, the reach, the dialer and the harbor core.
49
+ src/conformance/ the behaviours any ward must show, and the fixed beings they are shown with.
50
+ test/ the suites.
51
+ vectors/ fixed inputs and outputs: the arithmetic, Quo's own framing, the frames on the wire.
52
+ ```
53
+
54
+ Entry points: `@quo-systems/quo` is the Being side, `@quo-systems/quo/ward`
55
+ is `Ward`, `@quo-systems/quo/harbor` is `MemoryHarbor`, `Harbor`, the
56
+ store, the reach and the dialer, and `@quo-systems/quo/conformance` is
57
+ `conform` with the store and reach suites beside it.
58
+
59
+ ## The terrain
60
+
61
+ Under `src/being/`, `src/ward/`, `src/harbor/` and `src/conformance/` no
62
+ runtime is named. The whole
63
+ platform surface is the language plus ten globals. Six are the ward's own:
64
+ `crypto`, `TextEncoder`, `TextDecoder`, `structuredClone`, `setTimeout` and
65
+ `atob`. Four more came with the harbor, and are the whole of what a reach
66
+ costs: `fetch` and `WebSocket`, the two kinds of reach; `DataView`, the frame
67
+ id on a socket; and `clearTimeout`, the dialer's reconnect called off. That
68
+ list is a promise, not an accident: `test/terrain.test.ts` fails the build
69
+ both when a file names a platform and when it reaches for a global outside
70
+ the list, and when the list names one the tree has stopped using.
71
+
72
+ The package exports the emitted `dist/`, JavaScript with a declaration
73
+ beside it, on every specifier, so plain Node imports it; the tree runs the
74
+ source directly, and every other terrain reaches a ward through a
75
+ bundler. `test/bundle.test.ts` bundles the three words and runs them, and
76
+ asserts the artefact carries nothing a terrain cannot provide.
77
+
78
+ The five algorithms are read out of WebCrypto, which is why the ward names no
79
+ package, on any terrain that carries them. `crypto.subtle` is read at every
80
+ use and never captured at load, so a page without one, a plain http:// origin
81
+ or a sandboxed frame, fails at the first ask with one sentence rather than
82
+ deep inside a key import. The floor is probed in `test/floor.test.ts`, the
83
+ arithmetic is `src/ward/arithmetic.ts` and the seal is `src/ward/seal.ts`.
84
+
85
+ ## The being side
86
+
87
+ Silence is the symbol `quo.silence` and a word is a frozen object under the
88
+ symbol key `quo.word` holding its name, both from `src/being/silence.ts`;
89
+ `isWord` and `wordOf` read them, and `isUnreached` is the one word a being
90
+ asks about most. `told` names a word and leaves everything else exactly as it
91
+ came, so asking which word arrived is one comparison rather than two joined by
92
+ an and: `told(out) === 'late'`. A kit in another language spells it however
93
+ that language spells one value standing for either, and owes nothing here.
94
+
95
+ The digest is `src/being/digest.ts`.
96
+
97
+ ### The base class
98
+
99
+ The kit offers a base class, `Being` in `src/being/being.ts`, which is a
100
+ convenience and not Quo:
101
+
102
+ - `static cells` are her defaults, merged at birth only where a key is
103
+ missing, so a restart keeps what she wrote.
104
+ - `static asks` declares what she can be asked, name to
105
+ `{ description?, input?, output?, for? }`, in blueprint order.
106
+ `for(occupant, asker)` decides whether this asker sees the ask, and so
107
+ whether this asker may call it: what she shows is what she can be asked,
108
+ one gate for describe and for dispatch.
109
+ - Both statics are read off the class the object was made from, and a
110
+ subclass that declares either **replaces** its parent's rather than adding
111
+ to it. That is the rule and not an oversight: a being's blueprint is
112
+ exactly what the class in front of you declares, in the order she chose,
113
+ and a merge would hand her asks she may mean to drop and an order she did
114
+ not write. A subclass that means to extend says so, in one spelling,
115
+ `static override asks = { ...Parent.asks, mine: {} }`, and the same for
116
+ cells. A parent that means to be subclassed at all annotates rather than
117
+ infers, `static override cells: JsonObject = { ... }`, since this language
118
+ holds a subclass's static side to its parent's and would otherwise refuse a
119
+ subclass declaring fewer keys than the parent happened to write.
120
+ - `answer` is written for her. The empty ask is `describe(asker)`, which she
121
+ may override by hand. A named ask calls the method of that name with
122
+ `(args, asker)`. Anything not declared, hidden from this asker, or
123
+ inherited from Object's prototype is `{ error: 'unknown ask' }` and the
124
+ method is never entered.
125
+ - A declared ask with no method, or one named after the base's own members,
126
+ fails at birth, loudly, so a boot fails and nothing half-lives. The
127
+ method is one on her prototype chain below Object's: a class field
128
+ holding a function is not there yet when the base checks, and the
129
+ refusal says so.
130
+ - `occupant(asker)` is the occupant record for whoever is at the door, and
131
+ undefined at a public being. `invite`, `knock`, `take`, `boot`, `cells`,
132
+ `standings` and `occupants` reach the stance and nothing else.
133
+
134
+ ### Examples
135
+
136
+ The smallest being. Answers whoever her ward names, describes one ask.
137
+
138
+ ```js
139
+ class Echo {
140
+ constructor(stance) {
141
+ this.s = stance;
142
+ }
143
+ answer(asker, method, args) {
144
+ if (method === undefined) return { asks: [{ name: 'echo', input: {} }], notes: {} };
145
+ return { from: asker.id ?? null, ...args };
146
+ }
147
+ }
148
+ ```
149
+
150
+ A shop. Invites in her own time, hands the invitation out by any channel,
151
+ and takes a guest back only if the guest offers a way.
152
+
153
+ ```js
154
+ class Shop {
155
+ constructor(stance) {
156
+ this.s = stance;
157
+ }
158
+ async invite(name) {
159
+ const id = `g-${name}`;
160
+ const inv = await this.s.occupants.invite(id);
161
+ this.s.cells.occupants[id].notes.expireAt = 2028; // hers, not Quo's
162
+ return inv; // goes by mail
163
+ }
164
+ async answer(asker, method, args) {
165
+ if (method === undefined) return { asks: [{ name: 'hello', input: {} }], notes: {} };
166
+ if (asker.id === undefined) return { welcome: false }; // a stranger at the public door has no record
167
+ const rec = this.s.cells.occupants[asker.id];
168
+ if (method === 'hello' && args.invitation) {
169
+ const back = await this.s.standings.knock(args.invitation, 'hi');
170
+ if (back !== silence && !isUnreached(back))
171
+ await this.s.standings.take(`back-${asker.id}`, args.invitation);
172
+ }
173
+ return { welcome: rec.notes.expireAt > 2026 };
174
+ }
175
+ }
176
+ ```
177
+
178
+ A guest. Consumes an invitation, and only then decides to keep the shop.
179
+
180
+ ```js
181
+ class Guest {
182
+ constructor(stance) {
183
+ this.s = stance;
184
+ }
185
+ async join(invitation) {
186
+ const mine = await this.s.occupants.invite('shop'); // so the shop can reach me
187
+ const out = await this.s.standings.knock(invitation, 'hello', { invitation: mine });
188
+ if (out === silence || isUnreached(out)) return out; // nothing was born
189
+ await this.s.standings.take('shop', invitation); // now, and only now
190
+ return out;
191
+ }
192
+ answer(asker, method) {
193
+ if (method === undefined) return { asks: [{ name: 'hi', input: {} }], notes: {} };
194
+ return { heard: method };
195
+ }
196
+ }
197
+ ```
198
+
199
+ A relay. Forwards every ask to one standing and never looks inside, and asks
200
+ for a short wait because she is one door of several. Silence passes through
201
+ her; a word does not, since a word out of a being is `threw` at the door,
202
+ D13, so she says it as an error of her own.
203
+
204
+ ```js
205
+ class Relay {
206
+ constructor(stance) {
207
+ this.s = stance;
208
+ }
209
+ async answer(asker, method, args) {
210
+ const out = await this.s.standings.next.ask(method, args, { time: 2000 });
211
+ return isWord(out) ? { error: wordOf(out) } : out; // unreached and late reach her asker as an error, not as a throw
212
+ }
213
+ }
214
+ ```
215
+
216
+ A watcher. Notices a standing changed shape, then decides.
217
+
218
+ ```js
219
+ class Watcher {
220
+ constructor(stance) {
221
+ this.s = stance;
222
+ }
223
+ async answer(asker, method, args) {
224
+ if (method === undefined) return { asks: [{ name: 'poke', input: {} }], notes: {} };
225
+ const out = await this.s.standings.src.ask('read', args);
226
+ const rec = this.s.cells.standings.src;
227
+ if (rec.seen !== rec.digest) {
228
+ const bp = await this.s.standings.src.ask(); // refresh, or
229
+ if (!bp?.asks.some((a) => a.name === 'read')) this.s.standings.remove('src'); // walk away
230
+ }
231
+ return out;
232
+ }
233
+ }
234
+ ```
235
+
236
+ ## The ward side
237
+
238
+ The protocol names the ground as seven capabilities and leaves how they are
239
+ gathered to a kit. This kit gathers them into one object, `Ground` in
240
+ `src/ward/ground.ts`, with one name each:
241
+
242
+ ```
243
+ ground
244
+ seed bytes, or a string. 32 bytes are the seed; anything else is SHA-256'd to 32 bytes first.
245
+ memory the partition. an object.
246
+ instantiate (class name, stance) -> object | null
247
+ carry (ward pk, bytes) -> bytes | undefined undefined: no door was reached. a throw is read the same.
248
+ random (n) -> n bytes of entropy
249
+ wrote () -> nothing. said after every write to the partition. left out by a harbor that keeps nothing.
250
+ lend lend(name, take) -> taken. left out by a harbor that lends nothing.
251
+ ```
252
+
253
+ Beside that contract are the three pieces every harbor in this kit builds one
254
+ out of: `maker`, the code half, which finds a class by own key in the first
255
+ registry that holds it and remembers the object by the cells; `entropy`, the
256
+ one line every terrain has; and `learnPk`, the empty ask, since a harbor
257
+ learns its ward's pk the way anyone learns anything and has no second
258
+ derivation to keep in step. They are convenience and never contract: a kit
259
+ writes its own harbor, and may write these again.
260
+
261
+ The cells guard is `src/ward/cells.ts`. Beside the nesting bound the protocol
262
+ sets, it refuses a hole in a list, an accessor and a key named `__proto__`,
263
+ because this runtime reads each one back as something JSON never wrote. The
264
+ same guard reads args at the seal and her answer at the door.
265
+
266
+ It also bounds breadth, at `BREADTH`, a million values, which is this kit's
267
+ number and no word of the protocol: a bound on what a being holds is never
268
+ the protocol's business, and what crosses is bounded in bytes at the seal.
269
+ Depth alone does not bound what a value costs to keep. One subvalue may sit
270
+ under two keys, and nesting that forty levels deep is inside the nesting
271
+ bound and is a trillion values once written down. So the walk counts the tree
272
+ while it proves the graph, and a value already counted is a value already
273
+ proven: a graph that shares is walked once per node and never once per path,
274
+ which is the difference between a guard and a ward stopped by one write.
275
+
276
+ The names this kit reserves beyond `OWNER` and `PUBLIC` are `__proto__`,
277
+ `knock`, `take` and `remove`. The last three are the calls on `standings`,
278
+ which is an object here whose methods are named on the same face as her ids,
279
+ so a standing under one of them would be unreachable. A kit with free
280
+ functions has no such collision, and no far ward can tell either way, because
281
+ an id never crosses a door.
282
+
283
+ This ward's allowance is thirty seconds by default and five minutes at the
284
+ ceiling, in `src/ward/allowance.ts`. Those two numbers are a ward's own
285
+ policy, never negotiated and never on the wire.
286
+
287
+ The partition shape is `src/ward/partition.ts`, and the three bounded lists
288
+ the protocol leaves to a ward are numbers here: eight pks under `minted`,
289
+ and `gone` and `knocks` bounded the same way, oldest out. `last` on the heir
290
+ is the cut slot for a reply a later version might keep; nothing writes it and
291
+ nothing reads it.
292
+
293
+ The seal is `src/ward/seal.ts` and the arithmetic is `src/ward/arithmetic.ts`.
294
+ `SIZE` there is the protocol's one mebibyte, read before an ask is opened and
295
+ before a reply is. `test/ward.test.ts` reads every byte string that crossed,
296
+ so that nothing inner is readable in the bytes.
297
+
298
+ ## The harbor side
299
+
300
+ The **store** is `src/harbor/store.ts`, the **reach** is
301
+ `src/harbor/reach.ts`, the **harbor core** is `src/harbor/core.ts` and the
302
+ **dialer** is `src/harbor/dial.ts`. Both kinds of reach are written on the
303
+ standard surface every terrain carries, fetch and WebSocket. The memory store
304
+ beside the store interface is the kit's own; a disk, a tab and an edge object
305
+ each have theirs, outside this tree, and every one of them passes
306
+ `src/conformance/store.ts` untouched, as every reach passes
307
+ `src/conformance/reach.ts`.
308
+
309
+ The dialer's wait doubles from a second to thirty when a line drops, and
310
+ returns to that first second where the far side announced and was taken.
311
+ Those are this harbor's numbers: what a far side sees is only that a dialer
312
+ refused for a suite waits.
313
+
314
+ ## The memory harbor
315
+
316
+ The memory harbor, `src/harbor/memory.ts`, is one process and no wire. It
317
+ keeps partitions by seed, routes its own doors, and may be linked to peers,
318
+ which stands in for a wire, and cut, which stands in for weather. It routes
319
+ one hop: a pk that is not its own and not a direct peer's is nothing. It
320
+ keeps no lease: two of them may hold one seed and one partition, which is
321
+ the broken vouch every real harbor refuses, kept here so that the tests can
322
+ show what divergence looks like. Every ward and being assertion in the tree
323
+ is made on it first, so that the network never hides a fault in the words;
324
+ the harbor core then passes the same suite over memory stores.
325
+
326
+ ## The suites
327
+
328
+ The beings are fixed and the ward is what is tested. A ward kit in any
329
+ language offers the ground, the door, and the owner's ask as written, and
330
+ passes the same suite. Those tests are the checklist, not the mock.
331
+
332
+ - `test/being.test.ts`: the base class against a stance stub. No ward, no
333
+ harbor. Defaults, describe, dispatch, the gate, the reserved names, the
334
+ digest.
335
+ - `src/conformance/index.ts`: one suite of behaviours, written against the
336
+ stance and one probe. `World` is this kit's spelling of the six obligations
337
+ the protocol names for a probe, and `conform` takes a call that makes one.
338
+ A kit hands the suite its ward's heir table and
339
+ bind table as values, and a forged knock, so the suite can see that a
340
+ refusal wrote nothing and a knock bound what it should. It reads the
341
+ tables and never writes them; the forged knock is the one thing it sends
342
+ through a door. With the beings in `src/conformance/beings.ts`: a
343
+ printer, a shop, two customers, one raw being with no base class, and a
344
+ member of an estate. The probe is asked to build a world, and what it is
345
+ asked for includes what the box lends: a name and the class of the being
346
+ it lends under it. The harness stands one of each in a ward its harbor
347
+ roots and grounds every ward it boots to lend them, which is how the
348
+ seventh member is reached from inside the suite. A harness with no such
349
+ ward to stand says `canLend: false` at the call and that chapter skips,
350
+ named, the way `canDown` already works. Any ward must pass it, under any
351
+ topology.
352
+ - `src/conformance/estate.ts`: the estate, and the last chapter of that
353
+ suite. Every other chapter is a scene: three beings, one move, one answer.
354
+ This one is a graph under churn, which is the shape an organisation running
355
+ on Quo actually has -- scattered wards on scattered harbors, partners and
356
+ employees invited, knocking, taken, kicked, and moving to harbors of their
357
+ own. It keeps a model of the graph, plays legal moves against it from three
358
+ fixed seeds, and after every move holds two things: that the ledger every
359
+ ward keeps closes on both ends, and that every arc in it answers when asked
360
+ exactly what being that arc means. The second is held under all three
361
+ topologies, and that is the promise of Quo at a scale a scene cannot
362
+ reach: the model never learns the topology, so neither may any answer. The
363
+ model is the script's own bookkeeping and is never compared with itself
364
+ across topologies, which would hold by construction and prove nothing. A
365
+ relation is two arcs and never one edge:
366
+ the occupant is the host's, the standing is the guest's, and the model
367
+ keeps them apart because the ward does. A red run prints its seed and the
368
+ moves that got there. It is read beside one hand-written story that says in
369
+ prose what the model is for.
370
+ - `test/ward.test.ts`: the conformance suite against the real ward under
371
+ three topologies over the memory harbor, one ward, one harbor with a ward
372
+ per being, two harbors; and what only a real ward can be asked: the
373
+ owner's asks, the door judged byte by byte, rotation and a lost reply,
374
+ replay, a restart, and every byte on the wire inspected for anything inner.
375
+ - `test/public.test.ts`: the public being, what the door does and does not
376
+ do for her.
377
+ - `test/silence.test.ts`: the protocol's chapter "Silence, the words, error"
378
+ by its numbers: the five cases and two unreacheds of her ward, the thirteen
379
+ cases of the door with a partition snapshot under each, seven strangers
380
+ met with one silence and six bound keys hearing their word, hops refused
381
+ at zero, and the six lines of the law of one silence.
382
+ - `test/blueprint.test.ts`: blueprints and instantiation through a real
383
+ ward. What a boot leaves behind, what a restart brings back when the code
384
+ moved under the cells, the gate and the door, the reserved ids.
385
+ - `test/allowance.test.ts`: the allowance. Default, ceiling, nonsense, the
386
+ bound, the signed body, the door's refusal, and one ask through a ward.
387
+ - `test/lend.test.ts`: the seventh member of the ground, over two real wards
388
+ on one memory harbor. The ground answers an invitation and the ward knocks
389
+ and takes it, so a being is handed an id and never the value; null is every
390
+ kind of no; a being of the harbor's own ward wakes her by holding a
391
+ standing she minted, and her door names it as the occupant she chose;
392
+ removing that occupant is the whole of unsubscribe; the relation is born
393
+ again under the same id; a taker that will not take leaves the faculty no
394
+ occupant and no key, and the next lend stands; and a being taken out of her
395
+ ward lends nothing.
396
+ - `test/seal.test.ts`: the arithmetic against `vectors/arithmetic.json`,
397
+ then the seal, round trip, and what it refuses; then the framing against
398
+ `vectors/framing.json`. The arithmetic is standard and any language has
399
+ it. The framing is Quo's own, and a kit that reproduces the hashes and
400
+ not the ward pk, the digest, the signed ask body, the two sealed shapes,
401
+ the invitation or the knock is not this protocol. Every seed in that file
402
+ is fixed, so every output is fixed. The ask body is the payload as JSON
403
+ and is not canonical: the vectors pin the order the type declares, and a
404
+ kit that emits another order interoperates, because a door verifies the
405
+ bytes it received, and will not reproduce the vectors. `hops` is in no
406
+ vector, because nothing sets it. The invitation is JSON too, and the
407
+ knock is pinned as the ask it is, by the heir, to the heir, announcing
408
+ the knocker's own key, and then proven at a real door: a ward booted on
409
+ the vector's seed, drawing the heir secret as its first entropy, mints
410
+ the invitation vector, binds the knock vector at its door, answers it,
411
+ spends the heir, and refuses the same bytes again.
412
+ - `test/break.test.ts`: adversarial probes. Each states what the protocol
413
+ promises, then tries to break it.
414
+ - `test/gaps.test.ts` and `test/gaps2.test.ts`: cases the memory harbor
415
+ cannot express because it is honest, in-order and lossless: a lost reply,
416
+ a cycle, a partition written down and read back, a long relation, one
417
+ seed booted twice. Each test is a claim that the ward is wrong; one that
418
+ will not go red is not a bug.
419
+ - `src/conformance/store.ts`: the store suite, written against the store
420
+ interface alone and handed a maker of fresh stores. A fresh store keeps
421
+ nothing; what was put comes back as it went in, as values and not as the
422
+ object; a name already kept is refused; save and record touch only their
423
+ part and are nothing on a name not kept; take hands a ward out and frees
424
+ the name; hints are kept by pk and the last one wins.
425
+ - `src/conformance/reach.ts`: the reach suite, written against the reach
426
+ interface alone and handed a far side it controls: a door held behind a
427
+ pk over there, and the far side dropped. The door's bytes come back;
428
+ nothing for a pk nobody holds; asks in flight at once each get their own
429
+ answer; what crosses is a copy both ways; and once the far side is gone,
430
+ nothing. It cannot assert that a reach which sent and then lost the line
431
+ answers nothing at all, since a suite cannot wait forever.
432
+ - `test/harbor.test.ts`: the harbor pieces on the library's own ground,
433
+ no device and no wire. The store suite against the memory store; the
434
+ reach suite against the socket framing over two lines in one process, so
435
+ the frames are asserted with no network under them; the conformance
436
+ suite against two harbor cores over memory stores reaching each other
437
+ in-process, drop and adopt as the migration; a ward that moves harbor
438
+ keeping its pk and every relation, the rendezvous told on the line in
439
+ hand; a ward booted or dropped after a line opened, announced on that
440
+ line, and a claim on a pk nobody holds binding nothing; a restart from
441
+ the store with the hints; the dialer over a stubbed line, announce, a claim proven
442
+ at a real door before it binds and a claim nobody there holds left
443
+ unbound, fallback, unbind and the wait before it dials again; and the frames against
444
+ `vectors/wire.json`, the ask, the reply, nothing delivered and the
445
+ announce, so a kit reproduces the bytes on a socket; and the request
446
+ reach against the request record in the same file, over a fetch that
447
+ sees what a listener would, one POST with the suite in its header, the
448
+ reply as a 200 and nothing delivered as a 404. A socket to a real
449
+ listener and every real store pass the same suites outside this tree.
450
+ - `test/terrain.test.ts`: the terrain census. Nothing under `src/being/`,
451
+ `src/ward/`, `src/harbor/` or `src/conformance/` names a runtime, and
452
+ nothing reaches for a global outside the ones it names; nor does the
453
+ census name one the tree has stopped using. It reads the source as text
454
+ and not as a parse: it is there to catch drift, and does not pretend to
455
+ stop someone determined to get around it.
456
+ - `test/assert.test.ts`: `src/conformance/assert.ts` against
457
+ `node:assert/strict`, pair by pair. The suite means the same thing on
458
+ every terrain only if those two agree, so they are compared and not
459
+ trusted.
460
+ - `test/floor.test.ts`: the five algorithms this terrain must carry, probed
461
+ one by one; that the arithmetic spends every one of them; and the two ways
462
+ a terrain can be short -- no `crypto.subtle` at all, and a subtle without
463
+ the curves -- each failing at the first call, in one sentence.
464
+ - `test/package.test.ts`: the package as a stranger meets it. The tarball
465
+ is packed, installed into an empty folder with nothing but Node, and
466
+ every entry point the exports map names is imported. Every other suite
467
+ reaches the source through a path or a workspace link, which resolves
468
+ outside `node_modules`, where Node strips types; this is the one that
469
+ cannot.
470
+ - `test/bundle.test.ts`: the three words bundled as a consumer must bundle
471
+ them, the artefact read for anything a terrain cannot provide, and then the
472
+ whole conformance suite run out of the bundle.
473
+ - `test/terrain/browser.test.ts`, `test/terrain/edge.test.ts`,
474
+ `test/terrain/deno.test.ts` and `test/terrain/bun.test.ts`: that same
475
+ bundle and that same suite in a real Chromium, in workerd, in Deno and in
476
+ Bun, plus each terrain's floor, and, for the browser, a page whose
477
+ `crypto` has no `subtle`. Deno runs it a second time with every permission
478
+ denied but read -- no net, no environment, no write, no subprocess -- which
479
+ is where a ward that had quietly come to need one of them would fail, and
480
+ nowhere else in this repository. A terrain whose binary is absent skips and
481
+ names the command that installs it; it never passes quietly.
482
+ `test/terrain/engine.ts` runs the bundle in another engine on this machine.
483
+ Behind `npm run check:terrain`. `test/terrain/bundle.ts` is the
484
+ bundler and the reference run, `test/terrain/exercise.ts` is what every
485
+ terrain runs, `test/terrain/floor.ts` is the floor -- each written once, so
486
+ no terrain can be probed for less than another.
487
+ - `test/world.ts`: the memory-harbor probe and the three topologies, held
488
+ apart from any one chapter because every terrain drives the same one. It
489
+ also answers the two asks the estate needs: the census, which is every
490
+ partition in the world as values, and a migration, which lifts a partition
491
+ out of one harbor and boots it from the same seed in the other -- same
492
+ seed, same pk, so every standing anyone holds still points at her.
493
+ - `test/repo.test.ts`: the papers, the README and the project instructions
494
+ against the repository: every path they name exists, every
495
+ source file carries its licence, the package names no host and no user,
496
+ the awaitable calls are marked and
497
+ awaited in the examples above, the allowance is decided in the protocol and
498
+ off its Open list, and the allowance is time alone on both sides of the
499
+ door.
500
+
501
+ The conformance suite is checkable wherever a ward runs. Its runner is handed
502
+ in, and the five assertions it makes are `src/conformance/assert.ts`, held to
503
+ `node:assert/strict`'s own behaviour by `test/assert.test.ts`.
504
+ `test/terrain/exercise.ts` is what every terrain runs -- the whole suite,
505
+ under all three topologies, and the floor probe. Node is the reference: no
506
+ terrain carries a count of its own, so a test added to the suite is demanded
507
+ of every terrain at once. Five run it: Node, a browser, workerd, Deno and
508
+ Bun. Bun is the one that is not V8, so "the language alone" is checked
509
+ against two implementations of the language and not one.
510
+
511
+ ## The gate
512
+
513
+ The gate is `npm run check`: typecheck under TypeScript 7, oxlint with
514
+ type-aware rules, markdownlint, then every suite under `node --test`. There
515
+ is no CI; the gate runs in seconds and is run before every commit. Nothing
516
+ red is committed except a test marked todo, which is a claim the tree does
517
+ not yet meet and says so.
518
+
519
+ `npm run check` is Node alone, and stays fast. `npm run check:terrain` is
520
+ every other terrain, the browser, workerd, Deno and Bun, kept apart because
521
+ each is a binary of its own and the browser is a download and not a
522
+ package: a fresh clone needs `npx playwright install chromium` first, and is
523
+ told so in one sentence rather than a stack trace.
@@ -16,14 +16,14 @@ export type AskSpec = {
16
16
  };
17
17
 
18
18
  // Names a subclass may not use for an ask, because they are the base's own.
19
- const RESERVED = new Set(['answer', 'describe', 'stance', 'cells', 'standings', 'occupants', 'occupant', 'invite', 'knock', 'take', 'boot', 'constructor']);
19
+ const RESERVED = new Set(['answer', 'describe', 'stance', 'cells', 'standings', 'occupants', 'occupant', 'invite', 'knock', 'take', 'boot', 'lend', 'constructor']);
20
20
 
21
21
  // Whether she has a method of that name, written on her own prototype chain
22
22
  // below Object's. A name Object lends every object, `hasOwnProperty` or
23
23
  // `toString`, is not a method she wrote; and a field she assigns in her own
24
24
  // constructor is not there yet when the base checks, so an ask is a method
25
25
  // on the prototype and nothing else.
26
- const method = (self: object, name: string): boolean => {
26
+ const wrote = (self: object, name: string): boolean => {
27
27
  for (let p = Object.getPrototypeOf(self); p !== null && p !== Object.prototype; p = Object.getPrototypeOf(p)) {
28
28
  if (Object.hasOwn(p, name)) return typeof (p as Record<string, unknown>)[name] === 'function';
29
29
  }
@@ -51,7 +51,7 @@ export class Being {
51
51
  const C = this.constructor as typeof Being;
52
52
  for (const name of Object.keys(C.asks)) {
53
53
  if (RESERVED.has(name)) throw new Error(`ask '${name}' is a reserved name`);
54
- if (!method(this, name)) throw new Error(`ask '${name}' has no method on the prototype`);
54
+ if (!wrote(this, name)) throw new Error(`ask '${name}' has no method on the prototype`);
55
55
  }
56
56
  // Own keys only: a default named after a member of Object's prototype is
57
57
  // still hers, and still missing until she writes it.
@@ -67,6 +67,11 @@ export class Being {
67
67
  get occupants(): Occupants {
68
68
  return this.stance.occupants;
69
69
  }
70
+ // A standing at one of the things this device can do, under an id of hers.
71
+ // The ward knocks and takes it for her; the invitation never reaches her.
72
+ lend(name: string, id: string): Promise<string | null> {
73
+ return this.stance.lend(name, id);
74
+ }
70
75
  invite(id: string, notes?: JsonObject): Promise<Invitation | null> {
71
76
  return this.stance.occupants.invite(id, notes);
72
77
  }
@@ -3,5 +3,5 @@
3
3
  export { Being, type AskSpec } from './being.ts';
4
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, isBlueprint, invitationArgs } from './types.ts';
6
+ export { OWNER, PUBLIC, RESERVED_IDS, isBlueprint, invitationArgs, isInvitation } from './types.ts';
7
7
  export type * from './types.ts';
@@ -41,6 +41,23 @@ export const invitationArgs = (inv: Invitation): JsonObject => ({
41
41
  ...(inv.secret !== undefined ? { secret: inv.secret } : {}),
42
42
  });
43
43
 
44
+ // Whether a value that arrived is an invitation. The shape is the spec's, so
45
+ // the reading of it is too, and it is one reading: a form that
46
+ // asks a guest for one, a faculty handed one to wake her maker by, and a
47
+ // shell that finds one in a link all ask the same question, and a kinder
48
+ // answer in one of them is a value that fails at a door instead of at the
49
+ // edge it came in by. A ward pk is a hundred and twenty-eight lowercase hex,
50
+ // and a heir comes with its secret or neither comes: an invitation to a
51
+ // public being carries no heir, and a heir with no secret opens nothing.
52
+ export const isInvitation = (v: unknown): v is Invitation => {
53
+ if (v === null || typeof v !== 'object' || Array.isArray(v)) return false;
54
+ const o = v as Record<string, unknown>;
55
+ if (typeof o.ward !== 'string' || !/^[0-9a-f]{128}$/.test(o.ward)) return false;
56
+ const heir = typeof o.heir === 'string',
57
+ secret = typeof o.secret === 'string';
58
+ return (heir && secret) || (!heir && !secret && !('heir' in o) && !('secret' in o));
59
+ };
60
+
44
61
  // A blueprint is an MCP tool list plus notes.
45
62
  export type Schema = JsonObject;
46
63
  export type Ask = { name: string; description?: string; input: Schema; output?: Schema };
@@ -119,6 +136,23 @@ export type Stance = {
119
136
  readonly cells: Cells;
120
137
  readonly occupants: Occupants;
121
138
  readonly standings: Standings;
139
+ // A standing at one of the things this device can do, by the harbor's name
140
+ // for it, under an id of hers. The id back, or null: this box lends no such
141
+ // name, the id already names a record or is a reserved word, or the
142
+ // relation was refused.
143
+ //
144
+ // Her ward asks the ground, then knocks and takes in her name, as it does
145
+ // for a being she boots. So the invitation never reaches her, because it is
146
+ // the device's capability and not her own relation to give away; what she
147
+ // holds afterwards is an ordinary standing that counts, rotates, can be
148
+ // removed and hears `removed`.
149
+ //
150
+ // The ground is the booting harbor's, so this reaches the box she is
151
+ // running on and no other. A standing she wakes up holding names a being on
152
+ // the box she was on when it was made, and a migration is a restart she
153
+ // cannot tell from any other, so a being who wants the box she is on asks
154
+ // again at every birth.
155
+ lend(name: string, id: string): Promise<string | null>;
122
156
  // A new being of her ward, by class name, under a key she chooses. The key
123
157
  // back, or null: the key is taken, the harbor holds no such class, or the
124
158
  // class threw at birth. A being may make; only the owner reaches into