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

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 (130) hide show
  1. package/dist/esm/build/csp-emit.d.ts.map +1 -1
  2. package/dist/esm/build/load-materialized-frontend-providers.d.ts.map +1 -1
  3. package/dist/esm/companion/application-documentation/application-administration-documentation.d.ts.map +1 -1
  4. package/dist/esm/companion/application-documentation/application-administration-documentation.js.map +1 -1
  5. package/dist/esm/companion/application-documentation/application-authentication-documentation.d.ts.map +1 -1
  6. package/dist/esm/companion/application-documentation/application-connection-documentation.d.ts +25 -1
  7. package/dist/esm/companion/application-documentation/application-connection-documentation.d.ts.map +1 -1
  8. package/dist/esm/companion/application-documentation/application-connection-documentation.js +28 -1
  9. package/dist/esm/companion/application-documentation/application-connection-documentation.js.map +1 -1
  10. package/dist/esm/companion/application-documentation/application-domain-documentation.d.ts +133 -0
  11. package/dist/esm/companion/application-documentation/application-domain-documentation.d.ts.map +1 -0
  12. package/dist/esm/companion/application-documentation/application-domain-documentation.js +243 -0
  13. package/dist/esm/companion/application-documentation/application-domain-documentation.js.map +1 -0
  14. package/dist/esm/companion/application-documentation/application-integration-documentation.d.ts.map +1 -1
  15. package/dist/esm/companion/application-documentation/application-integration-documentation.js +23 -0
  16. package/dist/esm/companion/application-documentation/application-integration-documentation.js.map +1 -1
  17. package/dist/esm/companion/application-documentation/application-organization-role-documentation.d.ts +30 -0
  18. package/dist/esm/companion/application-documentation/application-organization-role-documentation.d.ts.map +1 -1
  19. package/dist/esm/companion/application-documentation/application-organization-role-documentation.js +38 -0
  20. package/dist/esm/companion/application-documentation/application-organization-role-documentation.js.map +1 -1
  21. package/dist/esm/companion/application-documentation/application-organization-unit-resource-documentation.d.ts.map +1 -1
  22. package/dist/esm/companion/application-documentation/technical-documentation-engine-content-bundle.d.ts +61 -1
  23. package/dist/esm/companion/application-documentation/technical-documentation-engine-content-bundle.d.ts.map +1 -1
  24. package/dist/esm/companion/application-documentation/technical-documentation-engine-content-bundle.js +255 -218
  25. package/dist/esm/companion/application-documentation/technical-documentation-engine-content-bundle.js.map +1 -1
  26. package/dist/esm/companion/application-documentation/technical-documentation-operational-evidence.d.ts.map +1 -1
  27. package/dist/esm/companion/application-documentation/technical-documentation-private-derivation.d.ts +2 -0
  28. package/dist/esm/companion/application-documentation/technical-documentation-private-derivation.d.ts.map +1 -1
  29. package/dist/esm/companion/application-documentation/technical-documentation-private-derivation.js +1 -0
  30. package/dist/esm/companion/application-documentation/technical-documentation-private-derivation.js.map +1 -1
  31. package/dist/esm/companion/application-documentation/technical-documentation-publication-policy.d.ts.map +1 -1
  32. package/dist/esm/companion/application-documentation/technical-documentation-publication-policy.js +1 -1
  33. package/dist/esm/companion/application-documentation/technical-documentation-publication-policy.js.map +1 -1
  34. package/dist/esm/companion/content/application-consumer-documentation-content-loader.d.ts.map +1 -1
  35. package/dist/esm/companion/index.d.ts +2 -1
  36. package/dist/esm/companion/index.d.ts.map +1 -1
  37. package/dist/esm/companion/index.js +2 -1
  38. package/dist/esm/companion/index.js.map +1 -1
  39. package/dist/esm/companion/manual-controller-route-projection.d.ts +112 -0
  40. package/dist/esm/companion/manual-controller-route-projection.d.ts.map +1 -0
  41. package/dist/esm/companion/manual-controller-route-projection.js +249 -0
  42. package/dist/esm/companion/manual-controller-route-projection.js.map +1 -0
  43. package/dist/esm/companion/openapi-generator.d.ts +16 -0
  44. package/dist/esm/companion/openapi-generator.d.ts.map +1 -1
  45. package/dist/esm/companion/openapi-generator.js +493 -26
  46. package/dist/esm/companion/openapi-generator.js.map +1 -1
  47. package/dist/esm/companion/operation-projection.schemas.d.ts +44 -0
  48. package/dist/esm/companion/operation-projection.schemas.d.ts.map +1 -1
  49. package/dist/esm/companion/operation-projection.schemas.js +37 -0
  50. package/dist/esm/companion/operation-projection.schemas.js.map +1 -1
  51. package/dist/esm/companion/publish-result.types.d.ts +37 -4
  52. package/dist/esm/companion/publish-result.types.d.ts.map +1 -1
  53. package/dist/esm/companion/publish-result.types.js.map +1 -1
  54. package/dist/esm/companion/rendering/technical-documentation-authorized-bundle-reader.d.ts.map +1 -1
  55. package/dist/esm/companion/rendering/technical-documentation-docusaurus-renderer.d.ts +7 -1
  56. package/dist/esm/companion/rendering/technical-documentation-docusaurus-renderer.d.ts.map +1 -1
  57. package/dist/esm/companion/rendering/technical-documentation-docusaurus-renderer.js +34 -18
  58. package/dist/esm/companion/rendering/technical-documentation-docusaurus-renderer.js.map +1 -1
  59. package/dist/esm/companion/rendering/technical-documentation-managed-tree-validator.d.ts.map +1 -1
  60. package/dist/esm/companion/rendering/technical-documentation-managed-tree-validator.js +8 -1
  61. package/dist/esm/companion/rendering/technical-documentation-managed-tree-validator.js.map +1 -1
  62. package/dist/esm/companion/rendering/technical-documentation-openapi-renderer.d.ts.map +1 -1
  63. package/dist/esm/companion/rendering/technical-documentation-render-model.d.ts.map +1 -1
  64. package/dist/esm/companion/rendering/technical-documentation-render-model.js +30 -13
  65. package/dist/esm/companion/rendering/technical-documentation-render-model.js.map +1 -1
  66. package/dist/esm/companion/rendering/technical-documentation-search-index-renderer.d.ts +10 -0
  67. package/dist/esm/companion/rendering/technical-documentation-search-index-renderer.d.ts.map +1 -1
  68. package/dist/esm/companion/rendering/technical-documentation-search-index-renderer.js +10 -15
  69. package/dist/esm/companion/rendering/technical-documentation-search-index-renderer.js.map +1 -1
  70. package/dist/esm/companion/technical-documentation-asset-path.d.ts.map +1 -1
  71. package/dist/esm/companion/technical-documentation-capture-execution-port.d.ts +27 -1
  72. package/dist/esm/companion/technical-documentation-capture-execution-port.d.ts.map +1 -1
  73. package/dist/esm/companion/technical-documentation-capture-execution-port.js.map +1 -1
  74. package/dist/esm/companion/technical-documentation-diagram-definitions.d.ts +33 -0
  75. package/dist/esm/companion/technical-documentation-diagram-definitions.d.ts.map +1 -0
  76. package/dist/esm/companion/technical-documentation-diagram-definitions.js +54 -0
  77. package/dist/esm/companion/technical-documentation-diagram-definitions.js.map +1 -0
  78. package/dist/esm/companion/technical-documentation-diagram-materializer.d.ts +10 -18
  79. package/dist/esm/companion/technical-documentation-diagram-materializer.d.ts.map +1 -1
  80. package/dist/esm/companion/technical-documentation-diagram-materializer.js +9 -39
  81. package/dist/esm/companion/technical-documentation-diagram-materializer.js.map +1 -1
  82. package/dist/esm/companion/technical-documentation-placeholder-materializer.d.ts.map +1 -1
  83. package/dist/esm/companion/zod-to-openapi.d.ts.map +1 -1
  84. package/dist/esm/companion-exports.d.ts.map +1 -1
  85. package/dist/esm/config/define-tech-doc-config.d.ts.map +1 -1
  86. package/dist/esm/config/index.d.ts.map +1 -1
  87. package/dist/esm/config/wildo-tech-doc-config.schemas.d.ts +8 -4
  88. package/dist/esm/config/wildo-tech-doc-config.schemas.d.ts.map +1 -1
  89. package/dist/esm/config/wildo-tech-doc-config.schemas.js +8 -4
  90. package/dist/esm/config/wildo-tech-doc-config.schemas.js.map +1 -1
  91. package/dist/esm/content/application-consumer-documentation-content-manifest.schemas.d.ts.map +1 -1
  92. package/dist/esm/content/application-consumer-documentation-content.techdoc.d.ts +42 -64
  93. package/dist/esm/content/application-consumer-documentation-content.techdoc.d.ts.map +1 -1
  94. package/dist/esm/content/application-consumer-documentation-content.techdoc.js +164 -925
  95. package/dist/esm/content/application-consumer-documentation-content.techdoc.js.map +1 -1
  96. package/dist/esm/content.exports.d.ts.map +1 -1
  97. package/dist/esm/index.d.ts.map +1 -1
  98. package/dist/esm/openapi/api-reference-link-index.d.ts.map +1 -1
  99. package/dist/esm/openapi/index.d.ts.map +1 -1
  100. package/dist/esm/openapi/openapi-generation-output.schemas.d.ts.map +1 -1
  101. package/dist/esm/openapi-reference-model.exports.d.ts.map +1 -1
  102. package/dist/esm/runtime/AuthExchangePage.d.ts.map +1 -1
  103. package/dist/esm/runtime/DocsAuthContext.d.ts +16 -1
  104. package/dist/esm/runtime/DocsAuthContext.d.ts.map +1 -1
  105. package/dist/esm/runtime/DocsAuthContext.js +18 -2
  106. package/dist/esm/runtime/DocsAuthContext.js.map +1 -1
  107. package/dist/esm/runtime/decode-jwt-claims.d.ts.map +1 -1
  108. package/dist/esm/runtime/docs-auth-client.d.ts.map +1 -1
  109. package/dist/esm/runtime/docs-auth-session.schemas.d.ts.map +1 -1
  110. package/dist/esm/runtime/frontend-provider-registry.techdoc.d.ts +35 -13
  111. package/dist/esm/runtime/frontend-provider-registry.techdoc.d.ts.map +1 -1
  112. package/dist/esm/runtime/frontend-provider-registry.techdoc.js +28 -19
  113. package/dist/esm/runtime/frontend-provider-registry.techdoc.js.map +1 -1
  114. package/dist/esm/runtime/index.d.ts +1 -0
  115. package/dist/esm/runtime/index.d.ts.map +1 -1
  116. package/dist/esm/runtime/index.js +1 -0
  117. package/dist/esm/runtime/index.js.map +1 -1
  118. package/dist/esm/runtime/openapi-reference-conservation.d.ts.map +1 -1
  119. package/dist/esm/runtime/use-docs-auth-session.d.ts.map +1 -1
  120. package/dist/esm/runtime/use-docs-frontend-provider-registry.d.ts.map +1 -1
  121. package/dist/esm/runtime/use-docs-provider-sdks.d.ts +21 -0
  122. package/dist/esm/runtime/use-docs-provider-sdks.d.ts.map +1 -0
  123. package/dist/esm/runtime/use-docs-provider-sdks.js +49 -0
  124. package/dist/esm/runtime/use-docs-provider-sdks.js.map +1 -0
  125. package/dist/tsconfig.build.tsbuildinfo +1 -1
  126. package/package.json +6 -5
  127. package/dist/esm/companion/rendering/technical-documentation-markdown-renderer.d.ts +0 -9
  128. package/dist/esm/companion/rendering/technical-documentation-markdown-renderer.d.ts.map +0 -1
  129. package/dist/esm/companion/rendering/technical-documentation-markdown-renderer.js +0 -111
  130. package/dist/esm/companion/rendering/technical-documentation-markdown-renderer.js.map +0 -1
