@wildo-ai/saas-technical-doc 1.1.2 → 1.1.3

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 (96) hide show
  1. package/dist/esm/companion/application-documentation/application-connection-documentation.d.ts +25 -1
  2. package/dist/esm/companion/application-documentation/application-connection-documentation.d.ts.map +1 -1
  3. package/dist/esm/companion/application-documentation/application-connection-documentation.js +28 -1
  4. package/dist/esm/companion/application-documentation/application-connection-documentation.js.map +1 -1
  5. package/dist/esm/companion/application-documentation/application-domain-documentation.d.ts +133 -0
  6. package/dist/esm/companion/application-documentation/application-domain-documentation.d.ts.map +1 -0
  7. package/dist/esm/companion/application-documentation/application-domain-documentation.js +243 -0
  8. package/dist/esm/companion/application-documentation/application-domain-documentation.js.map +1 -0
  9. package/dist/esm/companion/application-documentation/application-integration-documentation.d.ts.map +1 -1
  10. package/dist/esm/companion/application-documentation/application-integration-documentation.js +23 -0
  11. package/dist/esm/companion/application-documentation/application-integration-documentation.js.map +1 -1
  12. package/dist/esm/companion/application-documentation/application-organization-role-documentation.d.ts +30 -0
  13. package/dist/esm/companion/application-documentation/application-organization-role-documentation.d.ts.map +1 -1
  14. package/dist/esm/companion/application-documentation/application-organization-role-documentation.js +38 -0
  15. package/dist/esm/companion/application-documentation/application-organization-role-documentation.js.map +1 -1
  16. package/dist/esm/companion/application-documentation/technical-documentation-engine-content-bundle.d.ts +61 -1
  17. package/dist/esm/companion/application-documentation/technical-documentation-engine-content-bundle.d.ts.map +1 -1
  18. package/dist/esm/companion/application-documentation/technical-documentation-engine-content-bundle.js +255 -218
  19. package/dist/esm/companion/application-documentation/technical-documentation-engine-content-bundle.js.map +1 -1
  20. package/dist/esm/companion/application-documentation/technical-documentation-private-derivation.d.ts +2 -0
  21. package/dist/esm/companion/application-documentation/technical-documentation-private-derivation.d.ts.map +1 -1
  22. package/dist/esm/companion/application-documentation/technical-documentation-private-derivation.js +1 -0
  23. package/dist/esm/companion/application-documentation/technical-documentation-private-derivation.js.map +1 -1
  24. package/dist/esm/companion/application-documentation/technical-documentation-publication-policy.js +1 -1
  25. package/dist/esm/companion/application-documentation/technical-documentation-publication-policy.js.map +1 -1
  26. package/dist/esm/companion/index.d.ts +2 -1
  27. package/dist/esm/companion/index.d.ts.map +1 -1
  28. package/dist/esm/companion/index.js +2 -1
  29. package/dist/esm/companion/index.js.map +1 -1
  30. package/dist/esm/companion/manual-controller-route-projection.d.ts +112 -0
  31. package/dist/esm/companion/manual-controller-route-projection.d.ts.map +1 -0
  32. package/dist/esm/companion/manual-controller-route-projection.js +249 -0
  33. package/dist/esm/companion/manual-controller-route-projection.js.map +1 -0
  34. package/dist/esm/companion/openapi-generator.d.ts +16 -0
  35. package/dist/esm/companion/openapi-generator.d.ts.map +1 -1
  36. package/dist/esm/companion/openapi-generator.js +489 -26
  37. package/dist/esm/companion/openapi-generator.js.map +1 -1
  38. package/dist/esm/companion/operation-projection.schemas.d.ts +44 -0
  39. package/dist/esm/companion/operation-projection.schemas.d.ts.map +1 -1
  40. package/dist/esm/companion/operation-projection.schemas.js +37 -0
  41. package/dist/esm/companion/operation-projection.schemas.js.map +1 -1
  42. package/dist/esm/companion/rendering/technical-documentation-docusaurus-renderer.d.ts +7 -1
  43. package/dist/esm/companion/rendering/technical-documentation-docusaurus-renderer.d.ts.map +1 -1
  44. package/dist/esm/companion/rendering/technical-documentation-docusaurus-renderer.js +34 -18
  45. package/dist/esm/companion/rendering/technical-documentation-docusaurus-renderer.js.map +1 -1
  46. package/dist/esm/companion/rendering/technical-documentation-managed-tree-validator.d.ts.map +1 -1
  47. package/dist/esm/companion/rendering/technical-documentation-managed-tree-validator.js +8 -1
  48. package/dist/esm/companion/rendering/technical-documentation-managed-tree-validator.js.map +1 -1
  49. package/dist/esm/companion/rendering/technical-documentation-markdown-renderer.d.ts +0 -9
  50. package/dist/esm/companion/rendering/technical-documentation-markdown-renderer.d.ts.map +0 -1
  51. package/dist/esm/companion/rendering/technical-documentation-markdown-renderer.js +0 -111
  52. package/dist/esm/companion/rendering/technical-documentation-markdown-renderer.js.map +0 -1
  53. package/dist/esm/companion/rendering/technical-documentation-render-model.d.ts.map +1 -1
  54. package/dist/esm/companion/rendering/technical-documentation-render-model.js +30 -13
  55. package/dist/esm/companion/rendering/technical-documentation-render-model.js.map +1 -1
  56. package/dist/esm/companion/rendering/technical-documentation-search-index-renderer.d.ts +10 -0
  57. package/dist/esm/companion/rendering/technical-documentation-search-index-renderer.d.ts.map +1 -1
  58. package/dist/esm/companion/rendering/technical-documentation-search-index-renderer.js +10 -15
  59. package/dist/esm/companion/rendering/technical-documentation-search-index-renderer.js.map +1 -1
  60. package/dist/esm/companion/technical-documentation-capture-execution-port.d.ts +27 -1
  61. package/dist/esm/companion/technical-documentation-capture-execution-port.d.ts.map +1 -1
  62. package/dist/esm/companion/technical-documentation-capture-execution-port.js.map +1 -1
  63. package/dist/esm/companion/technical-documentation-diagram-definitions.d.ts +33 -0
  64. package/dist/esm/companion/technical-documentation-diagram-definitions.d.ts.map +1 -0
  65. package/dist/esm/companion/technical-documentation-diagram-definitions.js +54 -0
  66. package/dist/esm/companion/technical-documentation-diagram-definitions.js.map +1 -0
  67. package/dist/esm/companion/technical-documentation-diagram-materializer.d.ts +10 -18
  68. package/dist/esm/companion/technical-documentation-diagram-materializer.d.ts.map +1 -1
  69. package/dist/esm/companion/technical-documentation-diagram-materializer.js +9 -39
  70. package/dist/esm/companion/technical-documentation-diagram-materializer.js.map +1 -1
  71. package/dist/esm/config/wildo-tech-doc-config.schemas.d.ts +8 -4
  72. package/dist/esm/config/wildo-tech-doc-config.schemas.d.ts.map +1 -1
  73. package/dist/esm/config/wildo-tech-doc-config.schemas.js +8 -4
  74. package/dist/esm/config/wildo-tech-doc-config.schemas.js.map +1 -1
  75. package/dist/esm/content/application-consumer-documentation-content.techdoc.d.ts +42 -64
  76. package/dist/esm/content/application-consumer-documentation-content.techdoc.d.ts.map +1 -1
  77. package/dist/esm/content/application-consumer-documentation-content.techdoc.js +164 -925
  78. package/dist/esm/content/application-consumer-documentation-content.techdoc.js.map +1 -1
  79. package/dist/esm/runtime/DocsAuthContext.d.ts +16 -1
  80. package/dist/esm/runtime/DocsAuthContext.d.ts.map +1 -1
  81. package/dist/esm/runtime/DocsAuthContext.js +18 -2
  82. package/dist/esm/runtime/DocsAuthContext.js.map +1 -1
  83. package/dist/esm/runtime/frontend-provider-registry.techdoc.d.ts +35 -13
  84. package/dist/esm/runtime/frontend-provider-registry.techdoc.d.ts.map +1 -1
  85. package/dist/esm/runtime/frontend-provider-registry.techdoc.js +28 -19
  86. package/dist/esm/runtime/frontend-provider-registry.techdoc.js.map +1 -1
  87. package/dist/esm/runtime/index.d.ts +1 -0
  88. package/dist/esm/runtime/index.d.ts.map +1 -1
  89. package/dist/esm/runtime/index.js +1 -0
  90. package/dist/esm/runtime/index.js.map +1 -1
  91. package/dist/esm/runtime/use-docs-provider-sdks.d.ts +21 -0
  92. package/dist/esm/runtime/use-docs-provider-sdks.d.ts.map +1 -0
  93. package/dist/esm/runtime/use-docs-provider-sdks.js +49 -0
  94. package/dist/esm/runtime/use-docs-provider-sdks.js.map +1 -0
  95. package/dist/tsconfig.build.tsbuildinfo +1 -1
  96. package/package.json +6 -5
