@quo-systems/quo 0.1.0 → 0.1.1

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 (63) hide show
  1. package/README.md +9 -6
  2. package/SPEC.md +51 -24
  3. package/dist/being/being.d.ts +22 -0
  4. package/dist/being/being.js +78 -0
  5. package/dist/being/digest.d.ts +3 -0
  6. package/dist/being/digest.js +19 -0
  7. package/dist/being/index.d.ts +5 -0
  8. package/dist/being/index.js +6 -0
  9. package/dist/being/silence.d.ts +5 -0
  10. package/dist/being/silence.js +11 -0
  11. package/dist/being/types.d.ts +80 -0
  12. package/dist/being/types.js +16 -0
  13. package/dist/conformance/assert.d.ts +11 -0
  14. package/dist/conformance/assert.js +72 -0
  15. package/dist/conformance/beings.d.ts +114 -0
  16. package/dist/conformance/beings.js +126 -0
  17. package/dist/conformance/estate.d.ts +5 -0
  18. package/dist/conformance/estate.js +310 -0
  19. package/dist/conformance/index.d.ts +65 -0
  20. package/dist/conformance/index.js +446 -0
  21. package/dist/conformance/reach.d.ts +10 -0
  22. package/dist/conformance/reach.js +72 -0
  23. package/dist/conformance/store.d.ts +5 -0
  24. package/dist/conformance/store.js +97 -0
  25. package/dist/harbor/core.d.ts +41 -0
  26. package/dist/harbor/core.js +209 -0
  27. package/dist/harbor/dial.d.ts +8 -0
  28. package/dist/harbor/dial.js +45 -0
  29. package/dist/harbor/index.d.ts +6 -0
  30. package/dist/harbor/index.js +10 -0
  31. package/dist/harbor/memory.d.ts +20 -0
  32. package/dist/harbor/memory.js +63 -0
  33. package/dist/harbor/reach.d.ts +36 -0
  34. package/dist/harbor/reach.js +166 -0
  35. package/dist/harbor/store.d.ts +33 -0
  36. package/dist/harbor/store.js +41 -0
  37. package/dist/ward/allowance.d.ts +10 -0
  38. package/dist/ward/allowance.js +60 -0
  39. package/dist/ward/arithmetic.d.ts +26 -0
  40. package/dist/ward/arithmetic.js +159 -0
  41. package/dist/ward/cells.d.ts +3 -0
  42. package/dist/ward/cells.js +79 -0
  43. package/dist/ward/door.d.ts +12 -0
  44. package/dist/ward/door.js +107 -0
  45. package/dist/ward/ground.d.ts +12 -0
  46. package/dist/ward/ground.js +1 -0
  47. package/dist/ward/heirs.d.ts +12 -0
  48. package/dist/ward/heirs.js +85 -0
  49. package/dist/ward/index.d.ts +8 -0
  50. package/dist/ward/index.js +10 -0
  51. package/dist/ward/owner.d.ts +32 -0
  52. package/dist/ward/owner.js +114 -0
  53. package/dist/ward/partition.d.ts +51 -0
  54. package/dist/ward/partition.js +62 -0
  55. package/dist/ward/seal.d.ts +48 -0
  56. package/dist/ward/seal.js +105 -0
  57. package/dist/ward/stance.d.ts +19 -0
  58. package/dist/ward/stance.js +250 -0
  59. package/dist/ward/ward.d.ts +2 -0
  60. package/dist/ward/ward.js +197 -0
  61. package/package.json +10 -6
  62. package/src/harbor/core.ts +45 -4
  63. package/src/harbor/dial.ts +1 -1
package/README.md CHANGED
@@ -18,8 +18,10 @@ assumes nothing from any other document, this README included. Read it first.
18
18
 
19
19
  ## This package
20
20
 
21
- A TypeScript implementation, run directly on Node's own type stripping. No
22
- build step, no dependencies.
21
+ A TypeScript implementation, run directly on Node's own type stripping, with
22
+ no dependencies. The package ships JavaScript with declarations, emitted by
23
+ `npm run build` before every gate and every publish, because Node strips
24
+ types nowhere under `node_modules`.
23
25
 
