@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.
- package/README.md +10 -4
- package/SPEC.md +354 -536
- package/dist/being/being.d.ts +1 -0
- package/dist/being/being.js +8 -3
- package/dist/being/index.d.ts +1 -1
- package/dist/being/index.js +1 -1
- package/dist/being/types.d.ts +2 -0
- package/dist/being/types.js +17 -0
- package/dist/conformance/beings.d.ts +41 -0
- package/dist/conformance/beings.js +28 -2
- package/dist/conformance/index.d.ts +10 -1
- package/dist/conformance/index.js +162 -6
- package/dist/harbor/core.d.ts +4 -2
- package/dist/harbor/core.js +25 -4
- package/dist/harbor/index.d.ts +1 -1
- package/dist/harbor/memory.d.ts +3 -3
- package/dist/harbor/memory.js +6 -3
- package/dist/harbor/reach.js +1 -1
- package/dist/ward/cells.d.ts +2 -0
- package/dist/ward/cells.js +60 -11
- package/dist/ward/door.d.ts +1 -0
- package/dist/ward/door.js +29 -10
- package/dist/ward/ground.d.ts +3 -1
- package/dist/ward/ground.js +1 -1
- package/dist/ward/heirs.js +8 -1
- package/dist/ward/index.d.ts +2 -2
- package/dist/ward/index.js +3 -3
- package/dist/ward/owner.js +49 -8
- package/dist/ward/seal.d.ts +1 -0
- package/dist/ward/seal.js +10 -2
- package/dist/ward/stance.d.ts +1 -0
- package/dist/ward/stance.js +62 -4
- package/dist/ward/ward.js +5 -0
- package/package.json +5 -3
- package/quo-kit.md +523 -0
- package/src/being/being.ts +8 -3
- package/src/being/index.ts +1 -1
- package/src/being/types.ts +34 -0
- package/src/conformance/beings.ts +25 -2
- package/src/conformance/estate.ts +9 -9
- package/src/conformance/index.ts +204 -7
- package/src/conformance/reach.ts +1 -1
- package/src/harbor/core.ts +26 -5
- package/src/harbor/index.ts +1 -1
- package/src/harbor/memory.ts +7 -4
- package/src/harbor/reach.ts +1 -1
- package/src/ward/cells.ts +59 -10
- package/src/ward/door.ts +27 -9
- package/src/ward/ground.ts +39 -11
- package/src/ward/heirs.ts +7 -1
- package/src/ward/index.ts +4 -4
- package/src/ward/owner.ts +45 -10
- package/src/ward/seal.ts +11 -2
- package/src/ward/stance.ts +60 -4
- 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.
|
package/src/being/being.ts
CHANGED
|
@@ -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
|
|
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 (!
|
|
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
|
}
|
package/src/being/index.ts
CHANGED
|
@@ -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';
|
package/src/being/types.ts
CHANGED
|
@@ -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
|