@fitzzero/quickdraw-core 5.0.1 → 5.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 (60) hide show
  1. package/README.md +303 -32
  2. package/dist/{admin-CBlTY0do.d.ts → admin-DKwkIh5i.d.ts} +2 -2
  3. package/dist/{adminSchemas-zji11V74.d.ts → adminSchemas-2DUA_hZ8.d.ts} +1 -1
  4. package/dist/{auth-CMyRkfjZ.d.ts → auth-BhWqdyb-.d.ts} +2 -2
  5. package/dist/{caller-BqCYnpmC.d.ts → caller-CXTFswBO.d.ts} +27 -5
  6. package/dist/{chunk-2XJFNVHW.js → chunk-2PWMUVC7.js} +1 -1
  7. package/dist/{chunk-2XJFNVHW.js.map → chunk-2PWMUVC7.js.map} +1 -1
  8. package/dist/{chunk-O2PWC2YW.js → chunk-OECZPRPW.js} +3 -3
  9. package/dist/{chunk-3JNMVB2V.js → chunk-QCHKWIRS.js} +2 -2
  10. package/dist/{chunk-5YRQ26LF.js → chunk-RT6OILOI.js} +1 -1
  11. package/dist/chunk-RT6OILOI.js.map +1 -0
  12. package/dist/{chunk-NS5PUHIZ.js → chunk-RZTP4IEW.js} +130 -23
  13. package/dist/chunk-RZTP4IEW.js.map +1 -0
  14. package/dist/{chunk-HQ6WIRE2.js → chunk-SHEGLZEP.js} +4 -4
  15. package/dist/{chunk-WVIRXYI4.js → chunk-VSQBHPWH.js} +2 -2
  16. package/dist/{chunk-WVIRXYI4.js.map → chunk-VSQBHPWH.js.map} +1 -1
  17. package/dist/chunk-XNLWBZIG.js +9 -0
  18. package/dist/chunk-XNLWBZIG.js.map +1 -0
  19. package/dist/{chunk-ZACRRYWL.js → chunk-ZIVPHYRG.js} +4 -4
  20. package/dist/cli/quickdraw-docs.js +241 -27
  21. package/dist/cli/quickdraw-docs.js.map +1 -1
  22. package/dist/client/index.d.ts +7 -7
  23. package/dist/client/index.js +4 -4
  24. package/dist/{clientTypes-DuZW_8mb.d.ts → clientTypes-Bc-G8n-U.d.ts} +3 -3
  25. package/dist/{connection-DuN8GvcD.d.ts → connection-Cr1BU07W.d.ts} +2 -2
  26. package/dist/{createServer-CGsJlxM8.d.ts → createServer-CP9eDHZZ.d.ts} +30 -9
  27. package/dist/{createTestApp-Cg2CZa1g.d.ts → createTestApp-UKVGEmOf.d.ts} +6 -6
  28. package/dist/{envelope-nf5eGB-a.d.ts → envelope-Cp3YcVxC.d.ts} +1 -1
  29. package/dist/{http-D3fSFQ57.d.ts → http-Djj2Inuu.d.ts} +6 -6
  30. package/dist/index.d.ts +6 -6
  31. package/dist/index.js +26 -6
  32. package/dist/index.js.map +1 -1
  33. package/dist/{infer-Dw21kmLw.d.ts → infer-Teo1xpsd.d.ts} +17 -0
  34. package/dist/{metrics-C0WvB7-I.d.ts → metrics-BdBu5o8G.d.ts} +2 -2
  35. package/dist/prisma/index.d.ts +4 -4
  36. package/dist/server/auth/index.d.ts +3 -3
  37. package/dist/server/index.d.ts +47 -26
  38. package/dist/server/index.js +62 -36
  39. package/dist/server/index.js.map +1 -1
  40. package/dist/server/mcp/index.d.ts +51 -11
  41. package/dist/server/mcp/index.js +129 -8
  42. package/dist/server/mcp/index.js.map +1 -1
  43. package/dist/server/otel.d.ts +3 -3
  44. package/dist/{sharing-CoreezAq.d.ts → sharing-DVa8W-yR.d.ts} +3 -3
  45. package/dist/testing/client.d.ts +13 -13
  46. package/dist/testing/client.js +5 -5
  47. package/dist/testing/index.d.ts +156 -12
  48. package/dist/testing/index.js +1036 -21
  49. package/dist/testing/index.js.map +1 -1
  50. package/dist/testing/mock.d.ts +5 -5
  51. package/dist/testing/mock.js +4 -4
  52. package/dist/{types-Bwg3kIEc.d.ts → types-Cu0vmx7c.d.ts} +8 -2
  53. package/dist/utils/index.d.ts +1 -1
  54. package/package.json +1 -1
  55. package/dist/chunk-5YRQ26LF.js.map +0 -1
  56. package/dist/chunk-NS5PUHIZ.js.map +0 -1
  57. /package/dist/{chunk-O2PWC2YW.js.map → chunk-OECZPRPW.js.map} +0 -0
  58. /package/dist/{chunk-3JNMVB2V.js.map → chunk-QCHKWIRS.js.map} +0 -0
  59. /package/dist/{chunk-HQ6WIRE2.js.map → chunk-SHEGLZEP.js.map} +0 -0
  60. /package/dist/{chunk-ZACRRYWL.js.map → chunk-ZIVPHYRG.js.map} +0 -0