@@ -41,7 +41,7 @@
41
41
  * barrel enters this semantic generator.
42
42
  */
43
43
  import { WildoHeaderKeys } from '@wildo-ai/saas-models/public-runtime';
44
- import { COLLECTION_QUERY_PARAMETERS, Resources_PaginationRequestSchema } from '@wildo-ai/saas-models';
44
+ import { COLLECTION_QUERY_PARAMETERS, ERROR_DEFINITIONS, ErrorHandling_Strategy, ErrorSeverity, ErrorType, M2M_WEBHOOK_DELIVERY_CONTRACT, Resources_PaginationRequestSchema, } from '@wildo-ai/saas-models';
45
45
  import { OpenApiSection, } from '../openapi/openapi-generation-output.schemas.js';
46
46
  import { OpenApiGenerationInputSchema, OPENAPI_RESOURCE_IDENTITY_CONTRACT_VERSION, OperationProjectionAuthenticationMode, OperationProjectionVariantType, } from './operation-projection.schemas.js';
47
47
  import { stripImplementationNoise } from './spec-to-operation-doc.js';
@@ -345,6 +345,58 @@ function extractPathParameters(path) {
345
345
  const matches = path.matchAll(/\{([^}]+)\}/gu);
346
346
  return Array.from(matches, (match) => match[1]);
347
347
  }
348
+ /**
349
+ * What a path placeholder identifies, read off the path rather than guessed from its name.
350
+ *
351
+ * Measured on Wonder CRM: 0 of 911 path parameters carried a description while query parameters
352
+ * were 85% covered and headers 100% — so a reader looking at `/organizations/{organizationId}/deal/
353
+ * {dealId}` was told what every filter meant and nothing about the two values they must supply.
354
+ *
355
+ * The segment BEFORE a placeholder names the collection it addresses, which is the fact that makes
356
+ * this derivable rather than invented. The last placeholder addresses the operation's own resource;
357
+ * the earlier ones are the scope it lives in, and saying so is what tells a caller which id goes
358
+ * where. The collection name is quoted verbatim, never humanized: it is the identifier the API
359
+ * itself uses, so a reader can match it against the path, the tag and the payload.
360
+ */
361
+ function describePathParameter(path, parameterName, resourceIdentifier) {
362
+ const segments = path.split('/').filter((segment) => segment.length > 0);
363
+ const placeholderIndex = segments.indexOf(`{${parameterName}}`);
364
+ const owningCollection = placeholderIndex > 0 ? segments[placeholderIndex - 1] : undefined;
365
+ if (owningCollection === undefined || owningCollection.startsWith('{')) {
366
+ return `Identifier supplied in the request path as \`${parameterName}\`.`;
367
+ }
368
+ return owningCollection === resourceIdentifier
369
+ ? `Identifier of the \`${owningCollection}\` this operation addresses.`
370
+ : `Identifier of the \`${owningCollection}\` the addressed record belongs to.`;
371
+ }
372
+ /**
373
+ * Splits an operation's qualifier tail into the two kinds that look alike and mean opposite things.
374
+ *
375
+ * `Via<Parent>` qualifiers are ROUTING: the same operation reached through a different parent, so
376
+ * `GET /organizations/{organizationId}/organization-members/{id}` and
377
+ * `GET /users/{userId}/organization-members/{id}` are one operation at two addresses. Every other
378
+ * qualifier — `summary`, `context`, `BULK` — is a genuinely different operation with a different
379
+ * response.
380
+ *
381
+ * Conflating them is what makes the reference look four times its real size. Measured on Wonder
382
+ * CRM: 546 operations at 439 paths, of which 486 are another path's operation repeated —
383
+ * `organizationMembers` publishes its whole surface three times.
384
+ */
385
+ function splitOperationQualifiers(op) {
386
+ const tail = op.operationKey.startsWith(op.baseOperationIdentifier)
387
+ ? op.operationKey.slice(op.baseOperationIdentifier.length)
388
+ : '';
389
+ const segments = tail.split('_').filter(Boolean);
390
+ return {
391
+ via: segments.filter((segment) => segment.startsWith('Via')),
392
+ shape: segments.filter((segment) => !segment.startsWith('Via')),
393
+ };
394
+ }
395
+ /** The identity an alias shares with the one canonical route of the same operation. */
396
+ function canonicalRouteKey(op) {
397
+ const { shape } = splitOperationQualifiers(op);
398
+ return [op.resourceIdentifier, op.baseOperationIdentifier, shape.join('_'), op.httpVerb].join('\u0000');
399
+ }
348
400
  function isPlainObject(value) {
349
401
  return typeof value === 'object' && value !== null && !Array.isArray(value);
350
402
  }
