@nanobpm/agentic 0.1.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 (228) hide show
  1. package/README.md +22 -0
  2. package/dist/blackboard/family.d.ts +40 -0
  3. package/dist/blackboard/family.js +151 -0
  4. package/dist/blackboard/index.d.ts +21 -0
  5. package/dist/blackboard/index.js +19 -0
  6. package/dist/blackboard/schema.d.ts +30 -0
  7. package/dist/blackboard/schema.js +42 -0
  8. package/dist/blackboard/store.d.ts +138 -0
  9. package/dist/blackboard/store.js +216 -0
  10. package/dist/blackboard/test-db.d.ts +5 -0
  11. package/dist/blackboard/test-db.js +42 -0
  12. package/dist/channel/auth.d.ts +41 -0
  13. package/dist/channel/auth.js +67 -0
  14. package/dist/channel/clock.d.ts +11 -0
  15. package/dist/channel/clock.js +4 -0
  16. package/dist/channel/connection.d.ts +75 -0
  17. package/dist/channel/connection.js +14 -0
  18. package/dist/channel/dispatch.d.ts +46 -0
  19. package/dist/channel/dispatch.js +86 -0
  20. package/dist/channel/hub.d.ts +78 -0
  21. package/dist/channel/hub.js +157 -0
  22. package/dist/channel/index.d.ts +27 -0
  23. package/dist/channel/index.js +20 -0
  24. package/dist/channel/registry.d.ts +68 -0
  25. package/dist/channel/registry.js +84 -0
  26. package/dist/channel/ws-transport.d.ts +23 -0
  27. package/dist/channel/ws-transport.js +178 -0
  28. package/dist/cockpit/boot.d.ts +68 -0
  29. package/dist/cockpit/boot.js +202 -0
  30. package/dist/cockpit/fake-dom.d.ts +37 -0
  31. package/dist/cockpit/fake-dom.js +73 -0
  32. package/dist/cockpit/index.d.ts +27 -0
  33. package/dist/cockpit/index.js +27 -0
  34. package/dist/cockpit/relay-client.d.ts +52 -0
  35. package/dist/cockpit/relay-client.js +192 -0
  36. package/dist/cockpit/render.d.ts +58 -0
  37. package/dist/cockpit/render.js +122 -0
  38. package/dist/cockpit/terminal-session.d.ts +95 -0
  39. package/dist/cockpit/terminal-session.js +123 -0
  40. package/dist/cockpit/view.d.ts +79 -0
  41. package/dist/cockpit/view.js +58 -0
  42. package/dist/demand/c8-rest.d.ts +77 -0
  43. package/dist/demand/c8-rest.js +123 -0
  44. package/dist/demand/index.d.ts +24 -0
  45. package/dist/demand/index.js +24 -0
  46. package/dist/demand/model.d.ts +68 -0
  47. package/dist/demand/model.js +118 -0
  48. package/dist/demand/taskdef.d.ts +40 -0
  49. package/dist/demand/taskdef.js +67 -0
  50. package/dist/index.d.ts +17 -0
  51. package/dist/index.js +17 -0
  52. package/dist/presence/family.d.ts +40 -0
  53. package/dist/presence/family.js +166 -0
  54. package/dist/presence/index.d.ts +19 -0
  55. package/dist/presence/index.js +17 -0
  56. package/dist/presence/schema.d.ts +20 -0
  57. package/dist/presence/schema.js +32 -0
  58. package/dist/presence/store.d.ts +130 -0
  59. package/dist/presence/store.js +191 -0
  60. package/dist/presence/test-db.d.ts +5 -0
  61. package/dist/presence/test-db.js +42 -0
  62. package/dist/protocol/conformance/frames.d.ts +24 -0
  63. package/dist/protocol/conformance/frames.js +116 -0
  64. package/dist/protocol/conformance/index.d.ts +13 -0
  65. package/dist/protocol/conformance/index.js +13 -0
  66. package/dist/protocol/conformance/malformed.d.ts +14 -0
  67. package/dist/protocol/conformance/malformed.js +44 -0
  68. package/dist/protocol/conformance/tokens.d.ts +19 -0
  69. package/dist/protocol/conformance/tokens.js +49 -0
  70. package/dist/protocol/conformance/vocab.d.ts +23 -0
  71. package/dist/protocol/conformance/vocab.js +97 -0
  72. package/dist/protocol/families.d.ts +32 -0
  73. package/dist/protocol/families.js +45 -0
  74. package/dist/protocol/frame.d.ts +45 -0
  75. package/dist/protocol/frame.js +114 -0
  76. package/dist/protocol/hex.d.ts +7 -0
  77. package/dist/protocol/hex.js +26 -0
  78. package/dist/protocol/index.d.ts +23 -0
  79. package/dist/protocol/index.js +23 -0
  80. package/dist/protocol/lanes.d.ts +36 -0
  81. package/dist/protocol/lanes.js +40 -0
  82. package/dist/protocol/payloads.d.ts +64 -0
  83. package/dist/protocol/payloads.js +122 -0
  84. package/dist/protocol/token.d.ts +34 -0
  85. package/dist/protocol/token.js +81 -0
  86. package/dist/protocol/vocab/schema.d.ts +51 -0
  87. package/dist/protocol/vocab/schema.js +218 -0
  88. package/dist/relay/incarnation.d.ts +17 -0
  89. package/dist/relay/incarnation.js +50 -0
  90. package/dist/relay/index.d.ts +25 -0
  91. package/dist/relay/index.js +22 -0
  92. package/dist/relay/relay-family.d.ts +68 -0
  93. package/dist/relay/relay-family.js +272 -0
  94. package/dist/relay/ring.d.ts +49 -0
  95. package/dist/relay/ring.js +105 -0
  96. package/dist/relay/scheduler.d.ts +72 -0
  97. package/dist/relay/scheduler.js +180 -0
  98. package/dist/relay/validate.d.ts +29 -0
  99. package/dist/relay/validate.js +39 -0
  100. package/dist/transcript/index.d.ts +18 -0
  101. package/dist/transcript/index.js +17 -0
  102. package/dist/transcript/schema.d.ts +32 -0
  103. package/dist/transcript/schema.js +48 -0
  104. package/dist/transcript/store.d.ts +192 -0
  105. package/dist/transcript/store.js +347 -0
  106. package/dist/transcript/test-db.d.ts +5 -0
  107. package/dist/transcript/test-db.js +41 -0
  108. package/dist/vocab/core-vocab.d.ts +26 -0
  109. package/dist/vocab/core-vocab.js +67 -0
  110. package/dist/vocab/diversity.d.ts +78 -0
  111. package/dist/vocab/diversity.js +89 -0
  112. package/dist/vocab/index.d.ts +22 -0
  113. package/dist/vocab/index.js +22 -0
  114. package/dist/vocab/merge.d.ts +9 -0
  115. package/dist/vocab/merge.js +104 -0
  116. package/dist/vocab/requires.d.ts +49 -0
  117. package/dist/vocab/requires.js +107 -0
  118. package/dist/vocab/resolver.d.ts +62 -0
  119. package/dist/vocab/resolver.js +149 -0
  120. package/dist/vocab/serve.d.ts +39 -0
  121. package/dist/vocab/serve.js +36 -0
  122. package/package.json +108 -0
  123. package/page/cockpit.css +114 -0
  124. package/page/cockpit.page.json +33 -0
  125. package/page/embed.html +40 -0
  126. package/page/mount.js +78 -0
  127. package/page/standalone.html +43 -0
  128. package/src/blackboard/family.test.ts +280 -0
  129. package/src/blackboard/family.ts +208 -0
  130. package/src/blackboard/index.ts +42 -0
  131. package/src/blackboard/schema.test.ts +60 -0
  132. package/src/blackboard/schema.ts +44 -0
  133. package/src/blackboard/store.test.ts +189 -0
  134. package/src/blackboard/store.ts +331 -0
  135. package/src/blackboard/test-db.ts +47 -0
  136. package/src/channel/auth.test.ts +64 -0
  137. package/src/channel/auth.ts +101 -0
  138. package/src/channel/clock.ts +14 -0
  139. package/src/channel/connection.ts +77 -0
  140. package/src/channel/dispatch.test.ts +83 -0
  141. package/src/channel/dispatch.ts +102 -0
  142. package/src/channel/hub.test.ts +335 -0
  143. package/src/channel/hub.ts +222 -0
  144. package/src/channel/index.ts +55 -0
  145. package/src/channel/registry.test.ts +73 -0
  146. package/src/channel/registry.ts +137 -0
  147. package/src/channel/ws-transport.test.ts +234 -0
  148. package/src/channel/ws-transport.ts +212 -0
  149. package/src/cockpit/boot.test.ts +374 -0
  150. package/src/cockpit/boot.ts +280 -0
  151. package/src/cockpit/fake-dom.ts +90 -0
  152. package/src/cockpit/index.ts +63 -0
  153. package/src/cockpit/relay-client.test.ts +359 -0
  154. package/src/cockpit/relay-client.ts +234 -0
  155. package/src/cockpit/render.test.ts +149 -0
  156. package/src/cockpit/render.ts +194 -0
  157. package/src/cockpit/terminal-session.test.ts +252 -0
  158. package/src/cockpit/terminal-session.ts +194 -0
  159. package/src/cockpit/view.test.ts +117 -0
  160. package/src/cockpit/view.ts +140 -0
  161. package/src/demand/c8-rest.test.ts +140 -0
  162. package/src/demand/c8-rest.ts +167 -0
  163. package/src/demand/index.ts +42 -0
  164. package/src/demand/model.test.ts +197 -0
  165. package/src/demand/model.ts +183 -0
  166. package/src/demand/taskdef.test.ts +85 -0
  167. package/src/demand/taskdef.ts +78 -0
  168. package/src/index.ts +17 -0
  169. package/src/presence/family.test.ts +252 -0
  170. package/src/presence/family.ts +205 -0
  171. package/src/presence/index.ts +26 -0
  172. package/src/presence/schema.test.ts +53 -0
  173. package/src/presence/schema.ts +34 -0
  174. package/src/presence/store.test.ts +190 -0
  175. package/src/presence/store.ts +287 -0
  176. package/src/presence/test-db.test.ts +57 -0
  177. package/src/presence/test-db.ts +47 -0
  178. package/src/protocol/conformance/corpus.test.ts +66 -0
  179. package/src/protocol/conformance/frames.ts +142 -0
  180. package/src/protocol/conformance/index.ts +29 -0
  181. package/src/protocol/conformance/malformed.ts +59 -0
  182. package/src/protocol/conformance/tokens.ts +70 -0
  183. package/src/protocol/conformance/vocab.ts +122 -0
  184. package/src/protocol/families.ts +54 -0
  185. package/src/protocol/frame.test.ts +116 -0
  186. package/src/protocol/frame.ts +171 -0
  187. package/src/protocol/hex.ts +28 -0
  188. package/src/protocol/index.ts +84 -0
  189. package/src/protocol/lanes.test.ts +82 -0
  190. package/src/protocol/lanes.ts +54 -0
  191. package/src/protocol/payloads.test.ts +91 -0
  192. package/src/protocol/payloads.ts +201 -0
  193. package/src/protocol/token.test.ts +57 -0
  194. package/src/protocol/token.ts +123 -0
  195. package/src/protocol/vocab/schema.test.ts +67 -0
  196. package/src/protocol/vocab/schema.ts +281 -0
  197. package/src/relay/incarnation.test.ts +53 -0
  198. package/src/relay/incarnation.ts +54 -0
  199. package/src/relay/index.ts +34 -0
  200. package/src/relay/integration.test.ts +135 -0
  201. package/src/relay/relay-family.test.ts +236 -0
  202. package/src/relay/relay-family.ts +336 -0
  203. package/src/relay/ring.test.ts +138 -0
  204. package/src/relay/ring.ts +136 -0
  205. package/src/relay/scheduler.test.ts +233 -0
  206. package/src/relay/scheduler.ts +208 -0
  207. package/src/relay/validate.test.ts +43 -0
  208. package/src/relay/validate.ts +44 -0
  209. package/src/transcript/index.ts +33 -0
  210. package/src/transcript/integration.test.ts +108 -0
  211. package/src/transcript/schema.test.ts +69 -0
  212. package/src/transcript/schema.ts +51 -0
  213. package/src/transcript/store.test.ts +285 -0
  214. package/src/transcript/store.ts +530 -0
  215. package/src/transcript/test-db.ts +46 -0
  216. package/src/vocab/core-vocab.test.ts +34 -0
  217. package/src/vocab/core-vocab.ts +88 -0
  218. package/src/vocab/diversity.test.ts +153 -0
  219. package/src/vocab/diversity.ts +169 -0
  220. package/src/vocab/index.ts +55 -0
  221. package/src/vocab/merge.test.ts +73 -0
  222. package/src/vocab/merge.ts +117 -0
  223. package/src/vocab/requires.test.ts +69 -0
  224. package/src/vocab/requires.ts +155 -0
  225. package/src/vocab/resolver.test.ts +118 -0
  226. package/src/vocab/resolver.ts +187 -0
  227. package/src/vocab/serve.test.ts +64 -0
  228. package/src/vocab/serve.ts +66 -0
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Language-neutral hex helpers so the conformance corpus can express golden
3
+ * frames as hex strings that any consumer (this repo, jwulf/c8ctl-plugin-nano,
4
+ * a future non-JS client) can decode identically.
5
+ */
6
+ export function bytesToHex(bytes) {
7
+ let out = "";
8
+ for (const byte of bytes) {
9
+ out += byte.toString(16).padStart(2, "0");
10
+ }
11
+ return out;
12
+ }
13
+ export function hexToBytes(hex) {
14
+ if (hex.length % 2 !== 0) {
15
+ throw new Error("hex string must have an even length");
16
+ }
17
+ const out = new Uint8Array(hex.length / 2);
18
+ for (let i = 0; i < out.length; i++) {
19
+ const pair = hex.slice(i * 2, i * 2 + 2);
20
+ if (!/^[0-9a-fA-F]{2}$/.test(pair)) {
21
+ throw new Error(`invalid hex byte at index ${i * 2}`);
22
+ }
23
+ out[i] = Number.parseInt(pair, 16);
24
+ }
25
+ return out;
26
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @nanobpm/agentic/protocol — the wire contract for the Nano agentic protocol
3
+ * (ADR 0056): one app-tier channel carrying agent presence/registry,
4
+ * demand×supply, the blackboard and live terminal relay.
5
+ *
6
+ * This package is the single source of truth every other slice keys off:
7
+ * - the canonical message-family set ({@link MESSAGE_FAMILIES}),
8
+ * - the three QoS lanes ({@link QOS_LANES}),
9
+ * - the frame codec ({@link encodeFrame} / {@link decodeFrame}),
10
+ * - the routing-token grammar ({@link parseToken}),
11
+ * - the vocab-artifact schema ({@link validateVocabDocument}), and
12
+ * - per-family payload contracts ({@link validatePayload}).
13
+ *
14
+ * The shared conformance corpus is published at the
15
+ * `@nanobpm/agentic/protocol/conformance` subpath.
16
+ */
17
+ export { MESSAGE_FAMILIES, FAMILY_CODES, familyForCode, isMessageFamily, type MessageFamily, } from "./families.ts";
18
+ export { QOS_LANES, LANE_CODES, laneForCode, lanePriority, isQosLane, compareFrameOrder, type QosLane, } from "./lanes.ts";
19
+ export { encodeFrame, decodeFrame, FrameDecodeError, FrameEncodeError, FRAME_MAGIC, FRAME_VERSION, FRAME_HEADER_BYTES, MAX_SEQ, type Frame, type FrameDecodeErrorCode, type FrameEncodeErrorCode, } from "./frame.ts";
20
+ export { parseToken, formatToken, isValidToken, isSegmentName, isSeatLabel, TokenParseError, type RoutingToken, type TokenParseErrorCode, } from "./token.ts";
21
+ export { validateVocabDocument, type VocabDocument, type VocabNetwork, type VocabRole, type VocabError, type VocabValidationResult, } from "./vocab/schema.ts";
22
+ export { validatePayload, type Capability, type RegisterPayload, type HeartbeatPayload, type DeregisterPayload, type ServePayload, type DemandPayload, type BlackboardPayload, type BlackboardOp, type RelayPayload, type PayloadError, type PayloadValidationResult, } from "./payloads.ts";
23
+ export { bytesToHex, hexToBytes } from "./hex.ts";
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @nanobpm/agentic/protocol — the wire contract for the Nano agentic protocol
3
+ * (ADR 0056): one app-tier channel carrying agent presence/registry,
4
+ * demand×supply, the blackboard and live terminal relay.
5
+ *
6
+ * This package is the single source of truth every other slice keys off:
7
+ * - the canonical message-family set ({@link MESSAGE_FAMILIES}),
8
+ * - the three QoS lanes ({@link QOS_LANES}),
9
+ * - the frame codec ({@link encodeFrame} / {@link decodeFrame}),
10
+ * - the routing-token grammar ({@link parseToken}),
11
+ * - the vocab-artifact schema ({@link validateVocabDocument}), and
12
+ * - per-family payload contracts ({@link validatePayload}).
13
+ *
14
+ * The shared conformance corpus is published at the
15
+ * `@nanobpm/agentic/protocol/conformance` subpath.
16
+ */
17
+ export { MESSAGE_FAMILIES, FAMILY_CODES, familyForCode, isMessageFamily, } from "./families.js";
18
+ export { QOS_LANES, LANE_CODES, laneForCode, lanePriority, isQosLane, compareFrameOrder, } from "./lanes.js";
19
+ export { encodeFrame, decodeFrame, FrameDecodeError, FrameEncodeError, FRAME_MAGIC, FRAME_VERSION, FRAME_HEADER_BYTES, MAX_SEQ, } from "./frame.js";
20
+ export { parseToken, formatToken, isValidToken, isSegmentName, isSeatLabel, TokenParseError, } from "./token.js";
21
+ export { validateVocabDocument, } from "./vocab/schema.js";
22
+ export { validatePayload, } from "./payloads.js";
23
+ export { bytesToHex, hexToBytes } from "./hex.js";
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The three QoS lanes carried on the one agentic channel, in STRICT priority
3
+ * order: control/facts > interactive > bulk.
4
+ *
5
+ * The ordering is the contract: a bulk-output storm (relay chunks) must never
6
+ * head-of-line-block a heartbeat or a blackboard write. The scheduler that
7
+ * enforces this lives in S5; this module owns the canonical lane set, their
8
+ * wire codes, and the priority comparison every scheduler derives from.
9
+ */
10
+ export declare const QOS_LANES: readonly ["control", "interactive", "bulk"];
11
+ export type QosLane = (typeof QOS_LANES)[number];
12
+ export declare function isQosLane(value: unknown): value is QosLane;
13
+ /**
14
+ * On-wire lane codes. The numeric value is also the priority rank: a LOWER
15
+ * code is HIGHER priority (control=0 outranks bulk=2). Do not renumber.
16
+ */
17
+ export declare const LANE_CODES: {
18
+ readonly control: 0;
19
+ readonly interactive: 1;
20
+ readonly bulk: 2;
21
+ };
22
+ export declare function laneForCode(code: number): QosLane | undefined;
23
+ /** Priority rank of a lane; lower is dispatched first. */
24
+ export declare function lanePriority(lane: QosLane): number;
25
+ /**
26
+ * Canonical scheduler ordering derived from the lane priorities: control before
27
+ * interactive before bulk, ties broken by ascending `seq` so a single lane
28
+ * drains in emission order. Returns <0 when `a` should be dispatched before `b`.
29
+ */
30
+ export declare function compareFrameOrder(a: {
31
+ readonly lane: QosLane;
32
+ readonly seq: number;
33
+ }, b: {
34
+ readonly lane: QosLane;
35
+ readonly seq: number;
36
+ }): number;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The three QoS lanes carried on the one agentic channel, in STRICT priority
3
+ * order: control/facts > interactive > bulk.
4
+ *
5
+ * The ordering is the contract: a bulk-output storm (relay chunks) must never
6
+ * head-of-line-block a heartbeat or a blackboard write. The scheduler that
7
+ * enforces this lives in S5; this module owns the canonical lane set, their
8
+ * wire codes, and the priority comparison every scheduler derives from.
9
+ */
10
+ export const QOS_LANES = ["control", "interactive", "bulk"];
11
+ const LANE_SET = new Set(QOS_LANES);
12
+ export function isQosLane(value) {
13
+ return typeof value === "string" && LANE_SET.has(value);
14
+ }
15
+ /**
16
+ * On-wire lane codes. The numeric value is also the priority rank: a LOWER
17
+ * code is HIGHER priority (control=0 outranks bulk=2). Do not renumber.
18
+ */
19
+ export const LANE_CODES = {
20
+ control: 0,
21
+ interactive: 1,
22
+ bulk: 2,
23
+ };
24
+ const CODE_TO_LANE = new Map(QOS_LANES.map((lane) => [LANE_CODES[lane], lane]));
25
+ export function laneForCode(code) {
26
+ return CODE_TO_LANE.get(code);
27
+ }
28
+ /** Priority rank of a lane; lower is dispatched first. */
29
+ export function lanePriority(lane) {
30
+ return LANE_CODES[lane];
31
+ }
32
+ /**
33
+ * Canonical scheduler ordering derived from the lane priorities: control before
34
+ * interactive before bulk, ties broken by ascending `seq` so a single lane
35
+ * drains in emission order. Returns <0 when `a` should be dispatched before `b`.
36
+ */
37
+ export function compareFrameOrder(a, b) {
38
+ const byLane = lanePriority(a.lane) - lanePriority(b.lane);
39
+ return byLane !== 0 ? byLane : a.seq - b.seq;
40
+ }
@@ -0,0 +1,64 @@
1
+ import { type MessageFamily } from "./families.ts";
2
+ /**
3
+ * Minimal per-family payload contract. This is the wire-level shape every
4
+ * family module (S1 hub, S2 presence, S3 vocab, S5 relay, S7 blackboard) builds
5
+ * on — the single source of truth for the payload of each message family, held
6
+ * to by both this repo's codec and the c8ctl client.
7
+ *
8
+ * Validators are STRUCTURAL and forward-compatible: they require the core
9
+ * fields with correct types and otherwise tolerate additional properties, so a
10
+ * later slice may enrich a payload without breaking older peers.
11
+ *
12
+ * Capability (cognition/weight/family/host) travels on `register` as an
13
+ * enrolment attribute — it is NEVER a routing token.
14
+ */
15
+ export interface Capability {
16
+ readonly cognition?: string;
17
+ readonly weight?: number;
18
+ readonly family?: string;
19
+ readonly host?: string;
20
+ }
21
+ export interface RegisterPayload {
22
+ readonly instance: string;
23
+ readonly capability: Capability;
24
+ }
25
+ export interface HeartbeatPayload {
26
+ readonly instance: string;
27
+ }
28
+ export interface DeregisterPayload {
29
+ readonly instance: string;
30
+ readonly reason?: string;
31
+ }
32
+ export interface ServePayload {
33
+ readonly instance: string;
34
+ readonly tokens: readonly string[];
35
+ }
36
+ export interface DemandPayload {
37
+ readonly network: string;
38
+ readonly missing: readonly string[];
39
+ }
40
+ export type BlackboardOp = "append" | "read";
41
+ export interface BlackboardPayload {
42
+ readonly op: BlackboardOp;
43
+ readonly dedupeKey?: string;
44
+ readonly since?: number;
45
+ }
46
+ export interface RelayPayload {
47
+ readonly stream: string;
48
+ readonly offset: number;
49
+ readonly chunk: string;
50
+ }
51
+ export interface PayloadError {
52
+ readonly code: string;
53
+ readonly message: string;
54
+ }
55
+ export type PayloadValidationResult = {
56
+ readonly ok: true;
57
+ } | {
58
+ readonly ok: false;
59
+ readonly errors: readonly PayloadError[];
60
+ };
61
+ /**
62
+ * Validate a decoded payload against its family's minimal contract.
63
+ */
64
+ export declare function validatePayload(family: MessageFamily, payload: unknown): PayloadValidationResult;
@@ -0,0 +1,122 @@
1
+ import { isMessageFamily } from "./families.js";
2
+ import { isValidToken } from "./token.js";
3
+ function isPlainObject(value) {
4
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5
+ }
6
+ function nonEmptyString(value) {
7
+ return typeof value === "string" && value.length > 0;
8
+ }
9
+ function validateRegister(p, errors) {
10
+ if (!nonEmptyString(p.instance)) {
11
+ errors.push({ code: "bad-instance", message: "register.instance must be a non-empty string" });
12
+ }
13
+ if (!isPlainObject(p.capability)) {
14
+ errors.push({ code: "bad-capability", message: "register.capability must be an object" });
15
+ return;
16
+ }
17
+ const cap = p.capability;
18
+ for (const field of ["cognition", "family", "host"]) {
19
+ if (field in cap && typeof cap[field] !== "string") {
20
+ errors.push({ code: "bad-capability", message: `register.capability.${field} must be a string when present` });
21
+ }
22
+ }
23
+ if ("weight" in cap && typeof cap.weight !== "number") {
24
+ errors.push({ code: "bad-capability", message: "register.capability.weight must be a number when present" });
25
+ }
26
+ }
27
+ function validateInstanceOnly(family, p, errors) {
28
+ if (!nonEmptyString(p.instance)) {
29
+ errors.push({ code: "bad-instance", message: `${family}.instance must be a non-empty string` });
30
+ }
31
+ }
32
+ function validateDeregister(p, errors) {
33
+ validateInstanceOnly("deregister", p, errors);
34
+ if ("reason" in p && typeof p.reason !== "string") {
35
+ errors.push({ code: "bad-reason", message: "deregister.reason must be a string when present" });
36
+ }
37
+ }
38
+ function validateServe(p, errors) {
39
+ if (!nonEmptyString(p.instance)) {
40
+ errors.push({ code: "bad-instance", message: "serve.instance must be a non-empty string" });
41
+ }
42
+ const tokens = p.tokens;
43
+ if (!Array.isArray(tokens)) {
44
+ errors.push({ code: "bad-tokens", message: "serve.tokens must be an array" });
45
+ return;
46
+ }
47
+ tokens.forEach((token, index) => {
48
+ if (typeof token !== "string" || !isValidToken(token)) {
49
+ errors.push({ code: "bad-token", message: `serve.tokens[${index}] is not a valid routing token` });
50
+ }
51
+ });
52
+ }
53
+ function validateDemand(p, errors) {
54
+ if (!nonEmptyString(p.network)) {
55
+ errors.push({ code: "bad-network", message: "demand.network must be a non-empty string" });
56
+ }
57
+ const missing = p.missing;
58
+ if (!Array.isArray(missing) || !missing.every((entry) => typeof entry === "string")) {
59
+ errors.push({ code: "bad-missing", message: "demand.missing must be an array of strings" });
60
+ }
61
+ }
62
+ function validateBlackboard(p, errors) {
63
+ if (p.op !== "append" && p.op !== "read") {
64
+ errors.push({ code: "bad-op", message: "blackboard.op must be 'append' or 'read'" });
65
+ }
66
+ if ("dedupeKey" in p && typeof p.dedupeKey !== "string") {
67
+ errors.push({ code: "bad-dedupe-key", message: "blackboard.dedupeKey must be a string when present" });
68
+ }
69
+ if ("since" in p && (typeof p.since !== "number" || !Number.isInteger(p.since) || p.since < 0)) {
70
+ errors.push({ code: "bad-since", message: "blackboard.since must be a non-negative integer when present" });
71
+ }
72
+ if ("files" in p && (!Array.isArray(p.files) || !p.files.every((f) => typeof f === "string"))) {
73
+ errors.push({ code: "bad-files", message: "blackboard.files must be an array of strings when present" });
74
+ }
75
+ }
76
+ function validateRelay(p, errors) {
77
+ if (!nonEmptyString(p.stream)) {
78
+ errors.push({ code: "bad-stream", message: "relay.stream must be a non-empty string" });
79
+ }
80
+ if (typeof p.offset !== "number" || !Number.isInteger(p.offset) || p.offset < 0) {
81
+ errors.push({ code: "bad-offset", message: "relay.offset must be a non-negative integer" });
82
+ }
83
+ if (typeof p.chunk !== "string") {
84
+ errors.push({ code: "bad-chunk", message: "relay.chunk must be a string" });
85
+ }
86
+ }
87
+ /**
88
+ * Validate a decoded payload against its family's minimal contract.
89
+ */
90
+ export function validatePayload(family, payload) {
91
+ if (!isMessageFamily(family)) {
92
+ return { ok: false, errors: [{ code: "unknown-family", message: `unknown family: ${String(family)}` }] };
93
+ }
94
+ if (!isPlainObject(payload)) {
95
+ return { ok: false, errors: [{ code: "not-object", message: `${family} payload must be an object` }] };
96
+ }
97
+ const errors = [];
98
+ switch (family) {
99
+ case "register":
100
+ validateRegister(payload, errors);
101
+ break;
102
+ case "heartbeat":
103
+ validateInstanceOnly("heartbeat", payload, errors);
104
+ break;
105
+ case "deregister":
106
+ validateDeregister(payload, errors);
107
+ break;
108
+ case "serve":
109
+ validateServe(payload, errors);
110
+ break;
111
+ case "demand":
112
+ validateDemand(payload, errors);
113
+ break;
114
+ case "blackboard":
115
+ validateBlackboard(payload, errors);
116
+ break;
117
+ case "relay":
118
+ validateRelay(payload, errors);
119
+ break;
120
+ }
121
+ return errors.length === 0 ? { ok: true } : { ok: false, errors };
122
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Routing-token grammar: `network[.subnetwork…].role[#seat]`.
3
+ *
4
+ * The token is what the engine matches 1:1. Capability — cognition, weight,
5
+ * family, host — is NEVER encoded in the token; it is an enrolment attribute
6
+ * and a registry gate. Do not smuggle capability into a segment.
7
+ *
8
+ * Grammar:
9
+ * token = segment ("." segment)* ("#" seat)?
10
+ * segment = [a-z][a-z0-9-]* (first is the network, last is the role,
11
+ * any in between are subnetworks)
12
+ * seat = [a-z0-9-]+
13
+ *
14
+ * A single-segment token (e.g. `decide`) is a bare role with no network and no
15
+ * subnetworks (`network` is absent).
16
+ */
17
+ export interface RoutingToken {
18
+ readonly network?: string;
19
+ readonly subnetworks: readonly string[];
20
+ readonly role: string;
21
+ readonly seat?: string;
22
+ }
23
+ /** True when `name` is a valid network/subnetwork/role segment. */
24
+ export declare function isSegmentName(name: string): boolean;
25
+ /** True when `seat` is a valid seat label. */
26
+ export declare function isSeatLabel(seat: string): boolean;
27
+ export type TokenParseErrorCode = "empty" | "whitespace" | "multiple-seat-markers" | "empty-seat" | "bad-seat" | "empty-segment" | "bad-segment";
28
+ export declare class TokenParseError extends Error {
29
+ readonly code: TokenParseErrorCode;
30
+ constructor(code: TokenParseErrorCode, message: string);
31
+ }
32
+ export declare function parseToken(token: string): RoutingToken;
33
+ export declare function isValidToken(token: string): boolean;
34
+ export declare function formatToken(token: RoutingToken): string;
@@ -0,0 +1,81 @@
1
+ const SEGMENT_RE = /^[a-z][a-z0-9-]*$/;
2
+ const SEAT_RE = /^[a-z0-9-]+$/;
3
+ /** True when `name` is a valid network/subnetwork/role segment. */
4
+ export function isSegmentName(name) {
5
+ return SEGMENT_RE.test(name);
6
+ }
7
+ /** True when `seat` is a valid seat label. */
8
+ export function isSeatLabel(seat) {
9
+ return SEAT_RE.test(seat);
10
+ }
11
+ export class TokenParseError extends Error {
12
+ code;
13
+ constructor(code, message) {
14
+ super(message);
15
+ this.name = "TokenParseError";
16
+ this.code = code;
17
+ }
18
+ }
19
+ export function parseToken(token) {
20
+ if (token.length === 0) {
21
+ throw new TokenParseError("empty", "routing token must not be empty");
22
+ }
23
+ if (/\s/.test(token)) {
24
+ throw new TokenParseError("whitespace", "routing token must not contain whitespace");
25
+ }
26
+ const hashParts = token.split("#");
27
+ if (hashParts.length > 2) {
28
+ throw new TokenParseError("multiple-seat-markers", "routing token has more than one '#'");
29
+ }
30
+ const pathPart = hashParts[0];
31
+ const seatPart = hashParts.length === 2 ? hashParts[1] : undefined;
32
+ if (seatPart !== undefined) {
33
+ if (seatPart.length === 0) {
34
+ throw new TokenParseError("empty-seat", "seat after '#' must not be empty");
35
+ }
36
+ if (!isSeatLabel(seatPart)) {
37
+ throw new TokenParseError("bad-seat", `invalid seat label: ${seatPart}`);
38
+ }
39
+ }
40
+ const segments = pathPart.split(".");
41
+ for (const segment of segments) {
42
+ if (segment.length === 0) {
43
+ throw new TokenParseError("empty-segment", "routing token has an empty '.' segment");
44
+ }
45
+ if (!isSegmentName(segment)) {
46
+ throw new TokenParseError("bad-segment", `invalid token segment: ${segment}`);
47
+ }
48
+ }
49
+ const role = segments[segments.length - 1];
50
+ const base = segments.length === 1
51
+ ? { subnetworks: [], role }
52
+ : { network: segments[0], subnetworks: segments.slice(1, -1), role };
53
+ return seatPart === undefined ? base : { ...base, seat: seatPart };
54
+ }
55
+ export function isValidToken(token) {
56
+ try {
57
+ parseToken(token);
58
+ return true;
59
+ }
60
+ catch {
61
+ return false;
62
+ }
63
+ }
64
+ export function formatToken(token) {
65
+ if (token.network === undefined && token.subnetworks.length > 0) {
66
+ throw new Error("invalid RoutingToken: subnetworks present without a network");
67
+ }
68
+ const segments = token.network === undefined
69
+ ? [token.role]
70
+ : [token.network, ...token.subnetworks, token.role];
71
+ for (const segment of segments) {
72
+ if (!isSegmentName(segment)) {
73
+ throw new Error(`invalid RoutingToken: invalid segment: ${segment}`);
74
+ }
75
+ }
76
+ if (token.seat !== undefined && !isSeatLabel(token.seat)) {
77
+ throw new Error(`invalid RoutingToken: invalid seat label: ${token.seat}`);
78
+ }
79
+ const path = segments.join(".");
80
+ return token.seat === undefined ? path : `${path}#${token.seat}`;
81
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * The versioned vocab artifact: the capability→token map applied over the
3
+ * channel (REGISTER → SERVE). It is authoritative and out-of-band from any
4
+ * worker — no capability→token map is ever baked into a worker.
5
+ *
6
+ * Core vocabulary ships opinionated and works out of the box; authors extend it
7
+ * by adding networks/subnetworks/roles in THIS SAME schema (there is no second
8
+ * schema for extensions).
9
+ *
10
+ * Per-role attributes:
11
+ * - `requires` — enrolment capability requirements (the registry
12
+ * gate). These gate WHO may fill the role; they are
13
+ * never encoded in the routing token.
14
+ * - `weight` — cognition weight for the role.
15
+ * - `seats` — either a seat count (integer ≥ 0) or an explicit
16
+ * list of named seats (each a valid seat label).
17
+ * - `seatsDistinctFamily` — diversity SLO: when true, seats of this role must
18
+ * be filled by distinct families (e.g. #red ≠ #blue).
19
+ */
20
+ export interface VocabRole {
21
+ readonly requires?: readonly string[];
22
+ readonly weight?: number;
23
+ readonly seats?: number | readonly string[];
24
+ readonly seatsDistinctFamily?: boolean;
25
+ }
26
+ export interface VocabNetwork {
27
+ readonly roles?: Readonly<Record<string, VocabRole>>;
28
+ readonly subnetworks?: Readonly<Record<string, VocabNetwork>>;
29
+ }
30
+ export interface VocabDocument {
31
+ readonly version: number;
32
+ readonly networks: Readonly<Record<string, VocabNetwork>>;
33
+ }
34
+ export interface VocabError {
35
+ readonly path: string;
36
+ readonly code: string;
37
+ readonly message: string;
38
+ }
39
+ export type VocabValidationResult = {
40
+ readonly ok: true;
41
+ readonly value: VocabDocument;
42
+ } | {
43
+ readonly ok: false;
44
+ readonly errors: readonly VocabError[];
45
+ };
46
+ /**
47
+ * Validate an unknown value against the vocab-artifact schema. On success the
48
+ * returned `value` is a newly constructed {@link VocabDocument} normalized from
49
+ * the input — it does not share referential identity with `input`.
50
+ */
51
+ export declare function validateVocabDocument(input: unknown): VocabValidationResult;