@@ -13,15 +13,15 @@ import {
13
13
  searchTargetOf,
14
14
  streamMaxOf,
15
15
  viewPredicate
16
- } from "./chunk-O2PWC2YW.js";
16
+ } from "./chunk-OECZPRPW.js";
17
17
  import {
18
18
  MAX_SUBSCRIBE_IDS,
19
19
  PROTOCOL_VERSION,
20
20
  QUICKDRAW_VERSION
21
- } from "./chunk-WVIRXYI4.js";
21
+ } from "./chunk-VSQBHPWH.js";
22
22
  import {
23
23
  isScopedStream
24
- } from "./chunk-5YRQ26LF.js";
24
+ } from "./chunk-RT6OILOI.js";
25
25
  import {
26
26
  buildCaller,
27
27
  methodKey,
@@ -849,4 +849,4 @@ function createMockClient(contracts, options = {}) {
849
849
  export {
850
850
  createMockClient
851
851
  };
852
- //# sourceMappingURL=chunk-HQ6WIRE2.js.map
852
+ //# sourceMappingURL=chunk-SHEGLZEP.js.map
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-4YO5H5ON.js";
4
4
 
5
5
  // src/version.ts
6
- var QUICKDRAW_VERSION = "5.0.1";
6
+ var QUICKDRAW_VERSION = "5.1.1";
7
7
 
8
8
  // src/contract/names.ts
9
9
  function entityRoom(service, id, level) {
@@ -127,4 +127,4 @@ export {
127
127
  MAX_SUBSCRIBE_IDS,
128
128
  MAX_SCOPE_LENGTH
129
129
  };
130
- //# sourceMappingURL=chunk-WVIRXYI4.js.map
130
+ //# sourceMappingURL=chunk-VSQBHPWH.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/version.ts","../src/contract/names.ts","../src/protocol/version.ts","../src/contract/access.ts"],"sourcesContent":["/** The published version of `@fitzzero/quickdraw-core`. Kept in step with package.json. */\nexport const QUICKDRAW_VERSION = \"5.0.1\";\n","// Room and event names for protocol v5 (RFC 0003 sections 6, 7 and 8). Ported\n// from the 4.1 room helpers (4.1 `src/shared/types.ts:364-405`), which were\n// plain template strings so shared code could name a room without holding a\n// service instance. The 5.0 rooms live under a `qd:` prefix so they never\n// collide with an app's own rooms; the per-user room keeps its 4.1 name.\n\nimport type { AccessLevel } from \"./access\";\n\n/**\n * The room for one entity at one access tier: `qd:e:{service}:{id}@{level}`.\n * A subscriber joins the room for its own level, so a frame is built once and\n * stripped once per occupied tier.\n */\nexport function entityRoom(service: string, id: string, level: AccessLevel): string {\n return `qd:e:${service}:${id}@${level}`;\n}\n\n/** The room for one scope of a collection: `qd:c:{service}:{collection}:{scope}`. */\nexport function collectionRoom(service: string, collection: string, scope: string): string {\n return `qd:c:${service}:${collection}:${scope}`;\n}\n\n/** The room for a watched topic of a service: `qd:t:{service}:{topic}`. */\nexport function topicRoom(service: string, topic: string): string {\n return `qd:t:${service}:${topic}`;\n}\n\n/**\n * The change topic of a whole service (RFC 0003 section 11.3): every flush\n * that changes one of its rows or collection scopes signals it.\n */\nexport const SERVICE_TOPIC = \"service\";\n\n/** The change topic of one scope of a collection: `{collection}:{scope}` (RFC 0003 section 11.3). */\nexport function collectionTopic(collection: string, scope: string): string {\n return `${collection}:${scope}`;\n}\n\n/**\n * The room of a stream's feed (RFC 0003 section 12.5):\n * `qd:s:{service}:{stream}:{scope}` for one scope of a scoped stream, and\n * `qd:s:{service}:{stream}` for a global stream. Stream names hold no colon.\n */\nexport function streamRoom(service: string, stream: string, scope?: string): string {\n return scope === undefined ? `qd:s:${service}:${stream}` : `qd:s:${service}:${stream}:${scope}`;\n}\n\n/** The room every authenticated socket of a user joins: `user:{userId}`, as in 4.1. */\nexport function userRoom(userId: string): string {\n return `user:${userId}`;\n}\n\n/**\n * The prefixes of the rooms the framework manages: entity, collection, topic\n * and stream rooms (`qd:`) and user rooms (`user:`). `ctx.rooms.join` refuses\n * them; a socket enters those rooms only through their authorized paths.\n */\nexport const RESERVED_ROOM_PREFIXES: readonly string[] = Object.freeze([\"qd:\", \"user:\"]);\n\n/**\n * The reserved prefix `room` starts with (`qd:` or `user:`), or `undefined`\n * when an app room may have that name.\n */\nexport function reservedRoomPrefix(room: string): string | undefined {\n return RESERVED_ROOM_PREFIXES.find((prefix) => room.startsWith(prefix));\n}\n\n/** Events a client sends to the server (RFC 0003 section 8.2). */\nexport const CLIENT_EVENTS = Object.freeze({\n /** A method call: `{ id, s, m, i, v? }`, answered by ack. */\n call: \"qd:call\",\n /** Cancels an in-flight call: `{ id }`. */\n cancel: \"qd:cancel\",\n /** Subscribes to entities by id, with the revisions held. */\n sub: \"qd:sub\",\n /** Unsubscribes from entities. */\n unsub: \"qd:unsub\",\n /** Subscribes to a collection scope, or resumes it from a revision. */\n collectionSub: \"qd:col:sub\",\n /** Unsubscribes from a collection scope. */\n collectionUnsub: \"qd:col:unsub\",\n /** Loads collection items by id. */\n collectionItems: \"qd:col:items\",\n /** Joins a watched topic: `{ s, topic }`. */\n watch: \"qd:watch\",\n /** Leaves a watched topic. */\n unwatch: \"qd:unwatch\",\n /** Subscribes to a stream and receives its seed. */\n streamSub: \"qd:stream:sub\",\n /** Unsubscribes from a stream. */\n streamUnsub: \"qd:stream:unsub\",\n /** A fire-and-forget channel message, sent volatile: `[s, channel, payload]`. */\n channel: \"qd:ch\",\n} as const);\n\n/** Events the server sends to a client (RFC 0003 section 8.3). */\nexport const SERVER_EVENTS = Object.freeze({\n /** The handshake reply: `{ protocol, server, limits, features, userId, serviceAccess }`. */\n hello: \"qd:hello\",\n /** Entity frames: full update, patch or removal. */\n entity: \"qd:e\",\n /** Collection deltas, batched per flush. */\n collection: \"qd:c\",\n /** A watched topic changed; the client invalidates its queries. */\n changed: \"qd:changed\",\n /** Access to a subscription was revoked. */\n revoked: \"qd:revoked\",\n /** Stream items. */\n stream: \"qd:stream\",\n /** Typed custom room events declared in a contract's `events`. */\n event: \"qd:event\",\n /** Who is in an app room the socket is in: the list on joining, then each user who joins or leaves. */\n presence: \"qd:presence\",\n /** Reconnect within a jitter window. */\n rotate: \"qd:rotate\",\n /** The user's service grants changed. */\n access: \"qd:access\",\n} as const);\n\n/** The name of an event a client sends. */\nexport type ClientEventName = (typeof CLIENT_EVENTS)[keyof typeof CLIENT_EVENTS];\n\n/** The name of an event the server sends. */\nexport type ServerEventName = (typeof SERVER_EVENTS)[keyof typeof SERVER_EVENTS];\n","// The protocol version and the connection handshake (RFC 0003 section 8.1).\n// A v5 client connects with `auth: { token, qd: { protocol: 5, client } }`.\n// The server answers with `qd:hello`, or refuses a client that names another\n// protocol with `PROTOCOL_MISMATCH` in the `connect_error` data, and one whose\n// authentication failed with `UNAUTHENTICATED` there. A client that sends no\n// `auth.qd` is a 4.x client, which only the legacy shim serves (section 8.5).\n// There is no capability negotiation: the version decides.\n\nimport type { AccessLevel } from \"../contract/access\";\nimport { isRecord } from \"./guards\";\n\n/** The wire protocol this package speaks. */\nexport const PROTOCOL_VERSION = 5;\n\n/** `auth.qd`: what a v5 client sends when it connects. */\nexport interface QdHandshake {\n /** The protocol the client speaks. The server refuses any but its own. */\n readonly protocol: number;\n /** The client package's version, for logs. */\n readonly client: string;\n}\n\n/** The Socket.IO `auth` a v5 client connects with. */\nexport interface HandshakeAuth {\n /** The session token the server's `authenticate` reads. */\n readonly token?: string;\n /** The protocol the client speaks. A client without it is a 4.x client (RFC 0003 section 8.5). */\n readonly qd: QdHandshake;\n}\n\n/**\n * True when `value` has the shape of `auth.qd`, whatever protocol it names.\n * Compare `protocol` with {@link PROTOCOL_VERSION} to decide whether to serve it.\n */\nexport function isQdHandshake(value: unknown): value is QdHandshake {\n return isRecord(value) && Number.isInteger(value.protocol) && typeof value.client === \"string\";\n}\n\n/** The code in the `connect_error` data of a connection refused for its protocol. */\nexport const PROTOCOL_MISMATCH = \"PROTOCOL_MISMATCH\";\n\n/**\n * The `data` of the error a server refuses a connection with when the client\n * speaks another protocol, or none. It is not an `ErrorCode`: no call failed.\n */\nexport interface ProtocolMismatch {\n readonly code: typeof PROTOCOL_MISMATCH;\n /** The protocol the server speaks. */\n readonly expected: number;\n}\n\n/** True when a `connect_error`'s `data` says the server speaks another protocol. */\nexport function isProtocolMismatch(value: unknown): value is ProtocolMismatch {\n return isRecord(value) && value.code === PROTOCOL_MISMATCH && Number.isInteger(value.expected);\n}\n\n/**\n * The `data` of the error a server refuses a connection with when\n * authenticating it failed: the app's `authenticate` threw, so the\n * credentials are bad or could not be checked. A client tells it apart from\n * {@link ProtocolMismatch} by `code`; reconnecting needs other credentials.\n */\nexport interface AuthenticationRefused {\n readonly code: \"UNAUTHENTICATED\";\n}\n\n/** True when a `connect_error`'s `data` says authenticating the connection failed. */\nexport function isAuthenticationRefused(value: unknown): value is AuthenticationRefused {\n return isRecord(value) && value.code === \"UNAUTHENTICATED\";\n}\n\n/** The most ids one `qd:sub` may name (RFC 0003 section 6), as `qd:hello` announces it. */\nexport const MAX_SUBSCRIBE_IDS = 500;\n\n/** The longest collection scope value or change topic a frame may name, in characters. */\nexport const MAX_SCOPE_LENGTH = 256;\n\n/**\n * Each socket's lane of subscription events (`qd:sub`, `qd:col:sub`,\n * `qd:col:items`, `qd:watch`; RFC 0003 section 8.2), as `qd:hello` announces it.\n */\nexport interface HelloSubscriptionLimits {\n /** Subscription events one socket may have running at once. */\n readonly maxInFlight: number;\n /** Subscription events one socket may have waiting; past that one fails with `RATE_LIMITED`. */\n readonly maxQueued: number;\n}\n\n/** The limits a server announces in `qd:hello`, so a client can stay inside them. */\nexport interface HelloLimits {\n /** Queries one socket may have running at once; more wait in a queue (RFC 0003 section 9). */\n readonly maxInFlightQueries: number;\n /** Queries one socket may have waiting; past that a query fails with `RATE_LIMITED`. */\n readonly maxQueuedQueries: number;\n /** Ids one `qd:sub` may name (RFC 0003 section 6). */\n readonly maxSubscribeIds: number;\n /**\n * A call's default time limit in milliseconds, after which it fails with\n * `TIMEOUT`. A v5 client waits this long plus 2 s for an answer, so a slow\n * call ends with the server's `TIMEOUT` rather than its own.\n */\n readonly callTimeoutMs: number;\n /** The socket's lane of subscription events. */\n readonly subscriptions: HelloSubscriptionLimits;\n}\n\n/**\n * `qd:hello`: the server's answer to a v5 handshake, sent once per\n * connection, after the socket's listeners are in place. It says who the\n * socket acts for, so a client knows its user without another call.\n */\nexport interface HelloFrame {\n /** The protocol the server speaks. */\n readonly protocol: typeof PROTOCOL_VERSION;\n /** The server package's version. */\n readonly server: string;\n /**\n * This server's id: random, new each time a server starts. A client that\n * reconnects to the same id came back after a blip, to another id to a\n * restarted server (a game's world and clock start over) or, behind a\n * cluster, to another node, each of which has its own.\n */\n readonly serverId: string;\n /** What a client must stay within. */\n readonly limits: HelloLimits;\n /** Names of optional server features that are on. Informational only. */\n readonly features: readonly string[];\n /** The user the socket acts for, or `null` when it is anonymous. */\n readonly userId: string | null;\n /**\n * The principal's service-wide grants when the socket connected, by\n * service name: `{ taskService: \"Admin\" }`. Empty for an anonymous socket\n * or a principal without grants. Later changes arrive as `qd:access`.\n */\n readonly serviceAccess: Readonly<Record<string, AccessLevel>>;\n}\n","// Access levels and access-control lists, unchanged from 4.1\n// (4.1 `src/shared/types.ts:7-14`). The level names are stored in app data\n// (`User.serviceAccess`, JSON access lists), so they never change.\n\nexport type AccessLevel = \"Public\" | \"Read\" | \"Moderate\" | \"Admin\";\n\n/** Every access level, lowest first. */\nexport const ACCESS_LEVELS: readonly AccessLevel[] = Object.freeze([\n \"Public\",\n \"Read\",\n \"Moderate\",\n \"Admin\",\n]);\n\n/** True when `value` is one of the four access level names. */\nexport function isAccessLevel(value: unknown): value is AccessLevel {\n return typeof value === \"string\" && (ACCESS_LEVELS as readonly string[]).includes(value);\n}\n\nexport type ACE = {\n userId: string;\n level: AccessLevel;\n};\n\nexport type ACL = ACE[];\n"],"mappings":";;;;;AACO,IAAM,oBAAoB;;;ACY1B,SAAS,WAAW,SAAiB,IAAY,OAA4B;AAClF,SAAO,QAAQ,OAAO,IAAI,EAAE,IAAI,KAAK;AACvC;AAGO,SAAS,eAAe,SAAiB,YAAoB,OAAuB;AACzF,SAAO,QAAQ,OAAO,IAAI,UAAU,IAAI,KAAK;AAC/C;AAGO,SAAS,UAAU,SAAiB,OAAuB;AAChE,SAAO,QAAQ,OAAO,IAAI,KAAK;AACjC;AAMO,IAAM,gBAAgB;AAGtB,SAAS,gBAAgB,YAAoB,OAAuB;AACzE,SAAO,GAAG,UAAU,IAAI,KAAK;AAC/B;AAOO,SAAS,WAAW,SAAiB,QAAgB,OAAwB;AAClF,SAAO,UAAU,SAAY,QAAQ,OAAO,IAAI,MAAM,KAAK,QAAQ,OAAO,IAAI,MAAM,IAAI,KAAK;AAC/F;AAGO,SAAS,SAAS,QAAwB;AAC/C,SAAO,QAAQ,MAAM;AACvB;AAOO,IAAM,yBAA4C,OAAO,OAAO,CAAC,OAAO,OAAO,CAAC;AAMhF,SAAS,mBAAmB,MAAkC;AACnE,SAAO,uBAAuB,KAAK,CAAC,WAAW,KAAK,WAAW,MAAM,CAAC;AACxE;AAGO,IAAM,gBAAgB,OAAO,OAAO;AAAA;AAAA,EAEzC,MAAM;AAAA;AAAA,EAEN,QAAQ;AAAA;AAAA,EAER,KAAK;AAAA;AAAA,EAEL,OAAO;AAAA;AAAA,EAEP,eAAe;AAAA;AAAA,EAEf,iBAAiB;AAAA;AAAA,EAEjB,iBAAiB;AAAA;AAAA,EAEjB,OAAO;AAAA;AAAA,EAEP,SAAS;AAAA;AAAA,EAET,WAAW;AAAA;AAAA,EAEX,aAAa;AAAA;AAAA,EAEb,SAAS;AACX,CAAU;AAGH,IAAM,gBAAgB,OAAO,OAAO;AAAA;AAAA,EAEzC,OAAO;AAAA;AAAA,EAEP,QAAQ;AAAA;AAAA,EAER,YAAY;AAAA;AAAA,EAEZ,SAAS;AAAA;AAAA,EAET,SAAS;AAAA;AAAA,EAET,QAAQ;AAAA;AAAA,EAER,OAAO;AAAA;AAAA,EAEP,UAAU;AAAA;AAAA,EAEV,QAAQ;AAAA;AAAA,EAER,QAAQ;AACV,CAAU;;;ACzGH,IAAM,mBAAmB;AAsBzB,SAAS,cAAc,OAAsC;AAClE,SAAO,SAAS,KAAK,KAAK,OAAO,UAAU,MAAM,QAAQ,KAAK,OAAO,MAAM,WAAW;AACxF;AAGO,IAAM,oBAAoB;AAa1B,SAAS,mBAAmB,OAA2C;AAC5E,SAAO,SAAS,KAAK,KAAK,MAAM,SAAS,qBAAqB,OAAO,UAAU,MAAM,QAAQ;AAC/F;AAaO,SAAS,wBAAwB,OAAgD;AACtF,SAAO,SAAS,KAAK,KAAK,MAAM,SAAS;AAC3C;AAGO,IAAM,oBAAoB;AAG1B,IAAM,mBAAmB;;;ACpEzB,IAAM,gBAAwC,OAAO,OAAO;AAAA,EACjE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAGM,SAAS,cAAc,OAAsC;AAClE,SAAO,OAAO,UAAU,YAAa,cAAoC,SAAS,KAAK;AACzF;","names":[]}
1
+ {"version":3,"sources":["../src/version.ts","../src/contract/names.ts","../src/protocol/version.ts","../src/contract/access.ts"],"sourcesContent":["/** The published version of `@fitzzero/quickdraw-core`. Kept in step with package.json. */\nexport const QUICKDRAW_VERSION = \"5.1.1\";\n","// Room and event names for protocol v5 (RFC 0003 sections 6, 7 and 8). Ported\n// from the 4.1 room helpers (4.1 `src/shared/types.ts:364-405`), which were\n// plain template strings so shared code could name a room without holding a\n// service instance. The 5.0 rooms live under a `qd:` prefix so they never\n// collide with an app's own rooms; the per-user room keeps its 4.1 name.\n\nimport type { AccessLevel } from \"./access\";\n\n/**\n * The room for one entity at one access tier: `qd:e:{service}:{id}@{level}`.\n * A subscriber joins the room for its own level, so a frame is built once and\n * stripped once per occupied tier.\n */\nexport function entityRoom(service: string, id: string, level: AccessLevel): string {\n return `qd:e:${service}:${id}@${level}`;\n}\n\n/** The room for one scope of a collection: `qd:c:{service}:{collection}:{scope}`. */\nexport function collectionRoom(service: string, collection: string, scope: string): string {\n return `qd:c:${service}:${collection}:${scope}`;\n}\n\n/** The room for a watched topic of a service: `qd:t:{service}:{topic}`. */\nexport function topicRoom(service: string, topic: string): string {\n return `qd:t:${service}:${topic}`;\n}\n\n/**\n * The change topic of a whole service (RFC 0003 section 11.3): every flush\n * that changes one of its rows or collection scopes signals it.\n */\nexport const SERVICE_TOPIC = \"service\";\n\n/** The change topic of one scope of a collection: `{collection}:{scope}` (RFC 0003 section 11.3). */\nexport function collectionTopic(collection: string, scope: string): string {\n return `${collection}:${scope}`;\n}\n\n/**\n * The room of a stream's feed (RFC 0003 section 12.5):\n * `qd:s:{service}:{stream}:{scope}` for one scope of a scoped stream, and\n * `qd:s:{service}:{stream}` for a global stream. Stream names hold no colon.\n */\nexport function streamRoom(service: string, stream: string, scope?: string): string {\n return scope === undefined ? `qd:s:${service}:${stream}` : `qd:s:${service}:${stream}:${scope}`;\n}\n\n/** The room every authenticated socket of a user joins: `user:{userId}`, as in 4.1. */\nexport function userRoom(userId: string): string {\n return `user:${userId}`;\n}\n\n/**\n * The prefixes of the rooms the framework manages: entity, collection, topic\n * and stream rooms (`qd:`) and user rooms (`user:`). `ctx.rooms.join` refuses\n * them; a socket enters those rooms only through their authorized paths.\n */\nexport const RESERVED_ROOM_PREFIXES: readonly string[] = Object.freeze([\"qd:\", \"user:\"]);\n\n/**\n * The reserved prefix `room` starts with (`qd:` or `user:`), or `undefined`\n * when an app room may have that name.\n */\nexport function reservedRoomPrefix(room: string): string | undefined {\n return RESERVED_ROOM_PREFIXES.find((prefix) => room.startsWith(prefix));\n}\n\n/** Events a client sends to the server (RFC 0003 section 8.2). */\nexport const CLIENT_EVENTS = Object.freeze({\n /** A method call: `{ id, s, m, i, v? }`, answered by ack. */\n call: \"qd:call\",\n /** Cancels an in-flight call: `{ id }`. */\n cancel: \"qd:cancel\",\n /** Subscribes to entities by id, with the revisions held. */\n sub: \"qd:sub\",\n /** Unsubscribes from entities. */\n unsub: \"qd:unsub\",\n /** Subscribes to a collection scope, or resumes it from a revision. */\n collectionSub: \"qd:col:sub\",\n /** Unsubscribes from a collection scope. */\n collectionUnsub: \"qd:col:unsub\",\n /** Loads collection items by id. */\n collectionItems: \"qd:col:items\",\n /** Joins a watched topic: `{ s, topic }`. */\n watch: \"qd:watch\",\n /** Leaves a watched topic. */\n unwatch: \"qd:unwatch\",\n /** Subscribes to a stream and receives its seed. */\n streamSub: \"qd:stream:sub\",\n /** Unsubscribes from a stream. */\n streamUnsub: \"qd:stream:unsub\",\n /** A fire-and-forget channel message, sent volatile: `[s, channel, payload]`. */\n channel: \"qd:ch\",\n} as const);\n\n/** Events the server sends to a client (RFC 0003 section 8.3). */\nexport const SERVER_EVENTS = Object.freeze({\n /** The handshake reply: `{ protocol, server, limits, features, userId, serviceAccess }`. */\n hello: \"qd:hello\",\n /** Entity frames: full update, patch or removal. */\n entity: \"qd:e\",\n /** Collection deltas, batched per flush. */\n collection: \"qd:c\",\n /** A watched topic changed; the client invalidates its queries. */\n changed: \"qd:changed\",\n /** Access to a subscription was revoked. */\n revoked: \"qd:revoked\",\n /** Stream items. */\n stream: \"qd:stream\",\n /** Typed custom room events declared in a contract's `events`. */\n event: \"qd:event\",\n /** Who is in an app room the socket is in: the list on joining, then each user who joins or leaves. */\n presence: \"qd:presence\",\n /** Reconnect within a jitter window. */\n rotate: \"qd:rotate\",\n /** The user's service grants changed. */\n access: \"qd:access\",\n} as const);\n\n/** The name of an event a client sends. */\nexport type ClientEventName = (typeof CLIENT_EVENTS)[keyof typeof CLIENT_EVENTS];\n\n/** The name of an event the server sends. */\nexport type ServerEventName = (typeof SERVER_EVENTS)[keyof typeof SERVER_EVENTS];\n","// The protocol version and the connection handshake (RFC 0003 section 8.1).\n// A v5 client connects with `auth: { token, qd: { protocol: 5, client } }`.\n// The server answers with `qd:hello`, or refuses a client that names another\n// protocol with `PROTOCOL_MISMATCH` in the `connect_error` data, and one whose\n// authentication failed with `UNAUTHENTICATED` there. A client that sends no\n// `auth.qd` is a 4.x client, which only the legacy shim serves (section 8.5).\n// There is no capability negotiation: the version decides.\n\nimport type { AccessLevel } from \"../contract/access\";\nimport { isRecord } from \"./guards\";\n\n/** The wire protocol this package speaks. */\nexport const PROTOCOL_VERSION = 5;\n\n/** `auth.qd`: what a v5 client sends when it connects. */\nexport interface QdHandshake {\n /** The protocol the client speaks. The server refuses any but its own. */\n readonly protocol: number;\n /** The client package's version, for logs. */\n readonly client: string;\n}\n\n/** The Socket.IO `auth` a v5 client connects with. */\nexport interface HandshakeAuth {\n /** The session token the server's `authenticate` reads. */\n readonly token?: string;\n /** The protocol the client speaks. A client without it is a 4.x client (RFC 0003 section 8.5). */\n readonly qd: QdHandshake;\n}\n\n/**\n * True when `value` has the shape of `auth.qd`, whatever protocol it names.\n * Compare `protocol` with {@link PROTOCOL_VERSION} to decide whether to serve it.\n */\nexport function isQdHandshake(value: unknown): value is QdHandshake {\n return isRecord(value) && Number.isInteger(value.protocol) && typeof value.client === \"string\";\n}\n\n/** The code in the `connect_error` data of a connection refused for its protocol. */\nexport const PROTOCOL_MISMATCH = \"PROTOCOL_MISMATCH\";\n\n/**\n * The `data` of the error a server refuses a connection with when the client\n * speaks another protocol, or none. It is not an `ErrorCode`: no call failed.\n */\nexport interface ProtocolMismatch {\n readonly code: typeof PROTOCOL_MISMATCH;\n /** The protocol the server speaks. */\n readonly expected: number;\n}\n\n/** True when a `connect_error`'s `data` says the server speaks another protocol. */\nexport function isProtocolMismatch(value: unknown): value is ProtocolMismatch {\n return isRecord(value) && value.code === PROTOCOL_MISMATCH && Number.isInteger(value.expected);\n}\n\n/**\n * The `data` of the error a server refuses a connection with when\n * authenticating it failed: the app's `authenticate` threw, so the\n * credentials are bad or could not be checked. A client tells it apart from\n * {@link ProtocolMismatch} by `code`; reconnecting needs other credentials.\n */\nexport interface AuthenticationRefused {\n readonly code: \"UNAUTHENTICATED\";\n}\n\n/** True when a `connect_error`'s `data` says authenticating the connection failed. */\nexport function isAuthenticationRefused(value: unknown): value is AuthenticationRefused {\n return isRecord(value) && value.code === \"UNAUTHENTICATED\";\n}\n\n/** The most ids one `qd:sub` may name (RFC 0003 section 6), as `qd:hello` announces it. */\nexport const MAX_SUBSCRIBE_IDS = 500;\n\n/** The longest collection scope value or change topic a frame may name, in characters. */\nexport const MAX_SCOPE_LENGTH = 256;\n\n/**\n * Each socket's lane of subscription events (`qd:sub`, `qd:col:sub`,\n * `qd:col:items`, `qd:watch`; RFC 0003 section 8.2), as `qd:hello` announces it.\n */\nexport interface HelloSubscriptionLimits {\n /** Subscription events one socket may have running at once. */\n readonly maxInFlight: number;\n /** Subscription events one socket may have waiting; past that one fails with `RATE_LIMITED`. */\n readonly maxQueued: number;\n}\n\n/** The limits a server announces in `qd:hello`, so a client can stay inside them. */\nexport interface HelloLimits {\n /** Queries one socket may have running at once; more wait in a queue (RFC 0003 section 9). */\n readonly maxInFlightQueries: number;\n /** Queries one socket may have waiting; past that a query fails with `RATE_LIMITED`. */\n readonly maxQueuedQueries: number;\n /** Ids one `qd:sub` may name (RFC 0003 section 6). */\n readonly maxSubscribeIds: number;\n /**\n * A call's default time limit in milliseconds, after which it fails with\n * `TIMEOUT`. A v5 client waits this long plus 2 s for an answer, so a slow\n * call ends with the server's `TIMEOUT` rather than its own.\n */\n readonly callTimeoutMs: number;\n /** The socket's lane of subscription events. */\n readonly subscriptions: HelloSubscriptionLimits;\n}\n\n/**\n * `qd:hello`: the server's answer to a v5 handshake, sent once per\n * connection, after the socket's listeners are in place. It says who the\n * socket acts for, so a client knows its user without another call.\n */\nexport interface HelloFrame {\n /** The protocol the server speaks. */\n readonly protocol: typeof PROTOCOL_VERSION;\n /** The server package's version. */\n readonly server: string;\n /**\n * This server's id: random, new each time a server starts. A client that\n * reconnects to the same id came back after a blip, to another id to a\n * restarted server (a game's world and clock start over) or, behind a\n * cluster, to another node, each of which has its own.\n */\n readonly serverId: string;\n /** What a client must stay within. */\n readonly limits: HelloLimits;\n /** Names of optional server features that are on. Informational only. */\n readonly features: readonly string[];\n /** The user the socket acts for, or `null` when it is anonymous. */\n readonly userId: string | null;\n /**\n * The principal's service-wide grants when the socket connected, by\n * service name: `{ taskService: \"Admin\" }`. Empty for an anonymous socket\n * or a principal without grants. Later changes arrive as `qd:access`.\n */\n readonly serviceAccess: Readonly<Record<string, AccessLevel>>;\n}\n","// Access levels and access-control lists, unchanged from 4.1\n// (4.1 `src/shared/types.ts:7-14`). The level names are stored in app data\n// (`User.serviceAccess`, JSON access lists), so they never change.\n\nexport type AccessLevel = \"Public\" | \"Read\" | \"Moderate\" | \"Admin\";\n\n/** Every access level, lowest first. */\nexport const ACCESS_LEVELS: readonly AccessLevel[] = Object.freeze([\n \"Public\",\n \"Read\",\n \"Moderate\",\n \"Admin\",\n]);\n\n/** True when `value` is one of the four access level names. */\nexport function isAccessLevel(value: unknown): value is AccessLevel {\n return typeof value === \"string\" && (ACCESS_LEVELS as readonly string[]).includes(value);\n}\n\nexport type ACE = {\n userId: string;\n level: AccessLevel;\n};\n\nexport type ACL = ACE[];\n"],"mappings":";;;;;AACO,IAAM,oBAAoB;;;ACY1B,SAAS,WAAW,SAAiB,IAAY,OAA4B;AAClF,SAAO,QAAQ,OAAO,IAAI,EAAE,IAAI,KAAK;AACvC;AAGO,SAAS,eAAe,SAAiB,YAAoB,OAAuB;AACzF,SAAO,QAAQ,OAAO,IAAI,UAAU,IAAI,KAAK;AAC/C;AAGO,SAAS,UAAU,SAAiB,OAAuB;AAChE,SAAO,QAAQ,OAAO,IAAI,KAAK;AACjC;AAMO,IAAM,gBAAgB;AAGtB,SAAS,gBAAgB,YAAoB,OAAuB;AACzE,SAAO,GAAG,UAAU,IAAI,KAAK;AAC/B;AAOO,SAAS,WAAW,SAAiB,QAAgB,OAAwB;AAClF,SAAO,UAAU,SAAY,QAAQ,OAAO,IAAI,MAAM,KAAK,QAAQ,OAAO,IAAI,MAAM,IAAI,KAAK;AAC/F;AAGO,SAAS,SAAS,QAAwB;AAC/C,SAAO,QAAQ,MAAM;AACvB;AAOO,IAAM,yBAA4C,OAAO,OAAO,CAAC,OAAO,OAAO,CAAC;AAMhF,SAAS,mBAAmB,MAAkC;AACnE,SAAO,uBAAuB,KAAK,CAAC,WAAW,KAAK,WAAW,MAAM,CAAC;AACxE;AAGO,IAAM,gBAAgB,OAAO,OAAO;AAAA;AAAA,EAEzC,MAAM;AAAA;AAAA,EAEN,QAAQ;AAAA;AAAA,EAER,KAAK;AAAA;AAAA,EAEL,OAAO;AAAA;AAAA,EAEP,eAAe;AAAA;AAAA,EAEf,iBAAiB;AAAA;AAAA,EAEjB,iBAAiB;AAAA;AAAA,EAEjB,OAAO;AAAA;AAAA,EAEP,SAAS;AAAA;AAAA,EAET,WAAW;AAAA;AAAA,EAEX,aAAa;AAAA;AAAA,EAEb,SAAS;AACX,CAAU;AAGH,IAAM,gBAAgB,OAAO,OAAO;AAAA;AAAA,EAEzC,OAAO;AAAA;AAAA,EAEP,QAAQ;AAAA;AAAA,EAER,YAAY;AAAA;AAAA,EAEZ,SAAS;AAAA;AAAA,EAET,SAAS;AAAA;AAAA,EAET,QAAQ;AAAA;AAAA,EAER,OAAO;AAAA;AAAA,EAEP,UAAU;AAAA;AAAA,EAEV,QAAQ;AAAA;AAAA,EAER,QAAQ;AACV,CAAU;;;ACzGH,IAAM,mBAAmB;AAsBzB,SAAS,cAAc,OAAsC;AAClE,SAAO,SAAS,KAAK,KAAK,OAAO,UAAU,MAAM,QAAQ,KAAK,OAAO,MAAM,WAAW;AACxF;AAGO,IAAM,oBAAoB;AAa1B,SAAS,mBAAmB,OAA2C;AAC5E,SAAO,SAAS,KAAK,KAAK,MAAM,SAAS,qBAAqB,OAAO,UAAU,MAAM,QAAQ;AAC/F;AAaO,SAAS,wBAAwB,OAAgD;AACtF,SAAO,SAAS,KAAK,KAAK,MAAM,SAAS;AAC3C;AAGO,IAAM,oBAAoB;AAG1B,IAAM,mBAAmB;;;ACpEzB,IAAM,gBAAwC,OAAO,OAAO;AAAA,EACjE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAGM,SAAS,cAAc,OAAsC;AAClE,SAAO,OAAO,UAAU,YAAa,cAAoC,SAAS,KAAK;AACzF;","names":[]}
@@ -0,0 +1,9 @@
1
+ // src/contract/toolName.ts
2
+ function defaultToolName(service, method) {
3
+ return `${service}_${method}`;
4
+ }
5
+
6
+ export {
7
+ defaultToolName
8
+ };
9
+ //# sourceMappingURL=chunk-XNLWBZIG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/contract/toolName.ts"],"sourcesContent":["// A method tool's default name. It lives with the contracts, not in the MCP\n// bridge, so `quickdraw-docs` prints it without the bridge's entry: the\n// bridge's own files ship in `./server/mcp` alone (scripts/dist-smoke.mjs).\n\n/**\n * The name the MCP bridge gives a method's tool when the registry has no\n * `name` option: `{service}_{method}` (RFC 0003 section 10).\n */\nexport function defaultToolName(service: string, method: string): string {\n return `${service}_${method}`;\n}\n"],"mappings":";AAQO,SAAS,gBAAgB,SAAiB,QAAwB;AACvE,SAAO,GAAG,OAAO,IAAI,MAAM;AAC7B;","names":[]}
@@ -43,15 +43,15 @@ import {
43
43
  useQuickdrawContext,
44
44
  userOf,
45
45
  viewPredicate
46
- } from "./chunk-O2PWC2YW.js";
46
+ } from "./chunk-OECZPRPW.js";
47
47
  import {
48
48
  CLIENT_EVENTS,
49
49
  SERVICE_TOPIC,
50
50
  collectionTopic
51
- } from "./chunk-WVIRXYI4.js";
51
+ } from "./chunk-VSQBHPWH.js";
52
52
  import {
53
53
  isScopedStream
54
- } from "./chunk-5YRQ26LF.js";
54
+ } from "./chunk-RT6OILOI.js";
55
55
  import {
56
56
  buildCaller,
57
57
  collectionKey,
@@ -1410,4 +1410,4 @@ export {
1410
1410
  QuickdrawProvider,
1411
1411
  useQuickdraw
1412
1412
  };
1413
- //# sourceMappingURL=chunk-ZACRRYWL.js.map
1413
+ //# sourceMappingURL=chunk-ZIVPHYRG.js.map
@@ -1,10 +1,13 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ defaultToolName
4
+ } from "../chunk-XNLWBZIG.js";
2
5
  import {
3
6
  CHANNEL_DEFAULT_RATE,
4
7
  DEFAULT_COLLECTION_LIMIT,
5
8
  DEFAULT_COLLECTION_MAX_LIMIT,
6
9
  isScopedStream
7
- } from "../chunk-5YRQ26LF.js";
10
+ } from "../chunk-RT6OILOI.js";
8
11
  import {
9
12
  hasJsonSchema,
10
13
  isStandardSchema
@@ -76,6 +79,12 @@ function recordsOf(value) {
76
79
  }
77
80
  return value;
78
81
  }