@@ -738,6 +790,26 @@ function buildOperationObject(op, operationId, errorResponseSchema) {
738
790
  // Auth (Tier-1 framework knowledge). The source projector resolves access
739
791
  // semantics once; this generator only transports that fact into standard
740
792
  // OpenAPI `security` and the companion `x-wildo` extension.
793
+ /*
794
+ * Say that this call notifies, where the reader is standing.
795
+ *
796
+ * A developer looking at `changeStatus` needs to know it does more than change a record: every
797
+ * endpoint configured on that channel receives a delivery. The mechanics — retries, signature,
798
+ * how to verify — are the webhook contract's and are described once at the document's `webhooks`
799
+ * block, not restated on each of the operations that fire one.
800
+ */
801
+ const notifications = op.m2mNotifications ?? [];
802
+ if (notifications.length > 0) {
803
+ const channels = [...new Set(notifications.map((notification) => notification.channel))].sort();
804
+ const note = `A successful call also delivers an outbound webhook on ${channels.length === 1 ? 'the ' : ''}`
805
+ + `${channels.map((channel) => `\`${channel}\``).join(' and ')} channel${channels.length === 1 ? '' : 's'}`
806
+ + ', to every endpoint an organization has configured and enabled there. See this document\'s `webhooks` for the delivery contract.';
807
+ const existing = typeof operationObject['description'] === 'string' ? operationObject['description'] : '';
808
+ operationObject['description'] = existing.length > 0 ? `${existing}\n\n${note}` : note;
809
+ const wildo = operationObject['x-wildo'];
810
+ if (isPlainObject(wildo))
811
+ wildo['m2mNotifications'] = notifications.map((notification) => ({ ...notification }));
812
+ }
741
813
  const security = buildOperationSecurity(op);
742
814
  operationObject['security'] = security;
743
815
  if (op.access.authenticationMode !== OperationProjectionAuthenticationMode.PUBLIC && op.access.roleRequirements.length > 0) {
@@ -751,6 +823,7 @@ function buildOperationObject(op, operationId, errorResponseSchema) {
751
823
  in: 'path',
752
824
  required: true,
753
825
  schema: { type: 'string' },
826
+ description: describePathParameter(op.path, paramName, op.resourceIdentifier),
754
827
  })));
755
828
  }
756
829
  if (op.stepUpAuthentication !== undefined) {
@@ -890,6 +963,34 @@ const ENGINE_METADATA_PROPERTY_SCHEMAS = {
890
963
  + 'conflict-free merges of concurrent edits. Opaque to clients — safe to ignore.',
891
964
  },
892
965
  };
966
+ /**
967
+ * Descriptions for the properties EVERY resource carries and NO resource authors.
968
+ *
969
+ * `_version` and `_field_meta` above are injected by the generator, so they were described where
970
+ * they are created. These are different: they are real fields on the document, present on every
971
+ * resource, and therefore owned by no single resource specification — which is exactly why they
972
+ * were the most undescribed properties in the whole reference. Measured on Wonder CRM:
973
+ *
974
+ * data 123 · pagination 123 · retentionStatus 104 · impersonalizedAt 104 · _id 90 ·
975
+ * createdAt 65 · updatedAt 64 · organizationId 48
976
+ *
977
+ * — 721 of the 1,091 undescribed top-level properties, from eight names that mean the same thing
978
+ * everywhere. Asking every resource to describe them would be hundreds of copies of one sentence.
979
+ *
980
+ * An AUTHORED description always wins: this fills a gap, it never overrides a specification. That
981
+ * matters for names an application could legitimately reuse — a resource of its own with a field
982
+ * called `data` describes it, and this map stays out of the way.
983
+ */
984
+ const FRAMEWORK_OWNED_PROPERTY_DESCRIPTIONS = Object.freeze({
985
+ _id: 'The record’s identifier. Stable for the life of the record and the value every path parameter and reference uses.',
986
+ createdAt: 'When the record was created, set by the application and never accepted from a client.',
987
+ updatedAt: 'When the record last changed, set by the application on every write.',
988
+ organizationId: 'The organization this record belongs to. It scopes every read and write: a caller only ever addresses records in an organization they are a member of.',
989
+ retentionStatus: 'Whether the record is still `active` or has been `retained` — kept for a legal or accounting obligation after the person it concerned was erased. A retained record has had its personal detail removed.',
990
+ impersonalizedAt: 'When personal detail was stripped from this record, if it ever was. Absent on a record that has not been through erasure.',
991
+ data: 'The page of records this response carries.',
992
+ pagination: 'Where this page sits in the full result set — the page number, the page size, and the totals needed to ask for the next one.',
993
+ });
893
994
  /**
894
995
  * The structural signature of a MATERIALIZED resource document in a response
895
996
  * schema is the `_id` property. The serializer attaches `_version` /
@@ -955,11 +1056,47 @@ function injectEngineMetadataIntoEntitySchemas(schema) {
955
1056
  * (`stableStringify`) still collapses them onto one component. Non-object
956
1057
  * schemas (`null` / scalar) pass through unchanged.
957
1058
  */
1059
+ /**
1060
+ * Fills a description on the properties every resource carries and none authors.
1061
+ *
1062
+ * Recursive, because these appear at every depth: `data` and `pagination` wrap the envelope,
1063
+ * `_id` and `createdAt` sit on the document inside it, and a `context`-mode response nests another
1064
+ * collection again. Never overrides an authored description — a specification that describes its
1065
+ * own field wins, which is what keeps this safe for a name an application might reuse.
1066
+ */
1067
+ function fillFrameworkOwnedPropertyDescriptions(node) {
1068
+ if (Array.isArray(node)) {
1069
+ for (const entry of node)
1070
+ fillFrameworkOwnedPropertyDescriptions(entry);
1071
+ return;
1072
+ }
1073
+ if (!isPlainObject(node))
1074
+ return;
1075
+ const properties = node['properties'];
1076
+ if (isPlainObject(properties)) {
1077
+ for (const [name, property] of Object.entries(properties)) {
1078
+ if (!isPlainObject(property))
1079
+ continue;
1080
+ const framework = FRAMEWORK_OWNED_PROPERTY_DESCRIPTIONS[name];
1081
+ if (framework !== undefined && property['description'] === undefined) {
1082
+ property['description'] = framework;
1083
+ }
1084
+ fillFrameworkOwnedPropertyDescriptions(property);
1085
+ }
1086
+ }
1087
+ for (const key of ['items', 'allOf', 'oneOf', 'anyOf', 'additionalProperties']) {
1088
+ if (key in node)
1089
+ fillFrameworkOwnedPropertyDescriptions(node[key]);
1090
+ }
1091
+ }
958
1092
  function augmentResponseSchemaWithEngineMetadata(responseBodySchema) {
959
1093
  if (!isPlainObject(responseBodySchema)) {
960
1094
  return responseBodySchema;
961
1095
  }
962
1096
  const clone = structuredClone(responseBodySchema);
1097
+ // Safe to mutate: `clone` is this function's own deep copy, and the caller's schema is the
1098
+ // operation projection's — a generator that edited it would be editing the data it was handed.
1099
+ fillFrameworkOwnedPropertyDescriptions(clone);
963
1100
  const properties = clone['properties'];
964
1101
  const dataSchema = isPlainObject(properties) ? properties['data'] : undefined;
965
1102
  if (isPlainObject(dataSchema) && (dataSchema['type'] === 'array' || 'items' in dataSchema)) {
@@ -1093,27 +1230,151 @@ function buildResponsesObject(op, responseBodySchema, errorResponseSchema) {
1093
1230
  // 401 contract. A resource-owned 401 is more specific (for example a
1094
1231
  // step-up flow) and deliberately takes precedence.
1095
1232
  if (op.access.authenticationMode !== OperationProjectionAuthenticationMode.PUBLIC && responses['401'] === undefined) {
1096
- const description = (() => {
1097
- switch (op.access.authenticationMode) {
1098
- case OperationProjectionAuthenticationMode.ANONYMOUS_SESSION:
1099
- return 'The anonymous-session token is missing, malformed, expired, or invalid.';
1100
- case OperationProjectionAuthenticationMode.ANONYMOUS_OR_AUTHENTICATED:
1101
- return 'No valid anonymous-session token, bearer token, or API key was supplied.';
1102
- case OperationProjectionAuthenticationMode.AUTHENTICATED:
1103
- return 'No valid bearer token or API key was supplied.';
1104
- default: {
1105
- const exhaustiveAuthenticationMode = op.access.authenticationMode;
1106
- throw new Error(`OpenAPI generator: unsupported authentication mode ${String(exhaustiveAuthenticationMode)}`);
1107
- }
1108
- }
1109
- })();
1110
- responses['401'] = {
1111
- description,
1112
- content: { 'application/json': { schema: errorResponseSchema } },
1113
- };
1233
+ // Referenced, not inlined: the wording is the generator's and identical for every operation
1234
+ // with the same access mode, so `unauthorizedDescription` states it once and the component
1235
+ // carries it. Repeating the content block on 546 operations said nothing a reader could use.
1236
+ responses['401'] = { $ref: `#/components/responses/${unauthorizedResponseName(op.access.authenticationMode)}` };
1237
+ }
1238
+ /*
1239
+ * Every operation can fail inside the application, and not one said so. Added last and only when
1240
+ * absent, so an authored 500 — which can say something specific about THIS operation — wins.
1241
+ */
1242
+ if (responses['500'] === undefined) {
1243
+ responses['500'] = { $ref: `#/components/responses/${INTERNAL_ERROR_RESPONSE_NAME}` };
1114
1244
  }