24
26
  ```
25
27
  src/being/ the Being side: what a being author imports, if anything
@@ -80,10 +82,11 @@ a TypeScript ward. `SPEC.md` "The tree" says the whole of it.
80
82
 
81
83
  Published from 0.1.0, the first version under this name, with no
82
84
  compatibility promise before 1.0.0: the words may still move. `npm pack
83
- --dry-run` shows what ships: the source, the vectors, the spec, and the
84
- licence — no tests, no configs. The spec ships because it is the truth the
85
- source and the vectors are read against. `npm publish` runs both gates
86
- first, `check` and `check:terrain`, and refuses on a failure.
85
+ --dry-run` shows what ships: the emitted `dist/`, the source, the vectors,
86
+ the spec, and the licence — no tests, no configs. The spec ships because it
87
+ is the truth the source and the vectors are read against. `npm publish`
88
+ runs both gates first, `check` and `check:terrain`, and refuses on a
89
+ failure.
87
90
 
88
91
  ## License
89
92
 
package/SPEC.md CHANGED
@@ -1057,7 +1057,8 @@ byte, a four-byte id, the 64-byte pk and the bytes; a reply is the kind, the
1057
1057
  id and the bytes; and nothing delivered is the kind and the id alone. Both
1058
1058
  kinds are written on the standard surface every terrain carries, fetch and
1059
1059
  WebSocket, and this tree holds either end of a line and never a listener:
1060
- who accepts a socket is the terrain's business. A reach reads nothing.
1060
+ who accepts a socket is the terrain's business. A reach reads nothing; a
1061
+ harbor opens no box but its own probe's.
1061
1062
  Nothing comes back only where the reach knows nothing was delivered: no such
1062
1063
  pk at the far end, a connection that would not open, a line already gone. A
1063
1064
  reach that sent the bytes and lost the line after answers nothing at all,
@@ -1099,8 +1100,24 @@ ward the store keeps, keeps the map of ward pk to door for its own wards
1099
1100
  and the **directory**, pk to reach, for foreign ones, carries bytes to a
1100
1101
  pk and delivers bytes from the wire to one door. The directory is filled
1101
1102
  three ways, in this order: its own doors; a socket a dialer holds to it,
1102
- bound at the dialer's announce and unbound when the line closes; and a
1103
- hint, a pk at a URL, kept in the store. One rule makes a **rendezvous** of
1103
+ bound once the door behind the dialer's claim has proved it, and unbound
1104
+ when the line closes; and a hint, a pk at a URL, kept in the store.
1105
+
1106
+ An announce is a claim, and a claim binds nothing until proven: anyone who
1107
+ can reach a listener could otherwise name a pk that is not theirs and take
1108
+ its reachability, the one thing a rendezvous exists to give. The proof is
1109
+ the door as it already is. The harbor mints a lid and sends it down the
1110
+ line with noise after it, an ask for the claimed pk; that is a box that
1111
+ does not open, D1, and the door answers it with silence sealed to the lid
1112
+ and signed by the ward key. Only the holder of that seed writes that reply,
1113
+ the lid is fresh so nothing replays, and a box that does not open writes
1114
+ nothing at the ward. Each claimed pk is proven on its own, both ways: the
1115
+ listener proves the dialer's claims and the dialer the listener's. What a
1116
+ relay that genuinely reaches a ward elsewhere can still do is forward the
1117
+ probe and pass, and then it is a rendezvous for that ward like any other,
1118
+ able to drop and nothing more. This is the one box a harbor ever opens,
1119
+ the one it sealed itself, and it reads nothing from it but whose key
1120
+ signed. One rule makes a **rendezvous** of
1104
1121
  any listener: bytes that arrive from the wire go to an own door or to a
1105
1122
  socket this harbor holds for that pk, and never onward by request. So a
1106
1123
  harbor that cannot be dialed is reached through the one it dialed, by