@@ -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,38 @@ 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
+ // retention-authority: N/A — an API consumer's ENGLISH description of what the two values mean,
990
+ // rendered into the OpenAPI document. There is no emitter to consume here: the audience is a
991
+ // person reading a schema, and the sentence has to say `active` and `retained` in words for the
992
+ // document to be worth anything. It decides nothing and is read by no code.
993
+ 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.',
994
+ impersonalizedAt: 'When personal detail was stripped from this record, if it ever was. Absent on a record that has not been through erasure.',
995
+ data: 'The page of records this response carries.',
996
+ 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.',
997
+ });
893
998
  /**
894
999
  * The structural signature of a MATERIALIZED resource document in a response
895
1000
  * schema is the `_id` property. The serializer attaches `_version` /
@@ -955,11 +1060,47 @@ function injectEngineMetadataIntoEntitySchemas(schema) {
955
1060
  * (`stableStringify`) still collapses them onto one component. Non-object
956
1061
  * schemas (`null` / scalar) pass through unchanged.
957
1062
  */
1063
+ /**
1064
+ * Fills a description on the properties every resource carries and none authors.
1065
+ *
1066
+ * Recursive, because these appear at every depth: `data` and `pagination` wrap the envelope,
1067
+ * `_id` and `createdAt` sit on the document inside it, and a `context`-mode response nests another
1068
+ * collection again. Never overrides an authored description — a specification that describes its
1069
+ * own field wins, which is what keeps this safe for a name an application might reuse.
1070
+ */
1071
+ function fillFrameworkOwnedPropertyDescriptions(node) {
1072
+ if (Array.isArray(node)) {
1073
+ for (const entry of node)
1074
+ fillFrameworkOwnedPropertyDescriptions(entry);
1075
+ return;
1076
+ }
1077
+ if (!isPlainObject(node))
1078
+ return;
1079
+ const properties = node['properties'];
1080
+ if (isPlainObject(properties)) {
1081
+ for (const [name, property] of Object.entries(properties)) {
1082
+ if (!isPlainObject(property))
1083
+ continue;
1084
+ const framework = FRAMEWORK_OWNED_PROPERTY_DESCRIPTIONS[name];
1085
+ if (framework !== undefined && property['description'] === undefined) {
1086
+ property['description'] = framework;
1087
+ }
1088
+ fillFrameworkOwnedPropertyDescriptions(property);
1089
+ }
1090
+ }
1091
+ for (const key of ['items', 'allOf', 'oneOf', 'anyOf', 'additionalProperties']) {
1092
+ if (key in node)
1093
+ fillFrameworkOwnedPropertyDescriptions(node[key]);
1094
+ }
1095
+ }
958
1096
  function augmentResponseSchemaWithEngineMetadata(responseBodySchema) {
959
1097
  if (!isPlainObject(responseBodySchema)) {
960
1098
  return responseBodySchema;
961
1099
  }
962
1100
  const clone = structuredClone(responseBodySchema);
1101
+ // Safe to mutate: `clone` is this function's own deep copy, and the caller's schema is the
1102
+ // operation projection's — a generator that edited it would be editing the data it was handed.
1103
+ fillFrameworkOwnedPropertyDescriptions(clone);
963
1104
  const properties = clone['properties'];
964
1105
  const dataSchema = isPlainObject(properties) ? properties['data'] : undefined;
965
1106
  if (isPlainObject(dataSchema) && (dataSchema['type'] === 'array' || 'items' in dataSchema)) {
@@ -1093,27 +1234,151 @@ function buildResponsesObject(op, responseBodySchema, errorResponseSchema) {
1093
1234
  // 401 contract. A resource-owned 401 is more specific (for example a
1094
1235
  // step-up flow) and deliberately takes precedence.
1095
1236
  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
- };
1237
+ // Referenced, not inlined: the wording is the generator's and identical for every operation
1238
+ // with the same access mode, so `unauthorizedDescription` states it once and the component
1239
+ // carries it. Repeating the content block on 546 operations said nothing a reader could use.
1240
+ responses['401'] = { $ref: `#/components/responses/${unauthorizedResponseName(op.access.authenticationMode)}` };
1241
+ }
1242
+ /*
1243
+ * Every operation can fail inside the application, and not one said so. Added last and only when
1244
+ * absent, so an authored 500 — which can say something specific about THIS operation — wins.
1245
+ */
1246
+ if (responses['500'] === undefined) {
1247
+ responses['500'] = { $ref: `#/components/responses/${INTERNAL_ERROR_RESPONSE_NAME}` };
1114
1248
  }
1115
1249
  return responses;
1116
1250
  }
