@fitzzero/quickdraw-core 5.0.1 → 5.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 (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-3JNMVB2V.js → chunk-2UELNHDD.js} +2 -2
  9. package/dist/{chunk-HQ6WIRE2.js → chunk-3674EIRT.js} +4 -4
  10. package/dist/{chunk-NS5PUHIZ.js → chunk-A6DJBGOG.js} +130 -23
  11. package/dist/chunk-A6DJBGOG.js.map +1 -0
  12. package/dist/{chunk-ZACRRYWL.js → chunk-JZBCLNDU.js} +4 -4
  13. package/dist/{chunk-O2PWC2YW.js → chunk-KNDDCVQJ.js} +3 -3
  14. package/dist/{chunk-5YRQ26LF.js → chunk-RT6OILOI.js} +1 -1
  15. package/dist/chunk-RT6OILOI.js.map +1 -0
  16. package/dist/{chunk-WVIRXYI4.js → chunk-VJTFXHOK.js} +2 -2
  17. package/dist/{chunk-WVIRXYI4.js.map → chunk-VJTFXHOK.js.map} +1 -1
  18. package/dist/chunk-XNLWBZIG.js +9 -0
  19. package/dist/chunk-XNLWBZIG.js.map +1 -0
  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-3JNMVB2V.js.map → chunk-2UELNHDD.js.map} +0 -0
  58. /package/dist/{chunk-HQ6WIRE2.js.map → chunk-3674EIRT.js.map} +0 -0
  59. /package/dist/{chunk-ZACRRYWL.js.map → chunk-JZBCLNDU.js.map} +0 -0
  60. /package/dist/{chunk-O2PWC2YW.js.map → chunk-KNDDCVQJ.js.map} +0 -0
@@ -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) {