@@ -1296,17 +1313,30 @@ class Watcher {
1296
1313
  ## The tree
1297
1314
 
1298
1315
  The repository is one TypeScript implementation of this document, run
1299
- directly on Node's type stripping, no build step, no dependencies. Node 22.18
1316
+ directly on Node's type stripping, no dependencies. Node 22.18
1300
1317
  or later. The package is `@quo-systems/quo`, published from 0.1.0, the
1301
1318
  first version under this name, and it starts over: nothing before it under
1302
1319
  any other name is this protocol. Before 1.0.0 every version may change the
1303
- words, and nothing is kept for a holder of an earlier one. `npm pack
1304
- --dry-run` shows what ships: the four source folders, the vectors, this
1305
- document, the README and the licence, and nothing else. This document
1306
- ships, decided here: it is the truth the source and the vectors are read
1307
- against, and a kit in another language is written against it and nothing
1308
- else. Publishing runs both gates first, `npm run check` and
1309
- `npm run check:terrain`, and refuses on a failure.
1320
+ words, and nothing is kept for a holder of an earlier one.
1321
+
1322
+ The tree runs as TypeScript and the package ships as JavaScript. Node
1323
+ strips types nowhere under `node_modules`, so a consumer cannot load the
1324
+ source, and 0.1.0, which shipped it, imported nowhere; 0.1.1 is the first
1325
+ version a stranger can import. `npm run build` emits `dist/` from `src`
1326
+ alone, one JavaScript file and one declaration file per source file with
1327
+ every relative import rewritten, and the exports map points there, types
1328
+ beside default. The
1329
+ build runs before every gate and every publish and is never tracked;
1330
+ nothing under `test/` or `estate/` imports it except through the package
1331
+ name, which is how a consumer does. `test/package.test.ts` is the one
1332
+ suite that meets the package as a stranger: it packs the tarball,
1333
+ installs it into an empty folder, and imports every entry point under
1334
+ plain Node. `npm pack --dry-run` shows what ships: `dist/`, the four
1335
+ source folders, the vectors, this document, the README and the licence,
1336
+ and nothing else. This document ships, decided here: it is the truth the
1337
+ source and the vectors are read against, and a kit in another language is
1338
+ written against it and nothing else. Publishing runs both gates first,
1339
+ `npm run check` and `npm run check:terrain`, and refuses on a failure.
1310
1340
 
1311
1341
  ```
1312
1342
  src/being/ the Being side. types, the base class, silence, the digest.
@@ -1435,8 +1465,9 @@ passes the same suite. Those tests are the checklist, not the mock.
1435
1465
  the frames are asserted with no network under them; the conformance
1436
1466
  suite against two harbor cores over memory stores reaching each other
1437
1467
  in-process, drop and adopt as the migration; a restart from the store
1438
- with the hints; the dialer over a stubbed line, announce, bind, fallback,
1439
- unbind and the wait before it dials again; and the frames against
1468
+ with the hints; the dialer over a stubbed line, announce, a claim proven
1469
+ at a real door before it binds and a claim nobody there holds left
1470
+ unbound, fallback, unbind and the wait before it dials again; and the frames against
1440
1471
  `vectors/wire.json`, the ask, the reply, nothing delivered and the
1441
1472
  announce, so a kit reproduces the bytes on a socket; and the request
1442
1473
  reach against the request record in the same file, over a fetch that
@@ -1457,6 +1488,12 @@ passes the same suite. Those tests are the checklist, not the mock.
1457
1488
  one by one; that the arithmetic spends every one of them; and the two ways
1458
1489
  a terrain can be short -- no `crypto.subtle` at all, and a subtle without
1459
1490
  the curves -- each failing at the first call, in one sentence.
1491
+ - `test/package.test.ts`: the package as a stranger meets it. The tarball
1492
+ is packed, installed into an empty folder with nothing but Node, and
1493
+ every entry point the exports map names is imported. Every other suite
1494
+ reaches the source through a path or a workspace link, which resolves
1495
+ outside `node_modules`, where Node strips types; this is the one that
1496
+ cannot.
1460
1497
  - `test/bundle.test.ts`: the three words bundled as a consumer must bundle
1461
1498
  them, the artefact read for anything a terrain cannot provide, and then the
1462
1499
  whole conformance suite run out of the bundle.
@@ -1492,17 +1529,7 @@ passes the same suite. Those tests are the checklist, not the mock.
1492
1529
  The only place this document may name a gap between itself and the tree.
1493
1530
  Each line is a debt to close, not a note to keep.
1494
1531
 
1495
- - **An announce is believed.** A side that opens a socket to a listener and
1496
- announces a ward pk is bound to it, and nothing asks whether it holds that
1497
- ward. So anyone who can reach a rendezvous can announce a pk that is not
1498
- theirs, and asks for that pk are carried to them instead of to the harbor
1499
- that holds it. They learn nothing by it: what arrives is sealed to a ward
1500
- key they do not have, and they cannot answer, because a reply is signed by
1501
- that ward. What they take is reachability, which is the one thing a
1502
- rendezvous exists to give. Measured on the lab, 2026-09-04. The shape of
1503
- the answer is a proof at the announce, the ward signing something the
1504
- listener chose, so that binding a pk needs the key behind it; that is a
1505
- decision this document has not taken.
1532
+ None today.
1506
1533
 
1507
1534
  ## Open
1508
1535
 
@@ -0,0 +1,22 @@
1
+ import type { Asker, Blueprint, Cells, Invitation, JsonObject, Wanted, OccupantRecord, Occupants, Reply, Schema, Stance, Standings, Answer } from './types.ts';
2
+ export type AskSpec = {
3
+ description?: string;
4
+ input?: Schema;
5
+ output?: Schema;
6
+ for?: (occupant: OccupantRecord | undefined, asker: Asker) => boolean;
7
+ };
8
+ export declare class Being {
9
+ static cells: JsonObject;
10
+ static asks: Record<string, AskSpec>;
11
+ readonly stance: Stance;
12
+ constructor(stance: Stance);
13
+ get cells(): Cells;
14
+ get standings(): Standings;
15
+ get occupants(): Occupants;
16
+ invite(id: string): Promise<Invitation | null>;
17
+ knock(invitation: Invitation, method?: string, args?: JsonObject, wanted?: Wanted): Promise<Answer>;
18
+ take(id: string, invitation: Invitation): Promise<string | null>;
19
+ occupant(asker: Asker): OccupantRecord | undefined;
20
+ describe(asker: Asker): Blueprint;
21
+ answer(asker: Asker, method?: string, args?: JsonObject): Promise<Reply>;
22
+ }
@@ -0,0 +1,78 @@
1
+ // Names a subclass may not use for an ask, because they are the base's own.
2
+ const RESERVED = new Set(['answer', 'describe', 'stance', 'cells', 'standings', 'occupants', 'occupant', 'invite', 'knock', 'take', 'constructor']);
3
+ export class Being {
4
+ // Her cells' defaults. Merged in at birth, only where a key is missing, so
5
+ // a restart keeps what she wrote.
6
+ static cells = {};
7
+ // What she can be asked. Declaration order is blueprint order.
8
+ static asks = {};
9
+ stance;
10
+ constructor(stance) {
11
+ this.stance = stance;
12
+ const C = this.constructor;
13
+ for (const name of Object.keys(C.asks)) {
14
+ if (RESERVED.has(name))
15
+ throw new Error(`ask '${name}' is a reserved name`);
16
+ if (typeof this[name] !== 'function')
17
+ throw new Error(`ask '${name}' has no method`);
18
+ }
19
+ for (const [k, v] of Object.entries(C.cells))
20
+ if (!(k in stance.cells))
21
+ stance.cells[k] = structuredClone(v);
22
+ }
23
+ get cells() {
24
+ return this.stance.cells;
25
+ }
26
+ get standings() {
27
+ return this.stance.standings;
28
+ }
29
+ get occupants() {
30
+ return this.stance.occupants;
31
+ }
32
+ invite(id) {
33
+ return this.stance.occupants.invite(id);
34
+ }
35
+ knock(invitation, method, args, wanted) {
36
+ return this.stance.standings.knock(invitation, method, args, wanted);
37
+ }
38
+ take(id, invitation) {
39
+ return this.stance.standings.take(id, invitation);
40
+ }
41
+ // The occupant record for whoever is at the door. Undefined at a public being.
42
+ occupant(asker) {
43
+ return asker.id === undefined ? undefined : this.cells.occupants[asker.id];
44
+ }
45
+ // Her blueprint for this asker. Override to shape it by hand.
46
+ describe(asker) {
47
+ const C = this.constructor;
48
+ const rec = this.occupant(asker);
49
+ const asks = [];
50
+ for (const [name, spec] of Object.entries(C.asks)) {
51
+ if (spec.for && !spec.for(rec, asker))
52
+ continue;
53
+ const ask = { name, input: spec.input ?? { type: 'object' } };
54
+ if (spec.description !== undefined)
55
+ ask.description = spec.description;
56
+ if (spec.output !== undefined)
57
+ ask.output = spec.output;
58
+ asks.push(ask);
59
+ }
60
+ return { asks, notes: {} };
61
+ }
62
+ // The one function. Override to wrap it; call super to keep the dispatch.
63
+ async answer(asker, method, args = {}) {
64
+ if (method === undefined)
65
+ return this.describe(asker);
66
+ const C = this.constructor;
67
+ // Declared, by her, on purpose. `asks` is an ordinary object, so a bare
68
+ // lookup would also find every name on Object's prototype: `valueOf`
69
+ // would answer with her stance, `toString` with a string, and neither is
70
+ // an ask she wrote. Only her own keys are asks, which is what describe
71
+ // has always shown. What she shows is what she can be asked.
72
+ const spec = typeof method === 'string' && Object.hasOwn(C.asks, method) ? C.asks[method] : undefined;
73
+ if (!spec || (spec.for && !spec.for(this.occupant(asker), asker)))
74
+ return { error: 'unknown ask' };
75
+ const fn = this[method];
76
+ return fn.call(this, args ?? {}, asker);
77
+ }
78
+ }
@@ -0,0 +1,3 @@
1
+ import type { Json } from './types.ts';
2
+ export declare const canonical: (v: Json) => string;
3
+ export declare const digest: (blueprint: Json) => Promise<string>;
@@ -0,0 +1,19 @@
1
+ // A value I-JSON has no room for: a key left empty, a function, a symbol.
2
+ // None of them cross an edge, so none of them may reach a digest. A key
3
+ // carrying one is dropped and a slot carrying one is null, which is what
4
+ // crossing does to them, so the digest names what arrived and not what she
5
+ // happened to be holding.
6
+ const absent = (v) => v === undefined || typeof v === 'function' || typeof v === 'symbol';
7
+ // JCS for I-JSON values: sorted keys, no whitespace, JSON escaping. Numbers
8
+ // are serialized as ES does, which is what RFC 8785 specifies.
9
+ export const canonical = (v) => Array.isArray(v)
10
+ ? `[${v.map((slot) => (absent(slot) ? 'null' : canonical(slot))).join(',')}]`
11
+ : v !== null && typeof v === 'object'
12
+ ? `{${Object.keys(v)
13
+ .filter((k) => !absent(v[k]))
14
+ .sort()
15
+ .map((k) => `${JSON.stringify(k)}:${canonical(v[k])}`)
16
+ .join(',')}}`
17
+ : JSON.stringify(v);
18
+ const hex = (bytes) => Array.from(new Uint8Array(bytes), (b) => b.toString(16).padStart(2, '0')).join('');
19
+ export const digest = async (blueprint) => hex(await globalThis.crypto.subtle.digest('SHA-256', new TextEncoder().encode(canonical(blueprint))));
@@ -0,0 +1,5 @@
1
+ export { Being, type AskSpec } from './being.ts';
2
+ export { silence, isSilence, unreached, isUnreached } from './silence.ts';
3
+ export { digest, canonical } from './digest.ts';
4
+ export { OWNER, PUBLIC, RESERVED_IDS } from './types.ts';
5
+ export type * from './types.ts';
@@ -0,0 +1,6 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // @quo-systems/quo — the Being side. What a being author imports, if anything.
3
+ export { Being } from './being.js';
4
+ export { silence, isSilence, unreached, isUnreached } from './silence.js';
5
+ export { digest, canonical } from './digest.js';
6
+ export { OWNER, PUBLIC, RESERVED_IDS } from './types.js';
@@ -0,0 +1,5 @@
1
+ import { type Unreached } from './types.ts';
2
+ export declare const silence: unique symbol;
3
+ export declare const isSilence: (x: unknown) => x is typeof silence;
4
+ export declare const unreached: () => Unreached;
5
+ export declare const isUnreached: (x: unknown) => x is Unreached;
@@ -0,0 +1,11 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Silence is one distinguished value. Null is an answer. Silence is no answer.
3
+ import { UNREACHED_KEY } from './types.js';
4
+ export const silence = Symbol.for('quo.silence');
5
+ export const isSilence = (x) => x === silence;
6
+ // Unreached is the ward's own word: no far door was reached. It carries no
7
+ // reason. Safe to retry, because nothing was delivered. A being cannot
8
+ // produce it: a ward that sees it come out of a being treats it as silence.
9
+ const UNREACHED = Object.freeze({ [UNREACHED_KEY]: true });
10
+ export const unreached = () => UNREACHED;
11
+ export const isUnreached = (x) => x !== null && typeof x === 'object' && x[UNREACHED_KEY] === true;
@@ -0,0 +1,80 @@
1
+ import type { silence } from './silence.ts';
2
+ export type Json = null | boolean | number | string | Json[] | {
3
+ [key: string]: Json;
4
+ };
5
+ export type JsonObject = {
6
+ [key: string]: Json;
7
+ };
8
+ export type Asker = {
9
+ id: string;
10
+ } | {
11
+ id?: undefined;
12
+ };
13
+ export declare const OWNER = "OWNER";
14
+ export declare const PUBLIC = "PUBLIC";
15
+ export declare const RESERVED_IDS: readonly string[];
16
+ export type Invitation = {
17
+ ward: string;
18
+ heir?: string;
19
+ secret?: string;
20
+ };
21
+ export type Schema = JsonObject;
22
+ export type Ask = {
23
+ name: string;
24
+ description?: string;
25
+ input: Schema;
26
+ output?: Schema;
27
+ };
28
+ export type Blueprint = {
29
+ asks: Ask[];
30
+ notes: Json;
31
+ };
32
+ export type StandingRecord = {
33
+ id: string;
34
+ digest: string | null;
35
+ blueprint: Blueprint | null;
36
+ seen: string | null;
37
+ };
38
+ export type OccupantRecord = {
39
+ id: string;
40
+ notes: JsonObject;
41
+ };
42
+ export type Cells = {
43
+ standings: Record<string, StandingRecord>;
44
+ occupants: Record<string, OccupantRecord>;
45
+ [hers: string]: Json;
46
+ };
47
+ export type Silence = typeof silence;
48
+ export type Unreached = {
49
+ readonly [K in typeof UNREACHED_KEY]: true;
50
+ };
51
+ export declare const UNREACHED_KEY: unique symbol;
52
+ export type Answer = Json | Silence | Unreached;
53
+ export type Reply = Json | Silence;
54
+ export type Wanted = {
55
+ time?: number;
56
+ };
57
+ export type Standing = {
58
+ readonly id: string;
59
+ ask(method?: string, args?: JsonObject, wanted?: Wanted): Promise<Answer>;
60
+ };
61
+ export type Standings = {
62
+ knock(invitation: Invitation, method?: string, args?: JsonObject, wanted?: Wanted): Promise<Answer>;
63
+ take(id: string, invitation: Invitation): Promise<string | null>;
64
+ remove(id: string): void;
65
+ } & {
66
+ readonly [id: string]: Standing | undefined;
67
+ };
68
+ export type Occupants = {
69
+ invite(id: string): Promise<Invitation | null>;
70
+ remove(id: string): void;
71
+ };
72
+ export type Stance = {
73
+ readonly cells: Cells;
74
+ readonly occupants: Occupants;
75
+ readonly standings: Standings;
76
+ };
77
+ export interface BeingLike {
78
+ answer(asker: Asker, method?: string, args?: JsonObject): Reply | Promise<Reply>;
79
+ }
80
+ export type BeingClass = new (stance: Stance) => BeingLike;
@@ -0,0 +1,16 @@
1
+ // The ids the ward speaks under, and no being may mint. An asker id is hers:
2
+ // she chooses the strings, and the ward has no business in her namespace. But
3
+ // the ward must sometimes stand at her door itself — its describe asks every
4
+ // being under OWNER — and an id it shares with one of her occupants is two
5
+ // different parties wearing one name. She could not tell them apart, and the
6
+ // ward would shape its describe through an occupant's gate.
7
+ //
8
+ // So the ward's words are shouted, and refused at the mint. A word can only
9
+ // be reserved before anyone has used it: a partition already holding an
10
+ // occupant of that name could never have it taken back. PUBLIC guards nothing
11
+ // today — a public asker is {} and carries no id at all — and that is exactly
12
+ // why it is claimed now, while claiming it is free.
13
+ export const OWNER = 'OWNER';
14
+ export const PUBLIC = 'PUBLIC';
15
+ export const RESERVED_IDS = [OWNER, PUBLIC];
16
+ export const UNREACHED_KEY = Symbol.for('quo.unreached');
@@ -0,0 +1,11 @@
1
+ export declare class Failed extends Error {
2
+ }
3
+ export declare function same(a: unknown, b: unknown): boolean;
4
+ export type Assert = {
5
+ ok(value: unknown, message?: string): asserts value;
6
+ equal(actual: unknown, expected: unknown, message?: string): void;
7
+ notEqual(actual: unknown, expected: unknown, message?: string): void;
8
+ deepEqual(actual: unknown, expected: unknown, message?: string): void;
9
+ match(actual: string, re: RegExp, message?: string): void;
10
+ };
11
+ export declare const assert: Assert;
@@ -0,0 +1,72 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The five assertions the conformance suite makes, and no more. Node has all
3
+ // five in `node:assert/strict`, and naming that module would tie the suite to
4
+ // one terrain -- the suite is the ward's truth, and the ward's truth has to be
5
+ // checkable wherever a ward runs. So they are written here, in the language
6
+ // alone, and `test/assert.test.ts` holds them to Node's own behaviour.
7
+ export class Failed extends Error {
8
+ }
9
+ const show = (v) => {
10
+ if (typeof v === 'symbol')
11
+ return v.toString();
12
+ if (typeof v === 'bigint')
13
+ return `${v}n`;
14
+ if (v === undefined)
15
+ return 'undefined';
16
+ if (typeof v === 'function')
17
+ return `[function ${v.name}]`;
18
+ try {
19
+ return JSON.stringify(v) ?? Object.prototype.toString.call(v);
20
+ }
21
+ catch {
22
+ return Object.prototype.toString.call(v); // a cycle, or a null prototype
23
+ }
24
+ };
25
+ const fail = (why, message) => {
26
+ throw new Failed(message ? `${message}: ${why}` : why);
27
+ };
28
+ // Structural, and strict about shape the way `node:assert/strict` is: a value
29
+ // is not equal to one of another kind, and an array is not equal to an object
30
+ // that happens to hold the same keys.
31
+ export function same(a, b) {
32
+ if (Object.is(a, b))
33
+ return true;
34
+ if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null)
35
+ return false;
36
+ if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b))
37
+ return false;
38
+ if (Array.isArray(a) !== Array.isArray(b))
39
+ return false;
40
+ if (Array.isArray(a) && Array.isArray(b))
41
+ return a.length === b.length && a.every((x, i) => same(x, b[i]));
42
+ if (a instanceof Uint8Array && b instanceof Uint8Array)
43
+ return a.length === b.length && a.every((x, i) => x === b[i]);
44
+ if (a instanceof Map || b instanceof Map || a instanceof Set || b instanceof Set)
45
+ return false; // the suite has none, and a wrong yes is worse than a refusal
46
+ const ka = Reflect.ownKeys(a), kb = Reflect.ownKeys(b);
47
+ if (ka.length !== kb.length)
48
+ return false;
49
+ return ka.every((k) => kb.includes(k) && same(a[k], b[k]));
50
+ }
51
+ export const assert = {
52
+ ok(value, message) {
53
+ if (!value)
54
+ fail(`${show(value)} is not truthy`, message);
55
+ },
56
+ equal(actual, expected, message) {
57
+ if (!Object.is(actual, expected))
58
+ fail(`${show(actual)} !== ${show(expected)}`, message);
59
+ },
60
+ notEqual(actual, expected, message) {
61
+ if (Object.is(actual, expected))
62
+ fail(`${show(actual)} === ${show(expected)}`, message);
63
+ },
64
+ deepEqual(actual, expected, message) {
65
+ if (!same(actual, expected))
66
+ fail(`${show(actual)} is not ${show(expected)}`, message);
67
+ },
68
+ match(actual, re, message) {
69
+ if (typeof actual !== 'string' || !re.test(actual))
70
+ fail(`${show(actual)} does not match ${String(re)}`, message);
71
+ },
72
+ };
@@ -0,0 +1,114 @@
1
+ import { Being } from '../being/being.ts';
2
+ import type { Asker, Blueprint, Invitation, JsonObject, OccupantRecord, Reply, Stance } from '../being/types.ts';
3
+ export declare class Printer extends Being {
4
+ static cells: {
5
+ jobs: JsonObject[];
6
+ asks: JsonObject[];
7
+ };
8
+ static asks: {
9
+ print: {
10
+ input: {
11
+ type: string;
12
+ };
13
+ };
14
+ };
15
+ describe(): Blueprint;
16
+ print({ doc }: JsonObject, asker: Asker): {
17
+ printed: string | number | boolean | import("../being/types.ts").Json[] | {
18
+ [key: string]: import("../being/types.ts").Json;
19
+ } | null;
20
+ };
21
+ }
22
+ export declare class Shop extends Being {
23
+ static cells: {
24
+ printerId: string | null;
25
+ sales: JsonObject[];
26
+ minted: number;
27
+ };
28
+ static asks: {
29
+ hello: {
30
+ input: {
31
+ type: string;
32
+ };
33
+ };
34
+ buy: {
35
+ input: {
36
+ type: string;
37
+ properties: {
38
+ item: {
39
+ type: string;
40
+ };
41
+ };
42
+ };
43
+ };
44
+ refund: {
45
+ input: {
46
+ type: string;
47
+ };
48
+ for: (rec: OccupantRecord | undefined) => boolean;
49
+ };
50
+ };
51
+ answer(asker: Asker, method?: string, args?: JsonObject): Promise<Reply>;
52
+ invite(tier?: string): Promise<Invitation>;
53
+ keepPrinter(invitation: Invitation): Promise<boolean>;
54
+ hello({ invitation }: JsonObject, asker: Asker): Promise<{
55
+ welcome: boolean;
56
+ }>;
57
+ buy({ item }: JsonObject, asker: Asker): Promise<{
58
+ error: string;
59
+ retry?: undefined;
60
+ ok?: undefined;
61
+ receipt?: undefined;
62
+ } | {
63
+ error: string;
64
+ retry: boolean;
65
+ ok?: undefined;
66
+ receipt?: undefined;
67
+ } | {
68
+ error?: undefined;
69
+ retry?: undefined;
70
+ ok: boolean;
71
+ receipt: import("../being/types.ts").Json;
72
+ }>;
73
+ refund(): {
74
+ ok: boolean;
75
+ };
76
+ }
77
+ export declare class Customer extends Being {
78
+ static cells: {
79
+ heard: JsonObject[];
80
+ };
81
+ static asks: {
82
+ hi: {
83
+ input: {
84
+ type: string;
85
+ };
86
+ };
87
+ };
88
+ join(invitation: Invitation): Promise<typeof import("../being/silence.ts").silence | import("../being/types.ts").Unreached | import("../being/types.ts").Json>;
89
+ buy(item: string): Promise<import("../being/types.ts").Answer>;
90
+ learn(): Promise<import("../being/types.ts").Answer>;
91
+ hi(_args: JsonObject, asker: Asker): {
92
+ heard: string;
93
+ };
94
+ }
95
+ export declare class Echo {
96
+ s: Stance;
97
+ constructor(stance: Stance);
98
+ answer(asker: Asker, method?: string, args?: JsonObject): Reply;
99
+ }
100
+ export declare class Member extends Being {
101
+ static cells: {
102
+ heard: JsonObject[];
103
+ };
104
+ static asks: {
105
+ ping: {
106
+ input: {
107
+ type: string;
108
+ };
109
+ };
110
+ };
111
+ ping(_args: JsonObject, asker: Asker): {
112
+ pong: string | null;
113
+ };
114
+ }