@quo-systems/quo 0.1.0 → 0.2.0

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 (79) hide show
  1. package/README.md +9 -6
  2. package/SPEC.md +235 -137
  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 +10 -0
  10. package/dist/being/silence.js +29 -0
  11. package/dist/being/types.d.ts +84 -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 +316 -0
  19. package/dist/conformance/index.d.ts +65 -0
  20. package/dist/conformance/index.js +448 -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 +46 -0
  26. package/dist/harbor/core.js +218 -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 +24 -0
  32. package/dist/harbor/memory.js +68 -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 +16 -0
  44. package/dist/ward/door.js +127 -0
  45. package/dist/ward/ground.d.ts +15 -0
  46. package/dist/ward/ground.js +1 -0
  47. package/dist/ward/heirs.d.ts +14 -0
  48. package/dist/ward/heirs.js +102 -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 +116 -0
  53. package/dist/ward/partition.d.ts +57 -0
  54. package/dist/ward/partition.js +64 -0
  55. package/dist/ward/seal.d.ts +50 -0
  56. package/dist/ward/seal.js +109 -0
  57. package/dist/ward/stance.d.ts +19 -0
  58. package/dist/ward/stance.js +259 -0
  59. package/dist/ward/ward.d.ts +2 -0
  60. package/dist/ward/ward.js +197 -0
  61. package/package.json +21 -29
  62. package/src/being/index.ts +1 -1
  63. package/src/being/silence.ts +26 -8
  64. package/src/being/types.ts +13 -3
  65. package/src/conformance/beings.ts +5 -5
  66. package/src/conformance/estate.ts +11 -6
  67. package/src/conformance/index.ts +14 -12
  68. package/src/harbor/core.ts +56 -6
  69. package/src/harbor/dial.ts +1 -1
  70. package/src/harbor/memory.ts +6 -1
  71. package/src/ward/door.ts +54 -32
  72. package/src/ward/ground.ts +6 -2
  73. package/src/ward/heirs.ts +20 -5
  74. package/src/ward/owner.ts +4 -3
  75. package/src/ward/partition.ts +10 -0
  76. package/src/ward/seal.ts +8 -5
  77. package/src/ward/stance.ts +20 -14
  78. package/src/ward/ward.ts +9 -9
  79. package/vectors/framing.json +7 -1