82
+ function kindsOf(value) {
83
+ return Array.isArray(value) && value.every((kind) => typeof kind === "string") ? value : void 0;
84
+ }
85
+ function namesOf(value) {
86
+ return Array.isArray(value) ? value.filter((name) => typeof name === "string") : [];
87
+ }
79
88
  function mapEach(records, read) {
80
89
  return new Map([...records].map(([name, record]) => [name, read(record)]));
81
90
  }
@@ -94,13 +103,17 @@ function serviceDocOf(value) {
94
103
  }
95
104
  return Object.freeze({
96
105
  name: value.name,
106
+ model: typeof value.model === "string" ? value.model : void 0,
107
+ writes: namesOf(value.writes),
108
+ affects: Array.isArray(value.affects) ? value.affects.map((link) => contractName(isRecord(link) ? link.service : void 0)).filter((name) => name !== void 0) : [],
97
109
  policy: value.access,
98
110
  adminBypass: value.adminBypass !== false,
111
+ kinds: kindsOf(value.kinds),
99
112
  watchAccess: value.watchAccess,
100
113
  methods: new Map(
101
114
  Object.entries(methods).map(([name, method]) => [
102
115
  name,
103
- { access: method.access, rowless: method.rowless === true }
116
+ { access: method.access, rowless: method.rowless === true, kinds: kindsOf(method.kinds) }
104
117
  ])
105
118
  ),
106
119
  collections: mapEach(collections, (collection) => ({
@@ -140,8 +153,11 @@ function servicesOf(exports) {
140
153
  [...found].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([name, { doc }]) => [name, doc])
141
154
  );
142
155
  }
156
+ function contractName(contract) {
157
+ return isRecord(contract) && typeof contract.name === "string" ? contract.name : void 0;
158
+ }
143
159
  function serviceName(contract) {
144
- return isRecord(contract) && typeof contract.name === "string" ? contract.name : "another service";
160
+ return contractName(contract) ?? "another service";
145
161
  }
146
162
  function idOf(id) {
147
163
  if (id === void 0) {
@@ -220,13 +236,164 @@ function collectionAccessText(collection) {
220
236
  return `${String(collection.level)} or more on the ${code(serviceName(collection.anchor))} row the scope names`;
221
237
  }
222
238
 
239
+ // src/cli/dependencies.ts
240
+ var KINDS = ["inherit", "writes", "affects", "scope", "anchor"];
241
+ function isRecord2(value) {
242
+ return typeof value === "object" && value !== null && !Array.isArray(value);
243
+ }
244
+ function modelKey(model) {
245
+ return `${model.charAt(0).toLowerCase()}${model.slice(1)}`;
246
+ }
247
+ var MAX_POLICY_DEPTH2 = 3;
248
+ function inheritsOf(policy, depth = 0) {
249
+ if (!isRecord2(policy)) {
250
+ return [];
251
+ }
252
+ if (policy.kind === "inherit") {
253
+ const from = contractName(policy.from);
254
+ return from === void 0 ? [] : [{ from, via: String(policy.via) }];
255
+ }
256
+ if (policy.kind === "anyOf" && Array.isArray(policy.policies) && depth < MAX_POLICY_DEPTH2) {
257
+ return policy.policies.flatMap((member) => inheritsOf(member, depth + 1));
258
+ }
259
+ return [];
260
+ }
261
+ function scopeOf(form) {
262
+ return isRecord2(form) && typeof form.scope === "string" ? contractName(form.of) : void 0;
263
+ }
264
+ function dependenciesOfService(service, contract, owners) {
265
+ const found = /* @__PURE__ */ new Map();
266
+ const add = (target, kind, text) => {
267
+ if (target === void 0 || target === service.name) {
268
+ return;
269
+ }
270
+ found.set(target, [...found.get(target) ?? [], { kind, text }]);
271
+ };
272
+ for (const { from, via } of inheritsOf(service.policy)) {
273
+ add(from, "inherit", `row policy: ${code("inherit")} through the ${code(via)} column`);
274
+ }
275
+ const models = [];
276
+ for (const model of service.writes) {
277
+ const serving = owners.get(modelKey(model)) ?? [];
278
+ if (serving.length === 0) {
279
+ models.push(model);
280
+ }
281
+ for (const owner of serving) {
282
+ add(owner, "writes", `${code("writes")}: its ${code(model)} model`);
283
+ }
284
+ }
285
+ for (const target of new Set(service.affects)) {
286
+ add(target, "affects", `${code("affects")}: a write to a row here changes its rows`);
287
+ }
288
+ for (const [name, method] of service.methods) {
289
+ add(scopeOf(method.access), "scope", `method ${code(name)}: ${code("{ scope, of }")}`);
290
+ }
291
+ for (const [name, access] of service.channels) {
292
+ add(scopeOf(access), "scope", `channel ${code(name)}: ${code("{ scope, of }")}`);
293
+ }
294
+ for (const [name, stream] of Object.entries(contract?.streams ?? {})) {
295
+ add(scopeOf(stream.access), "scope", `stream ${code(name)}: ${code("{ scope, of }")}`);
296
+ }
297
+ for (const [name, collection] of service.collections) {
298
+ add(
299
+ contractName(collection.anchor),
300
+ "anchor",
301
+ `collection ${code(name)}: anchored on its rows`
302
+ );
303
+ }
304
+ return {
305
+ services: [...found].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([name, reasons]) => ({
306
+ service: name,
307
+ reasons: KINDS.flatMap((kind) => reasons.filter((reason) => reason.kind === kind))
308
+ })),
309
+ models
310
+ };
311
+ }
312
+ function dependenciesOf(contracts, services) {
313
+ const owners = /* @__PURE__ */ new Map();
314
+ for (const service of services.values()) {
315
+ if (service.model !== void 0) {
316
+ const key = modelKey(service.model);
317
+ owners.set(key, [...owners.get(key) ?? [], service.name]);
318
+ }
319
+ }
320
+ const byName = new Map(contracts.map((contract) => [contract.name, contract]));
321
+ return new Map(
322
+ [...services.values()].map((service) => [
323
+ service.name,
324
+ dependenciesOfService(service, byName.get(service.name), owners)
325
+ ])
326
+ );
327
+ }
328
+ function kindsOf2(dependency) {
329
+ return KINDS.filter((kind) => dependency.reasons.some((reason) => reason.kind === kind));
330
+ }
331
+ function dependsOnSection(dependencies) {
332
+ const { services, models } = dependencies;
333
+ const unserved = models.length === 0 ? [] : [`It also ${code("writes")} models no service's rows live in: ${list(models)}.`];
334
+ if (services.length === 0) {
335
+ return section("## Depends on", ["No other service: its declarations name none.", ...unserved]);
336
+ }
337
+ return section("## Depends on", [
338
+ `The other services its declarations name: its row policy, ${code("writes")}, ${code("affects")}, ${code("{ scope, of }")} access forms and collection anchors. A handler's undeclared reads are not listed.`,
339
+ table(
340
+ ["Service", "Declared by"],
341
+ services.map(({ service, reasons }) => [
342
+ code(service),
343
+ reasons.map((reason) => reason.text).join("; ")
344
+ ])
345
+ ),
346
+ ...unserved
347
+ ]);
348
+ }
349
+ function nodeIds(names) {
350
+ const ids = /* @__PURE__ */ new Map();
351
+ const taken = /* @__PURE__ */ new Set();
352
+ for (const name of names) {
353
+ const safe = name.replace(/\W/g, "_");
354
+ let id = /^end$/i.test(safe) ? `${safe}_` : safe;
355
+ for (let n = 2; taken.has(id); n += 1) {
356
+ id = `${safe}_${n}`;
357
+ }
358
+ taken.add(id);
359
+ ids.set(name, id);
360
+ }
361
+ return ids;
362
+ }
363
+ function dependencyGraph(dependencies) {
364
+ const edges = [...dependencies].flatMap(
365
+ ([from, { services }]) => services.map((dependency) => ({
366
+ from,
367
+ to: dependency.service,
368
+ label: kindsOf2(dependency).join(", ")
369
+ }))
370
+ );
371
+ if (edges.length === 0) {
372
+ return section("## Dependencies", ["No service's declarations name another service."]);
373
+ }
374
+ const names = [...new Set(edges.flatMap(({ from, to }) => [from, to]))].sort();
375
+ const ids = nodeIds(names);
376
+ const id = (name) => ids.get(name) ?? name;
377
+ const nodes = names.filter((name) => id(name) !== name).map((name) => ` ${id(name)}["${name.replace(/"/g, "#quot;")}"]`);
378
+ return section("## Dependencies", [
379
+ `An arrow points from a service to another its declarations name: ${code("inherit")} (its row policy), ${code("writes")} (that service's model), ${code("affects")}, ${code("scope")} (a ${code("{ scope, of }")} access form) or ${code("anchor")} (a collection's anchor). Each service's page lists them.`,
380
+ [
381
+ "```mermaid",
382
+ "graph LR",
383
+ ...nodes,
384
+ ...edges.map(({ from, to, label }) => ` ${id(from)} -->|${label}| ${id(to)}`),
385
+ "```"
386
+ ]
387
+ ]);
388
+ }
389
+
223
390
  // src/cli/schemaText.ts
224
391
  var NO_JSON_SCHEMA = "unknown (no JSON Schema)";
225
392
  var TARGET = "draft-07";
226
393
  var MAX_DEPTH = 8;
227
394
  var IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
228
395
  var ROOT = "#";
229
- function isRecord2(value) {
396
+ function isRecord3(value) {
230
397
  return typeof value === "object" && value !== null && !Array.isArray(value);
231
398
  }
232
399
  function jsonSchemaOf(schema, mode) {
@@ -235,7 +402,7 @@ function jsonSchemaOf(schema, mode) {
235
402
  }
236
403
  try {
237
404
  const json = schema["~standard"].jsonSchema[mode]({ target: TARGET });
238
- return isRecord2(json) ? json : void 0;
405
+ return isRecord3(json) ? json : void 0;
239
406
  } catch {
240
407
  return void 0;
241
408
  }
@@ -247,8 +414,8 @@ function definitionOf(root, ref) {
247
414
  }
248
415
  const [, group = "", name = ""] = match;
249
416
  const definitions = root[group];
250
- const found = isRecord2(definitions) ? definitions[decodeURIComponent(name)] : void 0;
251
- return isRecord2(found) ? found : void 0;
417
+ const found = isRecord3(definitions) ? definitions[decodeURIComponent(name)] : void 0;
418
+ return isRecord3(found) ? found : void 0;
252
419
  }
253
420
  function literal(value) {
254
421
  return value === void 0 ? "undefined" : JSON.stringify(value);
@@ -287,13 +454,13 @@ function requiredOf(schema) {
287
454
  return new Set(Array.isArray(schema.required) ? schema.required.map(String) : []);
288
455
  }
289
456
  function objectText(schema, scope) {
290
- const properties = isRecord2(schema.properties) ? schema.properties : {};
457
+ const properties = isRecord3(schema.properties) ? schema.properties : {};
291
458
  const required = requiredOf(schema);
292
459
  const members = Object.entries(properties).map(
293
460
  ([name, value]) => `${propertyName(name, !required.has(name))}: ${textOf(value, scope)}`
294
461
  );
295
462
  const extra = schema.additionalProperties;
296
- if (isRecord2(extra)) {
463
+ if (isRecord3(extra)) {
297
464
  const value = textOf(extra, scope);
298
465
  if (members.length === 0) {
299
466
  return `Record<string, ${value}>`;
@@ -351,10 +518,10 @@ function refText(ref, scope) {
351
518
  return textOf(target, { ...scope, open: [...scope.open, ref] });
352
519
  }
353
520
  function textOf(schema, scope) {
354
- if (schema === true || isRecord2(schema) && Object.keys(schema).length === 0) {
521
+ if (schema === true || isRecord3(schema) && Object.keys(schema).length === 0) {
355
522
  return "unknown";
356
523
  }
357
- if (!isRecord2(schema) || scope.depth > MAX_DEPTH) {
524
+ if (!isRecord3(schema) || scope.depth > MAX_DEPTH) {
358
525
  return schema === false ? "never" : "unknown";
359
526
  }
360
527
  const inner = { ...scope, depth: scope.depth + 1 };
@@ -381,7 +548,7 @@ function textOf(schema, scope) {
381
548
  if (typeof type === "string") {
382
549
  return typeNameText(type, schema, inner);
383
550
  }
384
- return isRecord2(schema.properties) ? objectText(schema, inner) : "unknown";
551
+ return isRecord3(schema.properties) ? objectText(schema, inner) : "unknown";
385
552
  }
386
553
  function schemaText(schema) {
387
554
  return textOf(schema, { root: schema, open: [ROOT], depth: 0 });
@@ -447,7 +614,7 @@ function schemaNotes(schema) {
447
614
  }
448
615
  function schemaFields(schema) {
449
616
  const properties = schema.properties;
450
- if (schema.type !== "object" || !isRecord2(properties) || Object.keys(properties).length === 0) {
617
+ if (schema.type !== "object" || !isRecord3(properties) || Object.keys(properties).length === 0) {
451
618
  return void 0;
452
619
  }
453
620
  const required = requiredOf(schema);
@@ -456,7 +623,7 @@ function schemaFields(schema) {
456
623
  name,
457
624
  optional: !required.has(name),
458
625
  type: textOf(value, scope),
459
- notes: isRecord2(value) ? schemaNotes(value) : ""
626
+ notes: isRecord3(value) ? schemaNotes(value) : ""
460
627
  }));
461
628
  }
462
629
 
@@ -547,6 +714,24 @@ function kindLine(method) {
547
714
  }
548
715
  return `A query that watches the ${code(method.watch.collection)} collection: a cached result is fetched again when the scope its input names changes.`;
549
716
  }
717
+ function described(text) {
718
+ return text === void 0 ? [] : [paragraph(text)];
719
+ }
720
+ function callableBy(name, access) {
721
+ const method = access?.service?.methods.get(name);
722
+ if (method?.kinds === void 0) {
723
+ return [];
724
+ }
725
+ const anonymous = method.access === "public" ? ", or anonymously" : "";
726
+ return [`- Callable by: ${list(method.kinds)}${anonymous}`];
727
+ }
728
+ function methodFacts(service, name, method, access) {
729
+ return [
730
+ `- MCP tool (default name): ${code(defaultToolName(service, name))}`,
731
+ ...method.kind === "query" ? ["- MCP read-only hint: yes, as on every query's tool"] : [],
732
+ ...callableBy(name, access)
733
+ ];
734
+ }
550
735
  function methodAccess(name, access) {
551
736
  const method = access?.service?.methods.get(name);
552
737
  if (method === void 0) {
@@ -557,11 +742,11 @@ function methodAccess(name, access) {
557
742
  ] : [];
558
743
  return [`Access: ${accessFormText(method.access)}.`, ...rowless];
559
744
  }
560
- function methodSection(name, method, access) {
561
- const described = method.describe === void 0 ? [] : [paragraph(method.describe)];
745
+ function methodSection(service, name, method, access) {
562
746
  return section(`### ${code(name)}`, [
563
- ...described,
747
+ ...described(method.describe),
564
748
  kindLine(method),
749
+ methodFacts(service, name, method, access),
565
750
  ...methodAccess(name, access),
566
751
  ...inputBlocks(method),
567
752
  `Output: ${outputText(method.output)}.`
@@ -575,7 +760,7 @@ function methodsSection(contract, access) {
575
760
  return [
576
761
  "## Methods",
577
762
  "",
578
- ...entries.flatMap(([name, method]) => methodSection(name, method, access))
763
+ ...entries.flatMap(([name, method]) => methodSection(contract.name, name, method, access))
579
764
  ];
580
765
  }
581
766
  function scopeText(scope) {
@@ -603,6 +788,7 @@ function collectionSection(name, collection, access) {
603
788
  const maxLimit = collection.maxLimit ?? DEFAULT_COLLECTION_MAX_LIMIT;
604
789
  const served = access?.service?.collections.get(name);
605
790
  return section(`### ${code(name)}`, [
791
+ ...described(collection.describe),
606
792
  optionTable([
607
793
  ["Scope", scopeText(collection.scope)],
608
794
  ["Item", code(collection.item)],
@@ -673,6 +859,7 @@ function streamSection(name, stream, access) {
673
859
  const scoped = isScopedStream(stream);
674
860
  const served = access?.service?.streams.get(name);
675
861
  return section(`### ${code(name)}`, [
862
+ ...described(stream.describe),
676
863
  optionTable([
677
864
  ["Item", code(typeOf(stream.item, "output"))],
678
865
  ["Scope", scoped ? `one feed per ${code(stream.scope ?? "")}` : "one global feed"],
@@ -700,6 +887,7 @@ function channelSection(name, channel, access) {
700
887
  const rate = channel.ratePerSecond ?? CHANNEL_DEFAULT_RATE;
701
888
  const served = access?.service?.channels.has(name) === true;
702
889
  return section(`### ${code(name)}`, [
890
+ ...described(channel.describe),
703
891
  optionTable([
704
892
  ["Payload", code(typeOf(channel.payload, "input"))],
705
893
  [
@@ -717,6 +905,7 @@ function channelSection(name, channel, access) {
717
905
  }
718
906
  function eventSection(name, event) {
719
907
  return section(`### ${code(name)}`, [
908
+ ...described(event.describe),
720
909
  optionTable([["Payload", code(typeOf(event.payload, "output"))]])
721
910
  ]);
722
911
  }
@@ -745,6 +934,10 @@ function accessSection(contract, access) {
745
934
  "Service-wide Admin",
746
935
  service.adminBypass ? "passes every check of the service" : `passes only the forms that name ${code("service")} (${code("adminBypass: false")})`
747
936
  ],
937
+ [
938
+ "Principal kinds",
939
+ service.kinds === void 0 ? void 0 : `${list(service.kinds)}: its methods, subscriptions and channels refuse every other kind`
940
+ ],
748
941
  ["Change topic", watchAccessText(service.watchAccess)],
749
942
  ["Field levels", fieldLevelsText(contract)]
750
943
  ]),
@@ -757,7 +950,9 @@ function renderService(contract, access) {
757
950
  "",
758
951
  `# ${contract.name}`,
759
952
  "",
953
+ ...described(contract.describe).flatMap((text) => [text, ""]),
760
954
  ...accessSection(contract, access),
955
+ ...access?.dependencies === void 0 ? [] : dependsOnSection(access.dependencies),
761
956
  ...entitySection(contract),
762
957
  ...projectionsSection(contract),
763
958
  ...methodsSection(contract, access),
@@ -787,10 +982,12 @@ function renderService(contract, access) {
787
982
  function fileOf(contract) {
788
983
  return `${contract.name.replace(/[^\w.-]/g, "-")}.md`;
789
984
  }
790
- function renderIndex(contracts) {
985
+ function renderIndex(contracts, dependencies) {
791
986
  const count = (members) => String(Object.keys(members).length);
987
+ const hasDescribe = contracts.some((contract) => contract.describe !== void 0);
792
988
  const rows = contracts.map((contract) => [
793
989
  `[${contract.name}](${fileOf(contract)})`,
990
+ ...hasDescribe ? [contract.describe ?? ""] : [],
794
991
  count(contract.methods),
795
992
  count(contract.collections),
796
993
  count(contract.streams),
@@ -804,9 +1001,21 @@ function renderIndex(contracts) {
804
1001
  "",
805
1002
  "One page per service, generated from its contract by `quickdraw-docs`.",
806
1003
  "",
807
- ...table(["Service", "Methods", "Collections", "Streams", "Channels", "Events"], rows)
1004
+ ...table(
1005
+ [
1006
+ "Service",
1007
+ ...hasDescribe ? ["Description"] : [],
1008
+ "Methods",
1009
+ "Collections",
1010
+ "Streams",
1011
+ "Channels",
1012
+ "Events"
1013
+ ],
1014
+ rows
1015
+ ),
1016
+ ...dependencies === void 0 ? [] : ["", ...dependencyGraph(dependencies)]
808
1017
  ];
809
- return `${lines.join("\n")}
1018
+ return `${lines.join("\n").trimEnd()}
810
1019
  `;
811
1020
  }
812
1021
 
@@ -815,7 +1024,8 @@ var USAGE = `Usage: quickdraw-docs <module> [--services <module>] [--out <dir>]
815
1024
 
816
1025
  <module> a module exporting the app's contracts: each one, or a map of them
817
1026
  --services <module> a module exporting the services (each, or a list of them): the pages
818
- then also say who may call each method, from the services' access
1027
+ then also say who may call each method, from the services' access,
1028
+ and which services each depends on, with a graph on the index
819
1029
  --out <dir> where the pages go: one per service, plus README.md (default: docs/api)
820
1030
  --check write nothing; exit 1 when the pages on disk differ from the contracts
821
1031
  `;
@@ -829,11 +1039,11 @@ var CONTRACT_KEYS = [
829
1039
  "channels",
830
1040
  "events"
831
1041
  ];
832
- function isRecord3(value) {
1042
+ function isRecord4(value) {
833
1043
  return typeof value === "object" && value !== null && !Array.isArray(value);
834
1044
  }
835
1045
  function isContract(value) {
836
- return isRecord3(value) && Object.isFrozen(value) && typeof value.name === "string" && "entity" in value && CONTRACT_KEYS.every((key) => isRecord3(value[key]));
1046
+ return isRecord4(value) && Object.isFrozen(value) && typeof value.name === "string" && "entity" in value && CONTRACT_KEYS.every((key) => isRecord4(value[key]));
837
1047
  }
838
1048
  function contractsOf(exports) {
839
1049
  const found = /* @__PURE__ */ new Map();
@@ -847,7 +1057,7 @@ function contractsOf(exports) {
847
1057
  for (const value of Object.values(exports)) {
848
1058
  if (isContract(value)) {
849
1059
  add(value);
850
- } else if (isRecord3(value)) {
1060
+ } else if (isRecord4(value)) {
851
1061
  Object.values(value).filter(isContract).forEach(add);
852
1062
  }
853
1063
  }
@@ -864,6 +1074,7 @@ function generateDocs(contracts, options = {}) {
864
1074
  );
865
1075
  }
866
1076
  }
1077
+ const dependencies = services === void 0 ? void 0 : dependenciesOf(contracts, services);
867
1078
  const files = /* @__PURE__ */ new Map();
868
1079
  for (const contract of contracts) {
869
1080
  const file = fileOf(contract);
@@ -874,11 +1085,14 @@ function generateDocs(contracts, options = {}) {
874
1085
  file,
875
1086
  renderService(
876
1087
  contract,
877
- services === void 0 ? void 0 : { service: services.get(contract.name) }
1088
+ services === void 0 ? void 0 : {
1089
+ service: services.get(contract.name),
1090
+ dependencies: dependencies?.get(contract.name)
1091
+ }
878
1092
  )
879
1093
  );
880
1094
  }
881
- files.set(INDEX_FILE, renderIndex(contracts));
1095
+ files.set(INDEX_FILE, renderIndex(contracts, dependencies));
882
1096
  return files;
883
1097
  }
884
1098
  function readText(path) {