1251
+ /**
1252
+ * The error responses the GENERATOR owns, named once in `components.responses`.
1253
+ *
1254
+ * Only the ones whose wording is the generator's own are shared. An authored response carries a
1255
+ * description written for that one operation — "No release record exists for the supplied id in the
1256
+ * caller's scope" — and that sentence is the most useful prose in the document; folding it into a
1257
+ * shared component would replace it with something generic, so authored responses stay inline.
1258
+ *
1259
+ * The 401 wording is derived from the operation's ACCESS MODE, which is why there are three: the
1260
+ * credential a caller is missing differs, and saying "no valid bearer token" to an anonymous-session
1261
+ * endpoint would be wrong.
1262
+ */
1263
+ const GENERATOR_OWNED_ERROR_RESPONSE_NAMES = Object.freeze({
1264
+ [OperationProjectionAuthenticationMode.ANONYMOUS_SESSION]: 'UnauthorizedAnonymousSession',
1265
+ [OperationProjectionAuthenticationMode.ANONYMOUS_OR_AUTHENTICATED]: 'UnauthorizedAnonymousOrAuthenticated',
1266
+ [OperationProjectionAuthenticationMode.AUTHENTICATED]: 'Unauthorized',
1267
+ [OperationProjectionAuthenticationMode.PUBLIC]: 'Unauthorized',
1268
+ });
1269
+ const INTERNAL_ERROR_RESPONSE_NAME = 'InternalServerError';
1270
+ function unauthorizedResponseName(mode) {
1271
+ return GENERATOR_OWNED_ERROR_RESPONSE_NAMES[mode];
1272
+ }
1273
+ /** The one place each 401 wording is written; the components below are built from it. */
1274
+ function unauthorizedDescription(mode) {
1275
+ switch (mode) {
1276
+ case OperationProjectionAuthenticationMode.ANONYMOUS_SESSION:
1277
+ return 'The anonymous-session token is missing, malformed, expired, or invalid.';
1278
+ case OperationProjectionAuthenticationMode.ANONYMOUS_OR_AUTHENTICATED:
1279
+ return 'No valid anonymous-session token, bearer token, or API key was supplied.';
1280
+ case OperationProjectionAuthenticationMode.AUTHENTICATED:
1281
+ case OperationProjectionAuthenticationMode.PUBLIC:
1282
+ return 'No valid bearer token or API key was supplied.';
1283
+ default: {
1284
+ const exhaustiveAuthenticationMode = mode;
1285
+ throw new Error(`OpenAPI generator: unsupported authentication mode ${String(exhaustiveAuthenticationMode)}`);
1286
+ }
1287
+ }
1288
+ }
1289
+ /**
1290
+ * The `components.responses` block, built from the same wording the operations reference.
1291
+ *
1292
+ * Emitted only for the names an operation actually used, so a document never carries a component
1293
+ * nothing points at — the reason `PUBLIC` shares the authenticated name rather than earning an
1294
+ * entry of its own: a public operation never gets a synthesised 401.
1295
+ */
1296
+ function buildErrorResponseComponents(usedNames, errorResponseSchema) {
1297
+ const content = { 'application/json': { schema: errorResponseSchema } };
1298
+ const all = { [INTERNAL_ERROR_RESPONSE_NAME]: { description: internalServerErrorDescription(), content } };
1299
+ for (const mode of [
1300
+ OperationProjectionAuthenticationMode.ANONYMOUS_SESSION,
1301
+ OperationProjectionAuthenticationMode.ANONYMOUS_OR_AUTHENTICATED,
1302
+ OperationProjectionAuthenticationMode.AUTHENTICATED,
1303
+ ]) {
1304
+ all[unauthorizedResponseName(mode)] = { description: unauthorizedDescription(mode), content };
1305
+ }
1306
+ return Object.fromEntries(Object.entries(all).filter(([name]) => usedNames.has(name)));
1307
+ }
1308
+ /**
1309
+ * Every operation can fail with the framework's error envelope, and until now not one of the 546
1310
+ * published operations said so — measured on Wonder CRM, `500` appeared zero times.
1311
+ *
1312
+ * Synthesised rather than authored, for the same reason the 401 is: it is true of every operation
1313
+ * by construction, so making each specification restate it would be 546 copies of one fact with 546
1314
+ * chances to drift. An authored 500 takes precedence, exactly as an authored 401 does.
1315
+ */
1316
+ function internalServerErrorDescription() {
1317
+ 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.';
1318
+ }
1319
+ /**
1320
+ * The document's `webhooks` block — what a RECEIVER is sent, described where OpenAPI 3.1 puts it.
1321
+ *
1322
+ * Until now the reference described only what a client CALLS. An integrator writing the other half
1323
+ * — the endpoint this application posts to — had no machine-readable contract at all, and every
1324
+ * operation that fires a delivery said nothing about it.
1325
+ *
1326
+ * Two facts make these entries honest rather than plausible:
1327
+ *
1328
+ * · THE BODY IS THE RESOURCE DOCUMENT ITSELF, not an envelope around it. The delivery worker
1329
+ * posts the stored payload verbatim, and a backend test exists specifically to fail if anyone
1330
+ * wraps it — because the body's bytes must hash to the signature's `bodyHash` claim. So the
1331
+ * schema referenced here is the resource's own read response, and when that component cannot
1332
+ * be resolved the entry carries prose instead of a shape it cannot prove.
1333
+ * · THE DELIVERY CONTRACT IS ONE OBJECT. Retries, timeout, signature algorithm and claim names
1334
+ * come from `M2M_WEBHOOK_DELIVERY_CONTRACT`, the same source the delivery worker derives its
1335
+ * tunables from, so the published contract cannot drift from what is actually sent.
1336
+ */
1337
+ function buildWebhooksObject(operations, canonicalReadResponseRefByResource) {
1338
+ const webhooks = {};
1339
+ for (const op of operations) {
1340
+ for (const notification of op.m2mNotifications ?? []) {
1341
+ const key = `${op.resourceIdentifier}.${op.baseOperationIdentifier}`;
1342
+ if (webhooks[key] !== undefined)
1343
+ continue;
1344
+ const bodyRef = canonicalReadResponseRefByResource.get(op.resourceIdentifier);
1345
+ const contract = M2M_WEBHOOK_DELIVERY_CONTRACT;
1346
+ webhooks[key] = {
1347
+ post: {
1348
+ summary: `${op.resourceIdentifier} ${op.baseOperationIdentifier}`,
1349
+ description: [
1350
+ `Sent to every endpoint configured and enabled on the \`${notification.channel}\` channel when \`${op.resourceIdentifier}.${op.baseOperationIdentifier}\` succeeds.`,
1351
+ '',
1352
+ `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.`,
1353
+ '',
1354
+ `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.`,
1355
+ '',
1356
+ `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.`,
1357
+ ].join('\n'),
1358
+ 'x-wildo': {
1359
+ resourceIdentifier: op.resourceIdentifier,
1360
+ operationIdentifier: op.baseOperationIdentifier,
1361
+ eventChannel: notification.channel,
1362
+ notificationIdentifier: notification.identifier,
1363
+ },
1364
+ requestBody: {
1365
+ required: true,
1366
+ content: {
1367
+ 'application/json': bodyRef === undefined
1368
+ // No read response to point at — say what the body is rather than assert a shape.
1369
+ ? { schema: { type: 'object', description: `The \`${op.resourceIdentifier}\` record as the API returns it.` } }
1370
+ : { schema: { $ref: bodyRef } },
1371
+ },
1372
+ },
1373
+ responses: {
1374
+ '2XX': { description: 'The delivery was accepted. Any 2xx ends the delivery; the body is ignored.' },
1375
+ },
1376
+ },
1377
+ };
1378
+ }
1379
+ }
1380
+ return webhooks;
1381
+ }
1117
1382
  /**
1118
1383
  * Builds the OpenAPI request-body `examples` map from authored examples that
1119
1384
  * carry a `request` payload. Returns undefined when there are none.
@@ -1175,6 +1440,23 @@ function stableStringify(value) {
1175
1440
  }
1176
1441
  return JSON.stringify(value) ?? 'null';
1177
1442
  }
1443
+ /**
1444
+ * Which half of an operation a generated component schema models.
1445
+ *
1446
+ * A closed two-member vocabulary, named rather than inlined because the two members are not
1447
+ * symmetric and the difference is worth stating once: a REQUEST schema is what a caller must
1448
+ * SEND and is therefore the contract they are held to, while a RESPONSE schema is what the
1449
+ * application PROMISES to return. Both the schema NAME and its description are derived from
1450
+ * this value, so the member is the single input that decides how an integrator reads the card.
1451
+ *
1452
+ * The member VALUES are the PascalCase suffixes appended to a schema name (`TodosCreateRequest`),
1453
+ * so they are wire-visible in the published OpenAPI document and not free to rename.
1454
+ */
1455
+ export var OpenApiComponentSchemaRole;
1456
+ (function (OpenApiComponentSchemaRole) {
1457
+ OpenApiComponentSchemaRole["REQUEST"] = "Request";
1458
+ OpenApiComponentSchemaRole["RESPONSE"] = "Response";
1459
+ })(OpenApiComponentSchemaRole || (OpenApiComponentSchemaRole = {}));
1178
1460
  /**
1179
1461
  * Component name for an operation's request/response body. Mirrors the
1180
1462
  * verb-first operationId namer (shares `pascal`): `<Resource><BaseVerb><Quals><Role>`,
@@ -1195,15 +1477,84 @@ function componentSchemaName(op, role) {
1195
1477
  .join('');
1196
1478
  return `${pascal(op.resourceIdentifier)}${pascal(op.baseOperationIdentifier)}${schemaQualifiers}${role}`;
1197
1479
  }
1480
+ /**
1481
+ * Describe the two closed vocabularies inside the framework's own error envelope, from the
1482
+ * definitions the runtime already carries.
1483
+ *
1484
+ * `error.type` is the field an integrator branches on, and it arrived as twenty bare tokens with no
1485
+ * explanation — the ONLY enum in the document that no authoring could reach, because the sweep that
1486
+ * describes enum values reads a field specification and this schema belongs to no resource. The
1487
+ * words are not written here: `ERROR_DEFINITIONS` is a `Record<ErrorType, …>` the error builder
1488
+ * itself consults, so the sentence, the HTTP status and whether the framework's own strategy is to
1489
+ * retry all come from the same source that decides them at runtime.
1490
+ *
1491
+ * Matching is by EXACT VALUE SET, and that is only sound because it is scoped to this one
1492
+ * generator-owned schema. Across the document at large it would not be: `['active', 'expired',
1493
+ * 'inactive']` occurs on an OAuth client and on an API key with different meanings, so propagating
1494
+ * one description to the other by value set would put "Client may mint tokens" on an API key.
1495
+ *
1496
+ * `severity` gets one sentence rather than six invented ones: it drives the framework's own logging
1497
+ * and alerting, and a client has nothing to do with it.
1498
+ *
1499
+ * Returns a described COPY. The caller's schema belongs to the projection input and a pin asserts
1500
+ * the generator leaves its input untouched.
1501
+ */
1502
+ function describeFrameworkErrorEnvelopeEnums(schema) {
1503
+ if (!isPlainObject(schema))
1504
+ return schema;
1505
+ const errorTypeValues = new Set(Object.values(ErrorType));
1506
+ const severityValues = new Set(Object.values(ErrorSeverity));
1507
+ const errorTypeMeanings = {};
1508
+ for (const value of Object.values(ErrorType)) {
1509
+ const definition = ERROR_DEFINITIONS[value];
1510
+ const retries = definition.errorHandlingStrategy === ErrorHandling_Strategy.RETRY;
1511
+ errorTypeMeanings[value] = `${definition.description} (HTTP ${String(definition.statusCode)})`
1512
+ + `${retries ? ' — retrying is expected to help.' : '.'}`;
1513
+ }
1514
+ const described = structuredClone(schema);
1515
+ const visit = (node) => {
1516
+ if (Array.isArray(node)) {
1517
+ for (const entry of node)
1518
+ visit(entry);
1519
+ return;
1520
+ }
1521
+ if (!isPlainObject(node))
1522
+ return;
1523
+ const values = node['enum'];
1524
+ if (Array.isArray(values) && node['description'] === undefined) {
1525
+ const present = new Set(values.map((value) => String(value)));
1526
+ if (present.size === errorTypeValues.size && [...present].every((value) => errorTypeValues.has(value))) {
1527
+ node['description'] = `Which kind of failure this was. Branch on this rather than on wording.\n\nValues:\n`
1528
+ + Object.entries(errorTypeMeanings).map(([value, meaning]) => `- \`${value}\`: ${meaning}`).join('\n');
1529
+ node['x-enum-descriptions'] = errorTypeMeanings;
1530
+ }
1531
+ else if (present.size === severityValues.size && [...present].every((value) => severityValues.has(value))) {
1532
+ node['description'] = 'How seriously the application itself treats this failure — it drives logging and alerting '
1533
+ + 'on the server. A client has nothing to decide from it; branch on `type` and `code`.';
1534
+ }
1535
+ }
1536
+ for (const entry of Object.values(node))
1537
+ visit(entry);
1538
+ };
1539
+ visit(described);
1540
+ return described;
1541
+ }
1198
1542
  function createSchemaRegistry() {
1199
1543
  const nameBySemanticIdentityAndContent = new Map();
1200
1544
  const contentByName = new Map();
1201
1545
  const schemas = {};
1202
1546
  return {
1203
- ref(schema, desiredName) {
1547
+ ref(schema, desiredName, description) {
1204
1548
  if (!isHoistableSchema(schema)) {
1205
1549
  return schema;
1206
1550
  }
1551
+ /*
1552
+ * Canonicalised BEFORE the description is attached, deliberately. Deduplication is by
1553
+ * name-and-content, so folding the description into the identity would split one component
1554
+ * into several whenever two operations produced the same shape — the opposite of what a
1555
+ * description is for. A component name maps to exactly one desired name, hence to one
1556
+ * operation's resource, so the description attached here describes the schema that is stored.
1557
+ */
1207
1558
  const canonical = stableStringify(schema);
1208
1559
  const semanticIdentityAndContent = `${desiredName}\u0000${canonical}`;
1209
1560
  const existingName = nameBySemanticIdentityAndContent.get(semanticIdentityAndContent);
@@ -1218,7 +1569,17 @@ function createSchemaRegistry() {
1218
1569
  }
1219
1570
  nameBySemanticIdentityAndContent.set(semanticIdentityAndContent, name);
1220
1571
  contentByName.set(name, canonical);
1221
- schemas[name] = schema;
1572
+ /*
1573
+ * COPIED, never mutated. The caller's schema object belongs to the operation projection, and
1574
+ * a pin asserts the generator leaves its input untouched — writing the description onto it
1575
+ * would have made this generator edit the data it was handed, which is the one thing that
1576
+ * test exists to prevent. A shallow copy suffices: only a top-level key is added, and an
1577
+ * authored description already on the schema outranks the derived one.
1578
+ */
1579
+ const stored = description !== undefined && isPlainObject(schema) && schema['description'] === undefined
1580
+ ? { ...schema, description }
1581
+ : schema;
1582
+ schemas[name] = stored;
1222
1583
  return { $ref: `#/components/schemas/${name}` };
1223
1584
  },
1224
1585
  schemas() {
@@ -1234,12 +1595,29 @@ function createSchemaRegistry() {
1234
1595
  * inline. All success statuses share one response schema object, so they
1235
1596
  * resolve to the same `$ref`. Mutates `operationObject` in place.
1236
1597
  */
1237
- function hoistOperationSchemas(operationObject, op, registry) {
1598
+ /**
1599
+ * What a component schema IS, taken from the operation that hoisted it.
1600
+ *
1601
+ * Measured on Wonder CRM: 0 of 474 component schemas carried a description, so the schema list read
1602
+ * as 474 names — `DealReadResponse`, `DealReadContextResponse`, `DealReadSummaryResponse` — with
1603
+ * nothing to tell them apart or say what a deal is.
1604
+ *
1605
+ * Derived from the OPERATION rather than from the schema's name. 473 of the 474 names do start with
1606
+ * their resource's tag, so a prefix match would have worked and would have been a guess: the
1607
+ * hoisting operation is the authoritative link, and it costs nothing to use it. The resource's own
1608
+ * purpose is appended when its specification authored one, because a reader who reached a schema
1609
+ * card may never have seen the tag.
1610
+ */
1611
+ function describeComponentSchema(op, role, resourcePurpose) {
1612
+ const what = `${role === OpenApiComponentSchemaRole.REQUEST ? 'Request body' : 'Response body'} for \`${op.resourceIdentifier}.${op.baseOperationIdentifier}\`.`;
1613
+ return resourcePurpose === undefined ? what : `${what} ${resourcePurpose}`;
1614
+ }
1615
+ function hoistOperationSchemas(operationObject, op, registry, resourcePurpose) {
1238
1616
  const requestBody = operationObject['requestBody'];
1239
1617
  if (isPlainObject(requestBody) && isPlainObject(requestBody['content'])) {
1240
1618
  const media = requestBody['content']['application/json'];
1241
1619
  if (isPlainObject(media) && isHoistableSchema(media['schema'])) {
1242
- media['schema'] = registry.ref(media['schema'], componentSchemaName(op, 'Request'));
1620
+ media['schema'] = registry.ref(media['schema'], componentSchemaName(op, OpenApiComponentSchemaRole.REQUEST), describeComponentSchema(op, OpenApiComponentSchemaRole.REQUEST, resourcePurpose));
1243
1621
  }
1244
1622
  }
1245
1623
  const responses = operationObject['responses'];
@@ -1250,7 +1628,7 @@ function hoistOperationSchemas(operationObject, op, registry) {
1250
1628
  }
1251
1629
  const media = entry['content']['application/json'];
1252
1630
  if (isPlainObject(media) && isHoistableSchema(media['schema'])) {
1253
- media['schema'] = registry.ref(media['schema'], componentSchemaName(op, 'Response'));
1631
+ media['schema'] = registry.ref(media['schema'], componentSchemaName(op, OpenApiComponentSchemaRole.RESPONSE), describeComponentSchema(op, OpenApiComponentSchemaRole.RESPONSE, resourcePurpose));
1254
1632
  }
1255
1633
  }
1256
1634
  }
@@ -1344,6 +1722,44 @@ function buildOpenApiDocument(section, operations, input) {
1344
1722
  const sectionLabel = section === OpenApiSection.API_REFERENCE
1345
1723
  ? 'API reference'
1346
1724
  : 'Application administration API reference';
1725
+ /*
1726
+ * Resource purposes, indexed before the operation loop so a hoisted schema can carry what its
1727
+ * resource IS. The tag descriptions below read the same map; building it once keeps the schema
1728
+ * card and the tag card quoting one string rather than two copies that can drift.
1729
+ */
1730
+ const purposeByResourceIdentifier = new Map();
1731
+ for (const tag of input.resourceTags ?? []) {
1732
+ if (tag.description !== undefined && !purposeByResourceIdentifier.has(tag.name)) {
1733
+ purposeByResourceIdentifier.set(tag.name, tag.description);
1734
+ }
1735
+ }
1736
+ /*
1737
+ * The canonical route of every operation that has one, indexed before any operation object is
1738
+ * built. A route with no `Via` qualifier is the operation's own address; the `Via` ones are the
1739
+ * same operation reached through a parent.
1740
+ *
1741
+ * Indexed rather than inferred per operation, because "shortest path wins" would be a guess and
1742
+ * this is a fact the operation key already carries. An operation reachable ONLY through a parent
1743
+ * has no canonical entry, and is deliberately left unmarked: calling it an alias of nothing would
1744
+ * tell a reader to go somewhere that does not exist.
1745
+ */
1746
+ const canonicalPathByRoute = new Map();
1747
+ for (const op of sortedOps) {
1748
+ if (splitOperationQualifiers(op).via.length > 0)
1749
+ continue;
1750
+ const key = canonicalRouteKey(op);
1751
+ if (!canonicalPathByRoute.has(key))
1752
+ canonicalPathByRoute.set(key, op.path);
1753
+ }
1754
+ /** Exactly the shared error responses some operation referenced, so none is emitted unused. */
1755
+ const usedErrorResponseNames = new Set();
1756
+ /**
1757
+ * Each resource's canonical read-response component, for the `webhooks` block to point at.
1758
+ *
1759
+ * The read operation's response IS the record a delivery carries, so this is a resolved
1760
+ * reference rather than a guess — and a resource with no read operation simply gets prose.
1761
+ */
1762
+ const canonicalReadResponseRefByResource = new Map();
1347
1763
  const paths = {};
1348
1764
  const ownershipByPathAndVerb = new Map();
1349
1765
  const tagSet = new Set();
@@ -1360,7 +1776,15 @@ function buildOpenApiDocument(section, operations, input) {
1360
1776
  // The schema comes from the same model the backend error handler serializes.
1361
1777
  // Register before per-operation hoisting so every non-success response shares
1362
1778
  // the stable, named component instead of producing resource-local copies.
1363
- const errorResponseSchema = schemaRegistry.ref(input.errorResponseSchema, 'ErrorResponse');
1779
+ const errorResponseSchema = schemaRegistry.ref(describeFrameworkErrorEnvelopeEnums(input.errorResponseSchema), 'ErrorResponse',
1780
+ // The one schema the GENERATOR owns rather than projects, so it was the one schema with no
1781
+ // description while all 473 others had one: the sweep that described component schemas words
1782
+ // them from the operation and the resource, and this belongs to neither.
1783
+ 'The body returned whenever a request does not succeed. `error.type` says which kind of failure it '
1784
+ + 'was and `error.code`, when present, is the stable identifier to branch on in code — branch on '
1785
+ + 'those rather than on wording. `error.customMessage` is the text meant for a person, and '
1786
+ + '`error.customMessageReference` is its translation key; `error.message` is the technical '
1787
+ + 'explanation. `error.context` is present only on validation, conflict and rate-limit failures.');
1364
1788
  for (const op of sortedOps) {
1365
1789
  if (paths[op.path] === undefined) {
1366
1790
  paths[op.path] = {};
@@ -1377,7 +1801,40 @@ function buildOpenApiDocument(section, operations, input) {
1377
1801
  operationIdUseCount.set(baseOperationId, priorUses + 1);
1378
1802
  const operationId = priorUses === 0 ? baseOperationId : `${baseOperationId}${priorUses + 1}`;
1379
1803
  const operationObject = buildOperationObject(op, operationId, errorResponseSchema);
1380
- hoistOperationSchemas(operationObject, op, schemaRegistry);
1804
+ for (const response of Object.values((operationObject['responses'] ?? {}))) {
1805
+ const ref = isPlainObject(response) ? response['$ref'] : undefined;
1806
+ if (typeof ref === 'string' && ref.startsWith('#/components/responses/')) {
1807
+ usedErrorResponseNames.add(ref.slice('#/components/responses/'.length));
1808
+ }
1809
+ }
1810
+ /*
1811
+ * Mark an alias route AS one, and point at the operation's own address.
1812
+ *
1813
+ * OpenAPI has no concept of "the same operation at another path", so a reader meeting
1814
+ * `organizationMembers` for the first time sees its whole surface three times with no way to
1815
+ * tell that two of them are conveniences. The description says so in words a reader acts on,
1816
+ * and `x-wildo.aliasOfPath` gives a renderer something to group or fold on.
1817
+ *
1818
+ * NOT `deprecated`: an alias is a supported route and a client calling it is doing nothing
1819
+ * wrong. Saying "prefer" would be an instruction nobody asked this generator to give.
1820
+ */
1821
+ const canonicalPath = splitOperationQualifiers(op).via.length === 0
1822
+ ? undefined
1823
+ : canonicalPathByRoute.get(canonicalRouteKey(op));
1824
+ if (canonicalPath !== undefined && canonicalPath !== op.path) {
1825
+ const wildo = operationObject['x-wildo'];
1826
+ if (isPlainObject(wildo))
1827
+ wildo['aliasOfPath'] = canonicalPath;
1828
+ const existing = typeof operationObject['description'] === 'string' ? operationObject['description'] : '';
1829
+ 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.`;
1830
+ operationObject['description'] = existing.length > 0 ? `${existing}\n\n${note}` : note;
1831
+ }
1832
+ hoistOperationSchemas(operationObject, op, schemaRegistry, purposeByResourceIdentifier.get(op.resourceIdentifier));
1833
+ if (op.baseOperationIdentifier === 'read' && splitOperationQualifiers(op).via.length === 0 && !canonicalReadResponseRefByResource.has(op.resourceIdentifier)) {
1834
+ const okSchema = (operationObject['responses']?.['200']?.content?.['application/json']?.schema);
1835
+ if (typeof okSchema?.$ref === 'string')
1836
+ canonicalReadResponseRefByResource.set(op.resourceIdentifier, okSchema.$ref);
1837
+ }
1381
1838
  paths[op.path][op.httpVerb] = operationObject;
1382
1839
  ownershipByPathAndVerb.set(collisionKey, `${op.resourceIdentifier}.${op.operationKey}`);
1383
1840
  tagSet.add(op.resourceIdentifier);
@@ -1396,6 +1853,7 @@ function buildOpenApiDocument(section, operations, input) {
1396
1853
  }
1397
1854
  resourceTagsByName.set(tag.name, tag);
1398
1855
  }
1856
+ const webhooks = buildWebhooksObject(sortedOps, canonicalReadResponseRefByResource);
1399
1857
  return {
1400
1858
  openapi: '3.1.0',
1401
1859
  info: {
@@ -1429,6 +1887,9 @@ function buildOpenApiDocument(section, operations, input) {
1429
1887
  return description !== undefined && description.length > 0 ? { ...tag, description } : tag;
1430
1888
  }),
1431
1889
  paths,
1890
+ // `webhooks` (OpenAPI 3.1) = what this application SENDS. The reference described only what a
1891
+ // client calls; an integrator writing the receiving endpoint had no machine-readable contract.
1892
+ ...(Object.keys(webhooks).length > 0 ? { webhooks } : {}),
1432
1893
  // `components.schemas` = hoisted, content-deduped request/response body
1433
1894
  // models (named `<Resource><Verb><Quals>Request|Response`); `securitySchemes`
1434
1895
  // = framework-universal auth, referenced by each op's `security`.
@@ -1436,6 +1897,12 @@ function buildOpenApiDocument(section, operations, input) {
1436
1897
  ...(Object.keys(schemaRegistry.schemas()).length > 0
1437
1898
  ? { schemas: schemaRegistry.schemas() }
1438
1899
  : {}),
1900
+ // `responses` = the error responses the GENERATOR words (the access-mode 401s and the
1901
+ // universal 500). An authored response stays inline on its operation, because its
1902
+ // description is written for that one operation and is the document's most useful prose.
1903
+ ...(usedErrorResponseNames.size > 0
1904
+ ? { responses: buildErrorResponseComponents(usedErrorResponseNames, errorResponseSchema) }
1905
+ : {}),
1439
1906
  securitySchemes: SECURITY_SCHEMES,
1440
1907
  },
1441
1908
  };