@@ -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, word, isWord, wordOf, DOOR_WORDS, isDoorWord } 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, word, isWord, wordOf, DOOR_WORDS, isDoorWord } from './silence.js';
5
+ export { digest, canonical } from './digest.js';
6
+ export { OWNER, PUBLIC, RESERVED_IDS } from './types.js';
@@ -0,0 +1,10 @@
1
+ import { type DoorWord, type Unreached, type Word, type WordName } 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 DOOR_WORDS: readonly DoorWord[];
5
+ export declare const isDoorWord: (s: unknown) => s is DoorWord;
6
+ export declare const word: <W extends WordName>(name: W) => Word<W>;
7
+ export declare const isWord: (x: unknown) => x is Word;
8
+ export declare const wordOf: (x: Word) => WordName;
9
+ export declare const unreached: () => Unreached;
10
+ export declare const isUnreached: (x: unknown) => x is Unreached;
@@ -0,0 +1,29 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Silence is one distinguished value. Null is an answer. Silence is no answer.
3
+ //
4
+ // The ward's words are the other distinguished values: what her ward tells
5
+ // her when no object came back and it knows why. Each is one frozen object
6
+ // under one symbol key, so a being cannot make one by accident, none of them
7
+ // crosses an edge as a value, and a being who compares against them needs
8
+ // no fourth shape. `unreached` is one of them.
9
+ import { WORD_KEY } from './types.js';
10
+ export const silence = Symbol.for('quo.silence');
11
+ export const isSilence = (x) => x === silence;
12
+ // The words a door says to a key it has bound, and no other. They ride on
13
+ // the wire as `{ quo: word }`, so a reply is checked against this list.
14
+ export const DOOR_WORDS = ['removed', 'absent', 'unannounced', 'repeated', 'threw'];
15
+ export const isDoorWord = (s) => typeof s === 'string' && DOOR_WORDS.includes(s);
16
+ const WORDS = new Map();
17
+ export const word = (name) => {
18
+ let w = WORDS.get(name);
19
+ if (!w)
20
+ WORDS.set(name, (w = Object.freeze({ [WORD_KEY]: name })));
21
+ return w;
22
+ };
23
+ export const isWord = (x) => x !== null && typeof x === 'object' && typeof x[WORD_KEY] === 'string';
24
+ export const wordOf = (x) => x[WORD_KEY];
25
+ // Unreached: no far door was reached. Nothing is known to have been
26
+ // delivered, so asking again is safe. A being cannot produce it: a ward that
27
+ // sees a word come out of a being reads it as her having thrown.
28
+ export const unreached = () => word('unreached');
29
+ export const isUnreached = (x) => isWord(x) && wordOf(x) === 'unreached';
@@ -0,0 +1,84 @@
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 DoorWord = 'removed' | 'absent' | 'unannounced' | 'repeated' | 'threw';
49
+ export type WardWord = 'unreached' | 'late' | 'invitation' | 'dropped';
50
+ export type WordName = DoorWord | WardWord;
51
+ export declare const WORD_KEY: unique symbol;
52
+ export type Word<W extends WordName = WordName> = {
53
+ readonly [K in typeof WORD_KEY]: W;
54
+ };
55
+ export type Unreached = Word<'unreached'>;
56
+ export type Answer = Json | Silence | Word;
57
+ export type Reply = Json | Silence;
58
+ export type Wanted = {
59
+ time?: number;
60
+ };
61
+ export type Standing = {
62
+ readonly id: string;
63
+ ask(method?: string, args?: JsonObject, wanted?: Wanted): Promise<Answer>;
64
+ };
65
+ export type Standings = {
66
+ knock(invitation: Invitation, method?: string, args?: JsonObject, wanted?: Wanted): Promise<Answer>;
67
+ take(id: string, invitation: Invitation): Promise<string | null>;
68
+ remove(id: string): void;
69
+ } & {
70
+ readonly [id: string]: Standing | undefined;
71
+ };
72
+ export type Occupants = {
73
+ invite(id: string): Promise<Invitation | null>;
74
+ remove(id: string): void;
75
+ };
76
+ export type Stance = {
77
+ readonly cells: Cells;
78
+ readonly occupants: Occupants;
79
+ readonly standings: Standings;
80
+ };
81
+ export interface BeingLike {
82
+ answer(asker: Asker, method?: string, args?: JsonObject): Reply | Promise<Reply>;
83
+ }
84
+ 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 WORD_KEY = Symbol.for('quo.word');
@@ -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").Word | 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
+ }
@@ -0,0 +1,126 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The fixture beings. Fixed, so that the ward is what varies. A printer, a
3
+ // shop, a customer on the base class, and one raw being with no base at all.
4
+ import { Being } from '../being/being.js';
5
+ import { isSilence, isUnreached, isWord } from '../being/silence.js';
6
+ // A printer. Invites whom she is told to, prints for her occupants. Her
7
+ // blueprint lives in her cells so a test can change her shape.
8
+ export class Printer extends Being {
9
+ static cells = { jobs: [], asks: [{ name: 'print', input: { type: 'object' } }] };
10
+ static asks = { print: { input: { type: 'object' } } };
11
+ describe() {
12
+ return { asks: this.cells.asks, notes: { model: 'LX-2' } };
13
+ }
14
+ print({ doc }, asker) {
15
+ this.cells.jobs.push({ by: asker.id ?? null, doc: doc ?? null });
16
+ return { printed: doc ?? null };
17
+ }
18
+ }
19
+ // A shop. Invites customers in her own time. Keeps a printer as a standing.
20
+ // Takes a gold customer back if the customer offers a way. Counts every ask
21
+ // per occupant, by wrapping answer.
22
+ export class Shop extends Being {
23
+ static cells = { printerId: null, sales: [], minted: 0 };
24
+ static asks = {
25
+ hello: { input: { type: 'object' } },
26
+ buy: { input: { type: 'object', properties: { item: { type: 'string' } } } },
27
+ refund: { input: { type: 'object' }, for: (rec) => rec?.notes.tier === 'gold' },
28
+ };
29
+ async answer(asker, method, args) {
30
+ const rec = this.occupant(asker);
31
+ if (rec && method !== undefined)
32
+ rec.notes.asks = (Number(rec.notes.asks) || 0) + 1;
33
+ return super.answer(asker, method, args);
34
+ }
35
+ // Outside any ask. She mints, her ward seals, she hands it out by mail.
36
+ async invite(tier = 'plain') {
37
+ const id = `cust${++this.cells.minted}`;
38
+ const inv = (await super.invite(id));
39
+ this.cells.occupants[id].notes.tier = tier;
40
+ return inv;
41
+ }
42
+ async keepPrinter(invitation) {
43
+ const bp = await this.knock(invitation);
44
+ if (isSilence(bp) || isWord(bp))
45
+ return false;
46
+ this.cells.printerId = await this.take('prn', invitation);
47
+ return true;
48
+ }
49
+ async hello({ invitation }, asker) {
50
+ const rec = this.occupant(asker);
51
+ if (rec?.notes.tier === 'gold' && invitation) {
52
+ const back = await this.knock(invitation, 'hi');
53
+ if (!isSilence(back) && !isWord(back))
54
+ await this.take(`vip-${asker.id}`, invitation);
55
+ }
56
+ return { welcome: true };
57
+ }
58
+ async buy({ item }, asker) {
59
+ if (typeof item !== 'string')
60
+ return { error: 'no such item' }; // args are hers to check
61
+ const id = this.cells.printerId;
62
+ const printer = id ? this.standings[id] : undefined;
63
+ if (!printer)
64
+ return { error: 'no printer' };
65
+ const out = await printer.ask('print', { doc: `receipt for ${item}` });
66
+ if (isUnreached(out))
67
+ return { error: 'printer unreachable', retry: true };
68
+ if (isSilence(out) || isWord(out))
69
+ return { error: 'printer refused' };
70
+ const st = this.cells.standings[id];
71
+ if (st.seen !== st.digest)
72
+ this.occupant(asker).notes.printerChanged = true;
73
+ this.cells.sales.push({ item: item ?? null, by: asker.id ?? null });
74
+ return { ok: true, receipt: out.printed };
75
+ }
76
+ refund() {
77
+ return { ok: true };
78
+ }
79
+ }
80
+ // A customer. Consumes an invitation, and only then decides to keep the
81
+ // shop. Offers the shop a way back so the shop can reach her.
82
+ export class Customer extends Being {
83
+ static cells = { heard: [] };
84
+ static asks = { hi: { input: { type: 'object' } } };
85
+ async join(invitation) {
86
+ const mine = (await this.invite('shop-back'));
87
+ const out = await this.knock(invitation, 'hello', { invitation: mine });
88
+ if (isSilence(out) || isWord(out))
89
+ return out; // nothing was born
90
+ await this.take('shop', invitation); // now, and only now
91
+ return out;
92
+ }
93
+ buy(item) {
94
+ return this.standings.shop.ask('buy', { item });
95
+ }
96
+ learn() {
97
+ return this.standings.shop.ask();
98
+ }
99
+ hi(_args, asker) {
100
+ this.cells.heard.push({ from: asker.id ?? null, method: 'hi' });
101
+ return { heard: 'hi' };
102
+ }
103
+ }
104
+ // The raw shape. No base class, no import from the kit. A being all the same.
105
+ export class Echo {
106
+ s;
107
+ constructor(stance) {
108
+ this.s = stance;
109
+ }
110
+ answer(asker, method, args = {}) {
111
+ if (method === undefined)
112
+ return { asks: [{ name: 'echo', input: {} }], notes: {} };
113
+ return { from: asker.id ?? null, ...args };
114
+ }
115
+ }
116
+ // A member of an estate. Nothing scripted: she invites whom she is told to,
117
+ // knocks where she is told to, and answers `ping` with the asker's own name.
118
+ // The estate chapter drives her, and what it drives is the graph, not her.
119
+ export class Member extends Being {
120
+ static cells = { heard: [] };
121
+ static asks = { ping: { input: { type: 'object' } } };
122
+ ping(_args, asker) {
123
+ this.cells.heard.push({ from: asker.id ?? null });
124
+ return { pong: asker.id ?? null };
125
+ }
126
+ }
@@ -0,0 +1,5 @@
1
+ import type { Runner, World } from './index.ts';
2
+ export declare function estate(label: string, make: () => Promise<World>, { canDown, test }: {
3
+ canDown: boolean;
4
+ test: Runner;
5
+ }): void;