1115
1245
  return responses;
1116
1246
  }
1247
+ /**
1248
+ * The error responses the GENERATOR owns, named once in `components.responses`.
1249
+ *
1250
+ * Only the ones whose wording is the generator's own are shared. An authored response carries a
1251
+ * description written for that one operation — "No release record exists for the supplied id in the
1252
+ * caller's scope" — and that sentence is the most useful prose in the document; folding it into a
1253
+ * shared component would replace it with something generic, so authored responses stay inline.
1254
+ *
1255
+ * The 401 wording is derived from the operation's ACCESS MODE, which is why there are three: the
1256
+ * credential a caller is missing differs, and saying "no valid bearer token" to an anonymous-session
1257
+ * endpoint would be wrong.
1258
+ */
1259
+ const GENERATOR_OWNED_ERROR_RESPONSE_NAMES = Object.freeze({
1260
+ [OperationProjectionAuthenticationMode.ANONYMOUS_SESSION]: 'UnauthorizedAnonymousSession',
1261
+ [OperationProjectionAuthenticationMode.ANONYMOUS_OR_AUTHENTICATED]: 'UnauthorizedAnonymousOrAuthenticated',
1262
+ [OperationProjectionAuthenticationMode.AUTHENTICATED]: 'Unauthorized',
1263
+ [OperationProjectionAuthenticationMode.PUBLIC]: 'Unauthorized',
1264
+ });
1265
+ const INTERNAL_ERROR_RESPONSE_NAME = 'InternalServerError';
1266
+ function unauthorizedResponseName(mode) {
1267
+ return GENERATOR_OWNED_ERROR_RESPONSE_NAMES[mode];
1268
+ }
1269
+ /** The one place each 401 wording is written; the components below are built from it. */
1270
+ function unauthorizedDescription(mode) {
1271
+ switch (mode) {
1272
+ case OperationProjectionAuthenticationMode.ANONYMOUS_SESSION:
1273
+ return 'The anonymous-session token is missing, malformed, expired, or invalid.';
1274
+ case OperationProjectionAuthenticationMode.ANONYMOUS_OR_AUTHENTICATED:
1275
+ return 'No valid anonymous-session token, bearer token, or API key was supplied.';
1276
+ case OperationProjectionAuthenticationMode.AUTHENTICATED:
1277
+ case OperationProjectionAuthenticationMode.PUBLIC:
1278
+ return 'No valid bearer token or API key was supplied.';
1279
+ default: {
1280
+ const exhaustiveAuthenticationMode = mode;
1281
+ throw new Error(`OpenAPI generator: unsupported authentication mode ${String(exhaustiveAuthenticationMode)}`);
1282
+ }
1283
+ }
1284
+ }
1285
+ /**
1286
+ * The `components.responses` block, built from the same wording the operations reference.
1287
+ *
1288
+ * Emitted only for the names an operation actually used, so a document never carries a component
1289
+ * nothing points at — the reason `PUBLIC` shares the authenticated name rather than earning an
1290
+ * entry of its own: a public operation never gets a synthesised 401.
1291
+ */
1292
+ function buildErrorResponseComponents(usedNames, errorResponseSchema) {
1293
+ const content = { 'application/json': { schema: errorResponseSchema } };
1294
+ const all = { [INTERNAL_ERROR_RESPONSE_NAME]: { description: internalServerErrorDescription(), content } };
1295
+ for (const mode of [
1296
+ OperationProjectionAuthenticationMode.ANONYMOUS_SESSION,
1297
+ OperationProjectionAuthenticationMode.ANONYMOUS_OR_AUTHENTICATED,
1298
+ OperationProjectionAuthenticationMode.AUTHENTICATED,
1299
+ ]) {
1300
+ all[unauthorizedResponseName(mode)] = { description: unauthorizedDescription(mode), content };
1301
+ }
1302
+ return Object.fromEntries(Object.entries(all).filter(([name]) => usedNames.has(name)));
1303
+ }
1304
+ /**
1305
+ * Every operation can fail with the framework's error envelope, and until now not one of the 546
1306
+ * published operations said so — measured on Wonder CRM, `500` appeared zero times.
1307
+ *
1308
+ * Synthesised rather than authored, for the same reason the 401 is: it is true of every operation
1309
+ * by construction, so making each specification restate it would be 546 copies of one fact with 546
1310
+ * chances to drift. An authored 500 takes precedence, exactly as an authored 401 does.
1311
+ */
1312
+ function internalServerErrorDescription() {
1313
+ return 'The request failed inside the application. The body is the standard error envelope and carries a `correlationId` to quote when reporting it; the request was not necessarily rejected, so do not assume it had no effect until you have checked.';
1314
+ }
1315
+ /**
1316
+ * The document's `webhooks` block — what a RECEIVER is sent, described where OpenAPI 3.1 puts it.
1317
+ *
1318
+ * Until now the reference described only what a client CALLS. An integrator writing the other half
1319
+ * — the endpoint this application posts to — had no machine-readable contract at all, and every
1320
+ * operation that fires a delivery said nothing about it.
1321
+ *
1322
+ * Two facts make these entries honest rather than plausible:
1323
+ *
1324
+ * · THE BODY IS THE RESOURCE DOCUMENT ITSELF, not an envelope around it. The delivery worker
1325
+ * posts the stored payload verbatim, and a backend test exists specifically to fail if anyone
1326
+ * wraps it — because the body's bytes must hash to the signature's `bodyHash` claim. So the
1327
+ * schema referenced here is the resource's own read response, and when that component cannot
1328
+ * be resolved the entry carries prose instead of a shape it cannot prove.
1329
+ * · THE DELIVERY CONTRACT IS ONE OBJECT. Retries, timeout, signature algorithm and claim names
1330
+ * come from `M2M_WEBHOOK_DELIVERY_CONTRACT`, the same source the delivery worker derives its
1331
+ * tunables from, so the published contract cannot drift from what is actually sent.
1332
+ */
1333
+ function buildWebhooksObject(operations, canonicalReadResponseRefByResource) {
1334
+ const webhooks = {};
1335
+ for (const op of operations) {
1336
+ for (const notification of op.m2mNotifications ?? []) {
1337
+ const key = `${op.resourceIdentifier}.${op.baseOperationIdentifier}`;
1338
+ if (webhooks[key] !== undefined)
1339
+ continue;
1340
+ const bodyRef = canonicalReadResponseRefByResource.get(op.resourceIdentifier);
1341
+ const contract = M2M_WEBHOOK_DELIVERY_CONTRACT;
1342
+ webhooks[key] = {
1343
+ post: {
1344
+ summary: `${op.resourceIdentifier} ${op.baseOperationIdentifier}`,
1345
+ description: [
1346
+ `Sent to every endpoint configured and enabled on the \`${notification.channel}\` channel when \`${op.resourceIdentifier}.${op.baseOperationIdentifier}\` succeeds.`,
1347
+ '',
1348
+ `The body is the record itself, exactly as the API returns it — there is no envelope around it, and the bytes you receive are the bytes the signature was computed over.`,
1349
+ '',
1350
+ `Verify before trusting it: the \`${contract.signatureHeader}\` header carries a ${contract.signatureAlgorithm} JWT whose \`${contract.signatureClaims.bodyHash}\` claim is the ${contract.bodyHashAlgorithm} digest of the raw body. Its \`${contract.signatureClaims.deliveryId}\` claim is the delivery id and is identical across every retry, so use it to deduplicate.`,
1351
+ '',
1352
+ `Answer 2xx to acknowledge. Anything else is retried up to ${contract.maxAttempts} attempts with a fixed backoff, and each attempt is abandoned after ${Math.round(contract.requestTimeoutMs / 1000)} seconds.`,
1353
+ ].join('\n'),
1354
+ 'x-wildo': {
1355
+ resourceIdentifier: op.resourceIdentifier,
1356
+ operationIdentifier: op.baseOperationIdentifier,
1357
+ eventChannel: notification.channel,
1358
+ notificationIdentifier: notification.identifier,
1359
+ },
1360
+ requestBody: {
1361
+ required: true,
1362
+ content: {
1363
+ 'application/json': bodyRef === undefined
1364
+ // No read response to point at — say what the body is rather than assert a shape.
1365
+ ? { schema: { type: 'object', description: `The \`${op.resourceIdentifier}\` record as the API returns it.` } }
1366
+ : { schema: { $ref: bodyRef } },
1367
+ },
1368
+ },
1369
+ responses: {
1370
+ '2XX': { description: 'The delivery was accepted. Any 2xx ends the delivery; the body is ignored.' },
1371
+ },
1372
+ },
1373
+ };
1374
+ }
1375
+ }
1376
+ return webhooks;
1377
+ }
1117
1378
  /**
1118
1379
  * Builds the OpenAPI request-body `examples` map from authored examples that
1119
1380
  * carry a `request` payload. Returns undefined when there are none.
@@ -1175,6 +1436,23 @@ function stableStringify(value) {
1175
1436
  }
1176
1437
  return JSON.stringify(value) ?? 'null';
1177
1438
  }
1439
+ /**
1440
+ * Which half of an operation a generated component schema models.
1441
+ *
1442
+ * A closed two-member vocabulary, named rather than inlined because the two members are not
1443
+ * symmetric and the difference is worth stating once: a REQUEST schema is what a caller must
1444
+ * SEND and is therefore the contract they are held to, while a RESPONSE schema is what the
1445
+ * application PROMISES to return. Both the schema NAME and its description are derived from
1446
+ * this value, so the member is the single input that decides how an integrator reads the card.
1447
+ *
1448
+ * The member VALUES are the PascalCase suffixes appended to a schema name (`TodosCreateRequest`),
1449
+ * so they are wire-visible in the published OpenAPI document and not free to rename.
1450
+ */
1451
+ export var OpenApiComponentSchemaRole;
1452
+ (function (OpenApiComponentSchemaRole) {
1453
+ OpenApiComponentSchemaRole["REQUEST"] = "Request";
1454
+ OpenApiComponentSchemaRole["RESPONSE"] = "Response";
1455
+ })(OpenApiComponentSchemaRole || (OpenApiComponentSchemaRole = {}));
1178
1456
  /**
1179
1457
  * Component name for an operation's request/response body. Mirrors the
1180
1458
  * verb-first operationId namer (shares `pascal`): `<Resource><BaseVerb><Quals><Role>`,
@@ -1195,15 +1473,84 @@ function componentSchemaName(op, role) {
1195
1473
  .join('');
1196
1474
  return `${pascal(op.resourceIdentifier)}${pascal(op.baseOperationIdentifier)}${schemaQualifiers}${role}`;
1197
1475
  }
1476
+ /**
1477
+ * Describe the two closed vocabularies inside the framework's own error envelope, from the
1478
+ * definitions the runtime already carries.
1479
+ *
1480
+ * `error.type` is the field an integrator branches on, and it arrived as twenty bare tokens with no
1481
+ * explanation — the ONLY enum in the document that no authoring could reach, because the sweep that
1482
+ * describes enum values reads a field specification and this schema belongs to no resource. The
1483
+ * words are not written here: `ERROR_DEFINITIONS` is a `Record<ErrorType, …>` the error builder
1484
+ * itself consults, so the sentence, the HTTP status and whether the framework's own strategy is to
1485
+ * retry all come from the same source that decides them at runtime.
1486
+ *
1487
+ * Matching is by EXACT VALUE SET, and that is only sound because it is scoped to this one
1488
+ * generator-owned schema. Across the document at large it would not be: `['active', 'expired',
1489
+ * 'inactive']` occurs on an OAuth client and on an API key with different meanings, so propagating
1490
+ * one description to the other by value set would put "Client may mint tokens" on an API key.
1491
+ *
1492
+ * `severity` gets one sentence rather than six invented ones: it drives the framework's own logging
1493
+ * and alerting, and a client has nothing to do with it.
1494
+ *
1495
+ * Returns a described COPY. The caller's schema belongs to the projection input and a pin asserts
1496
+ * the generator leaves its input untouched.
1497
+ */
1498
+ function describeFrameworkErrorEnvelopeEnums(schema) {
1499
+ if (!isPlainObject(schema))
1500
+ return schema;
1501
+ const errorTypeValues = new Set(Object.values(ErrorType));
1502
+ const severityValues = new Set(Object.values(ErrorSeverity));
1503
+ const errorTypeMeanings = {};
1504
+ for (const value of Object.values(ErrorType)) {
1505
+ const definition = ERROR_DEFINITIONS[value];
1506
+ const retries = definition.errorHandlingStrategy === ErrorHandling_Strategy.RETRY;
1507
+ errorTypeMeanings[value] = `${definition.description} (HTTP ${String(definition.statusCode)})`
1508
+ + `${retries ? ' — retrying is expected to help.' : '.'}`;
1509
+ }
1510
+ const described = structuredClone(schema);
1511
+ const visit = (node) => {
1512
+ if (Array.isArray(node)) {
1513
+ for (const entry of node)
1514
+ visit(entry);
1515
+ return;
1516
+ }
1517
+ if (!isPlainObject(node))
1518
+ return;
1519
+ const values = node['enum'];
1520
+ if (Array.isArray(values) && node['description'] === undefined) {
1521
+ const present = new Set(values.map((value) => String(value)));
1522
+ if (present.size === errorTypeValues.size && [...present].every((value) => errorTypeValues.has(value))) {
1523
+ node['description'] = `Which kind of failure this was. Branch on this rather than on wording.\n\nValues:\n`
1524
+ + Object.entries(errorTypeMeanings).map(([value, meaning]) => `- \`${value}\`: ${meaning}`).join('\n');
1525
+ node['x-enum-descriptions'] = errorTypeMeanings;
1526
+ }
1527
+ else if (present.size === severityValues.size && [...present].every((value) => severityValues.has(value))) {
1528
+ node['description'] = 'How seriously the application itself treats this failure — it drives logging and alerting '
1529
+ + 'on the server. A client has nothing to decide from it; branch on `type` and `code`.';
1530
+ }
1531
+ }
1532
+ for (const entry of Object.values(node))
1533
+ visit(entry);
1534
+ };
1535
+ visit(described);
1536
+ return described;
1537
+ }
1198
1538
  function createSchemaRegistry() {
1199
1539
  const nameBySemanticIdentityAndContent = new Map();
1200
1540
  const contentByName = new Map();
1201
1541
  const schemas = {};
1202
1542
  return {
1203
- ref(schema, desiredName) {
1543
+ ref(schema, desiredName, description) {
1204
1544
  if (!isHoistableSchema(schema)) {
1205
1545
  return schema;
1206
1546
  }
1547
+ /*
1548
+ * Canonicalised BEFORE the description is attached, deliberately. Deduplication is by
1549
+ * name-and-content, so folding the description into the identity would split one component
1550
+ * into several whenever two operations produced the same shape — the opposite of what a
1551
+ * description is for. A component name maps to exactly one desired name, hence to one
1552
+ * operation's resource, so the description attached here describes the schema that is stored.
1553
+ */
1207
1554
  const canonical = stableStringify(schema);
1208
1555
  const semanticIdentityAndContent = `${desiredName}\u0000${canonical}`;
1209
1556
  const existingName = nameBySemanticIdentityAndContent.get(semanticIdentityAndContent);
@@ -1218,7 +1565,17 @@ function createSchemaRegistry() {
1218
1565
  }
1219
1566
  nameBySemanticIdentityAndContent.set(semanticIdentityAndContent, name);
1220
1567
  contentByName.set(name, canonical);
1221
- schemas[name] = schema;
1568
+ /*
1569
+ * COPIED, never mutated. The caller's schema object belongs to the operation projection, and
1570
+ * a pin asserts the generator leaves its input untouched — writing the description onto it
1571
+ * would have made this generator edit the data it was handed, which is the one thing that
1572
+ * test exists to prevent. A shallow copy suffices: only a top-level key is added, and an
1573
+ * authored description already on the schema outranks the derived one.
1574
+ */
1575
+ const stored = description !== undefined && isPlainObject(schema) && schema['description'] === undefined
1576
+ ? { ...schema, description }
1577
+ : schema;
1578
+ schemas[name] = stored;
1222
1579
  return { $ref: `#/components/schemas/${name}` };
1223
1580
  },
1224
1581
  schemas() {
@@ -1234,12 +1591,29 @@ function createSchemaRegistry() {
1234
1591
  * inline. All success statuses share one response schema object, so they
1235
1592
  * resolve to the same `$ref`. Mutates `operationObject` in place.
1236
1593
  */
1237
- function hoistOperationSchemas(operationObject, op, registry) {
1594
+ /**
1595
+ * What a component schema IS, taken from the operation that hoisted it.
1596
+ *
1597
+ * Measured on Wonder CRM: 0 of 474 component schemas carried a description, so the schema list read
1598
+ * as 474 names — `DealReadResponse`, `DealReadContextResponse`, `DealReadSummaryResponse` — with
1599
+ * nothing to tell them apart or say what a deal is.
1600
+ *
1601
+ * Derived from the OPERATION rather than from the schema's name. 473 of the 474 names do start with
1602
+ * their resource's tag, so a prefix match would have worked and would have been a guess: the
1603
+ * hoisting operation is the authoritative link, and it costs nothing to use it. The resource's own
1604
+ * purpose is appended when its specification authored one, because a reader who reached a schema
1605
+ * card may never have seen the tag.
1606
+ */
1607
+ function describeComponentSchema(op, role, resourcePurpose) {
1608
+ const what = `${role === OpenApiComponentSchemaRole.REQUEST ? 'Request body' : 'Response body'} for \`${op.resourceIdentifier}.${op.baseOperationIdentifier}\`.`;
1609
+ return resourcePurpose === undefined ? what : `${what} ${resourcePurpose}`;
1610
+ }
1611
+ function hoistOperationSchemas(operationObject, op, registry, resourcePurpose) {
1238
1612
  const requestBody = operationObject['requestBody'];
1239
1613
  if (isPlainObject(requestBody) && isPlainObject(requestBody['content'])) {
1240
1614
  const media = requestBody['content']['application/json'];
1241
1615
  if (isPlainObject(media) && isHoistableSchema(media['schema'])) {
1242
- media['schema'] = registry.ref(media['schema'], componentSchemaName(op, 'Request'));
1616
+ media['schema'] = registry.ref(media['schema'], componentSchemaName(op, OpenApiComponentSchemaRole.REQUEST), describeComponentSchema(op, OpenApiComponentSchemaRole.REQUEST, resourcePurpose));
1243
1617
  }
1244
1618
  }
1245
1619
  const responses = operationObject['responses'];
@@ -1250,7 +1624,7 @@ function hoistOperationSchemas(operationObject, op, registry) {
1250
1624
  }
1251
1625
  const media = entry['content']['application/json'];
1252
1626
  if (isPlainObject(media) && isHoistableSchema(media['schema'])) {
1253
- media['schema'] = registry.ref(media['schema'], componentSchemaName(op, 'Response'));
1627
+ media['schema'] = registry.ref(media['schema'], componentSchemaName(op, OpenApiComponentSchemaRole.RESPONSE), describeComponentSchema(op, OpenApiComponentSchemaRole.RESPONSE, resourcePurpose));
1254
1628
  }
1255
1629
  }
1256
1630
  }
@@ -1344,6 +1718,44 @@ function buildOpenApiDocument(section, operations, input) {
1344
1718
  const sectionLabel = section === OpenApiSection.API_REFERENCE
1345
1719
  ? 'API reference'
1346
1720
  : 'Application administration API reference';
1721
+ /*
1722
+ * Resource purposes, indexed before the operation loop so a hoisted schema can carry what its
1723
+ * resource IS. The tag descriptions below read the same map; building it once keeps the schema
1724
+ * card and the tag card quoting one string rather than two copies that can drift.
1725
+ */
1726
+ const purposeByResourceIdentifier = new Map();
1727
+ for (const tag of input.resourceTags ?? []) {
1728
+ if (tag.description !== undefined && !purposeByResourceIdentifier.has(tag.name)) {
1729
+ purposeByResourceIdentifier.set(tag.name, tag.description);
1730
+ }
1731
+ }
1732
+ /*
1733
+ * The canonical route of every operation that has one, indexed before any operation object is
1734
+ * built. A route with no `Via` qualifier is the operation's own address; the `Via` ones are the
1735
+ * same operation reached through a parent.
1736
+ *
1737
+ * Indexed rather than inferred per operation, because "shortest path wins" would be a guess and
1738
+ * this is a fact the operation key already carries. An operation reachable ONLY through a parent
1739
+ * has no canonical entry, and is deliberately left unmarked: calling it an alias of nothing would
1740
+ * tell a reader to go somewhere that does not exist.
1741
+ */
1742
+ const canonicalPathByRoute = new Map();
1743
+ for (const op of sortedOps) {
1744
+ if (splitOperationQualifiers(op).via.length > 0)
1745
+ continue;
1746
+ const key = canonicalRouteKey(op);
1747
+ if (!canonicalPathByRoute.has(key))
1748
+ canonicalPathByRoute.set(key, op.path);
1749
+ }
1750
+ /** Exactly the shared error responses some operation referenced, so none is emitted unused. */
1751
+ const usedErrorResponseNames = new Set();
1752
+ /**
1753
+ * Each resource's canonical read-response component, for the `webhooks` block to point at.
1754
+ *
1755
+ * The read operation's response IS the record a delivery carries, so this is a resolved
1756
+ * reference rather than a guess — and a resource with no read operation simply gets prose.
1757
+ */
1758
+ const canonicalReadResponseRefByResource = new Map();
1347
1759
  const paths = {};
1348
1760
  const ownershipByPathAndVerb = new Map();
1349
1761
  const tagSet = new Set();
@@ -1360,7 +1772,15 @@ function buildOpenApiDocument(section, operations, input) {
1360
1772
  // The schema comes from the same model the backend error handler serializes.
1361
1773
  // Register before per-operation hoisting so every non-success response shares
1362
1774
  // the stable, named component instead of producing resource-local copies.
1363
- const errorResponseSchema = schemaRegistry.ref(input.errorResponseSchema, 'ErrorResponse');
1775
+ const errorResponseSchema = schemaRegistry.ref(describeFrameworkErrorEnvelopeEnums(input.errorResponseSchema), 'ErrorResponse',
1776
+ // The one schema the GENERATOR owns rather than projects, so it was the one schema with no
1777
+ // description while all 473 others had one: the sweep that described component schemas words
1778
+ // them from the operation and the resource, and this belongs to neither.
1779
+ 'The body returned whenever a request does not succeed. `error.type` says which kind of failure it '
1780
+ + 'was and `error.code`, when present, is the stable identifier to branch on in code — branch on '
1781
+ + 'those rather than on wording. `error.customMessage` is the text meant for a person, and '
1782
+ + '`error.customMessageReference` is its translation key; `error.message` is the technical '
1783
+ + 'explanation. `error.context` is present only on validation, conflict and rate-limit failures.');
1364
1784
  for (const op of sortedOps) {
1365
1785
  if (paths[op.path] === undefined) {
1366
1786
  paths[op.path] = {};
@@ -1377,7 +1797,40 @@ function buildOpenApiDocument(section, operations, input) {
1377
1797
  operationIdUseCount.set(baseOperationId, priorUses + 1);
1378
1798
  const operationId = priorUses === 0 ? baseOperationId : `${baseOperationId}${priorUses + 1}`;
1379
1799
  const operationObject = buildOperationObject(op, operationId, errorResponseSchema);
1380
- hoistOperationSchemas(operationObject, op, schemaRegistry);
1800
+ for (const response of Object.values((operationObject['responses'] ?? {}))) {
1801
+ const ref = isPlainObject(response) ? response['$ref'] : undefined;
1802
+ if (typeof ref === 'string' && ref.startsWith('#/components/responses/')) {
1803
+ usedErrorResponseNames.add(ref.slice('#/components/responses/'.length));
1804
+ }
1805
+ }
1806
+ /*
1807
+ * Mark an alias route AS one, and point at the operation's own address.
1808
+ *
1809
+ * OpenAPI has no concept of "the same operation at another path", so a reader meeting
1810
+ * `organizationMembers` for the first time sees its whole surface three times with no way to
1811
+ * tell that two of them are conveniences. The description says so in words a reader acts on,
1812
+ * and `x-wildo.aliasOfPath` gives a renderer something to group or fold on.
1813
+ *
1814
+ * NOT `deprecated`: an alias is a supported route and a client calling it is doing nothing
1815
+ * wrong. Saying "prefer" would be an instruction nobody asked this generator to give.
1816
+ */
1817
+ const canonicalPath = splitOperationQualifiers(op).via.length === 0
1818
+ ? undefined
1819
+ : canonicalPathByRoute.get(canonicalRouteKey(op));
1820
+ if (canonicalPath !== undefined && canonicalPath !== op.path) {
1821
+ const wildo = operationObject['x-wildo'];
1822
+ if (isPlainObject(wildo))
1823
+ wildo['aliasOfPath'] = canonicalPath;
1824
+ const existing = typeof operationObject['description'] === 'string' ? operationObject['description'] : '';
1825
+ const note = `The same operation is published at \`${op.httpVerb.toUpperCase()} ${canonicalPath}\`. This route reaches it through its parent, which is a convenience rather than a different operation — either address does the same thing.`;
1826
+ operationObject['description'] = existing.length > 0 ? `${existing}\n\n${note}` : note;
1827
+ }
1828
+ hoistOperationSchemas(operationObject, op, schemaRegistry, purposeByResourceIdentifier.get(op.resourceIdentifier));
1829
+ if (op.baseOperationIdentifier === 'read' && splitOperationQualifiers(op).via.length === 0 && !canonicalReadResponseRefByResource.has(op.resourceIdentifier)) {
1830
+ const okSchema = (operationObject['responses']?.['200']?.content?.['application/json']?.schema);
1831
+ if (typeof okSchema?.$ref === 'string')
1832
+ canonicalReadResponseRefByResource.set(op.resourceIdentifier, okSchema.$ref);
1833
+ }
1381
1834
  paths[op.path][op.httpVerb] = operationObject;
1382
1835
  ownershipByPathAndVerb.set(collisionKey, `${op.resourceIdentifier}.${op.operationKey}`);
1383
1836
  tagSet.add(op.resourceIdentifier);
@@ -1396,6 +1849,7 @@ function buildOpenApiDocument(section, operations, input) {
1396
1849
  }
1397
1850
  resourceTagsByName.set(tag.name, tag);
1398
1851
  }
1852
+ const webhooks = buildWebhooksObject(sortedOps, canonicalReadResponseRefByResource);
1399
1853
  return {
1400
1854
  openapi: '3.1.0',
1401
1855
  info: {
@@ -1429,6 +1883,9 @@ function buildOpenApiDocument(section, operations, input) {
1429
1883
  return description !== undefined && description.length > 0 ? { ...tag, description } : tag;
1430
1884
  }),
1431
1885
  paths,
1886
+ // `webhooks` (OpenAPI 3.1) = what this application SENDS. The reference described only what a
1887
+ // client calls; an integrator writing the receiving endpoint had no machine-readable contract.
1888
+ ...(Object.keys(webhooks).length > 0 ? { webhooks } : {}),
1432
1889
  // `components.schemas` = hoisted, content-deduped request/response body
1433
1890
  // models (named `<Resource><Verb><Quals>Request|Response`); `securitySchemes`
1434
1891
  // = framework-universal auth, referenced by each op's `security`.
@@ -1436,6 +1893,12 @@ function buildOpenApiDocument(section, operations, input) {
1436
1893
  ...(Object.keys(schemaRegistry.schemas()).length > 0
1437
1894
  ? { schemas: schemaRegistry.schemas() }
1438
1895
  : {}),
1896
+ // `responses` = the error responses the GENERATOR words (the access-mode 401s and the
1897
+ // universal 500). An authored response stays inline on its operation, because its
1898
+ // description is written for that one operation and is the document's most useful prose.
1899
+ ...(usedErrorResponseNames.size > 0
1900
+ ? { responses: buildErrorResponseComponents(usedErrorResponseNames, errorResponseSchema) }
1901
+ : {}),
1439
1902
  securitySchemes: SECURITY_SCHEMES,
1440
1903
  },
1441
1904
  };