@wildo-ai/saas-technical-doc 1.1.1 → 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 (123) hide show
  1. package/dist/esm/companion/application-documentation/application-administration-documentation.d.ts +52 -0
  2. package/dist/esm/companion/application-documentation/application-administration-documentation.d.ts.map +1 -0
  3. package/dist/esm/companion/application-documentation/application-administration-documentation.js +58 -0
  4. package/dist/esm/companion/application-documentation/application-administration-documentation.js.map +1 -0
  5. package/dist/esm/companion/application-documentation/application-authentication-documentation.d.ts +76 -0
  6. package/dist/esm/companion/application-documentation/application-authentication-documentation.d.ts.map +1 -0
  7. package/dist/esm/companion/application-documentation/application-authentication-documentation.js +116 -0
  8. package/dist/esm/companion/application-documentation/application-authentication-documentation.js.map +1 -0
  9. package/dist/esm/companion/application-documentation/application-connection-documentation.d.ts +111 -0
  10. package/dist/esm/companion/application-documentation/application-connection-documentation.d.ts.map +1 -0
  11. package/dist/esm/companion/application-documentation/application-connection-documentation.js +165 -0
  12. package/dist/esm/companion/application-documentation/application-connection-documentation.js.map +1 -0
  13. package/dist/esm/companion/application-documentation/application-domain-documentation.d.ts +133 -0
  14. package/dist/esm/companion/application-documentation/application-domain-documentation.d.ts.map +1 -0
  15. package/dist/esm/companion/application-documentation/application-domain-documentation.js +243 -0
  16. package/dist/esm/companion/application-documentation/application-domain-documentation.js.map +1 -0
  17. package/dist/esm/companion/application-documentation/application-integration-documentation.d.ts +48 -0
  18. package/dist/esm/companion/application-documentation/application-integration-documentation.d.ts.map +1 -0
  19. package/dist/esm/companion/application-documentation/application-integration-documentation.js +86 -0
  20. package/dist/esm/companion/application-documentation/application-integration-documentation.js.map +1 -0
  21. package/dist/esm/companion/application-documentation/application-organization-role-documentation.d.ts +30 -0
  22. package/dist/esm/companion/application-documentation/application-organization-role-documentation.d.ts.map +1 -1
  23. package/dist/esm/companion/application-documentation/application-organization-role-documentation.js +38 -0
  24. package/dist/esm/companion/application-documentation/application-organization-role-documentation.js.map +1 -1
  25. package/dist/esm/companion/application-documentation/technical-documentation-engine-content-bundle.d.ts +82 -4
  26. package/dist/esm/companion/application-documentation/technical-documentation-engine-content-bundle.d.ts.map +1 -1
  27. package/dist/esm/companion/application-documentation/technical-documentation-engine-content-bundle.js +412 -219
  28. package/dist/esm/companion/application-documentation/technical-documentation-engine-content-bundle.js.map +1 -1
  29. package/dist/esm/companion/application-documentation/technical-documentation-private-derivation.d.ts +10 -0
  30. package/dist/esm/companion/application-documentation/technical-documentation-private-derivation.d.ts.map +1 -1
  31. package/dist/esm/companion/application-documentation/technical-documentation-private-derivation.js +9 -1
  32. package/dist/esm/companion/application-documentation/technical-documentation-private-derivation.js.map +1 -1
  33. package/dist/esm/companion/application-documentation/technical-documentation-publication-policy.js +1 -1
  34. package/dist/esm/companion/application-documentation/technical-documentation-publication-policy.js.map +1 -1
  35. package/dist/esm/companion/index.d.ts +6 -1
  36. package/dist/esm/companion/index.d.ts.map +1 -1
  37. package/dist/esm/companion/index.js +6 -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 +497 -32
  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/rendering/technical-documentation-docusaurus-renderer.d.ts +7 -1
  52. package/dist/esm/companion/rendering/technical-documentation-docusaurus-renderer.d.ts.map +1 -1
  53. package/dist/esm/companion/rendering/technical-documentation-docusaurus-renderer.js +44 -20
  54. package/dist/esm/companion/rendering/technical-documentation-docusaurus-renderer.js.map +1 -1
  55. package/dist/esm/companion/rendering/technical-documentation-managed-tree-validator.d.ts.map +1 -1
  56. package/dist/esm/companion/rendering/technical-documentation-managed-tree-validator.js +8 -1
  57. package/dist/esm/companion/rendering/technical-documentation-managed-tree-validator.js.map +1 -1
  58. package/dist/esm/companion/rendering/technical-documentation-markdown-links.d.ts +28 -0
  59. package/dist/esm/companion/rendering/technical-documentation-markdown-links.d.ts.map +1 -0
  60. package/dist/esm/companion/rendering/technical-documentation-markdown-links.js +52 -0
  61. package/dist/esm/companion/rendering/technical-documentation-markdown-links.js.map +1 -0
  62. package/dist/esm/companion/rendering/technical-documentation-markdown-renderer.d.ts +0 -9
  63. package/dist/esm/companion/rendering/technical-documentation-markdown-renderer.d.ts.map +0 -1
  64. package/dist/esm/companion/rendering/technical-documentation-markdown-renderer.js +0 -106
  65. package/dist/esm/companion/rendering/technical-documentation-markdown-renderer.js.map +0 -1
  66. package/dist/esm/companion/rendering/technical-documentation-render-model.d.ts +1 -0
  67. package/dist/esm/companion/rendering/technical-documentation-render-model.d.ts.map +1 -1
  68. package/dist/esm/companion/rendering/technical-documentation-render-model.js +112 -88
  69. package/dist/esm/companion/rendering/technical-documentation-render-model.js.map +1 -1
  70. package/dist/esm/companion/rendering/technical-documentation-search-index-renderer.d.ts +10 -0
  71. package/dist/esm/companion/rendering/technical-documentation-search-index-renderer.d.ts.map +1 -1
  72. package/dist/esm/companion/rendering/technical-documentation-search-index-renderer.js +10 -15
  73. package/dist/esm/companion/rendering/technical-documentation-search-index-renderer.js.map +1 -1
  74. package/dist/esm/companion/technical-documentation-capture-execution-port.d.ts +27 -1
  75. package/dist/esm/companion/technical-documentation-capture-execution-port.d.ts.map +1 -1
  76. package/dist/esm/companion/technical-documentation-capture-execution-port.js.map +1 -1
  77. package/dist/esm/companion/technical-documentation-diagram-definitions.d.ts +33 -0
  78. package/dist/esm/companion/technical-documentation-diagram-definitions.d.ts.map +1 -0
  79. package/dist/esm/companion/technical-documentation-diagram-definitions.js +54 -0
  80. package/dist/esm/companion/technical-documentation-diagram-definitions.js.map +1 -0
  81. package/dist/esm/companion/technical-documentation-diagram-materializer.d.ts +10 -18
  82. package/dist/esm/companion/technical-documentation-diagram-materializer.d.ts.map +1 -1
  83. package/dist/esm/companion/technical-documentation-diagram-materializer.js +9 -39
  84. package/dist/esm/companion/technical-documentation-diagram-materializer.js.map +1 -1
  85. package/dist/esm/config/wildo-tech-doc-config.schemas.d.ts +8 -4
  86. package/dist/esm/config/wildo-tech-doc-config.schemas.d.ts.map +1 -1
  87. package/dist/esm/config/wildo-tech-doc-config.schemas.js +8 -4
  88. package/dist/esm/config/wildo-tech-doc-config.schemas.js.map +1 -1
  89. package/dist/esm/content/application-consumer-documentation-content.techdoc.d.ts +105 -122
  90. package/dist/esm/content/application-consumer-documentation-content.techdoc.d.ts.map +1 -1
  91. package/dist/esm/content/application-consumer-documentation-content.techdoc.js +934 -2161
  92. package/dist/esm/content/application-consumer-documentation-content.techdoc.js.map +1 -1
  93. package/dist/esm/runtime/DocsAuthContext.d.ts +16 -1
  94. package/dist/esm/runtime/DocsAuthContext.d.ts.map +1 -1
  95. package/dist/esm/runtime/DocsAuthContext.js +18 -2
  96. package/dist/esm/runtime/DocsAuthContext.js.map +1 -1
  97. package/dist/esm/runtime/decode-jwt-claims.d.ts +7 -4
  98. package/dist/esm/runtime/decode-jwt-claims.d.ts.map +1 -1
  99. package/dist/esm/runtime/decode-jwt-claims.js +7 -4
  100. package/dist/esm/runtime/decode-jwt-claims.js.map +1 -1
  101. package/dist/esm/runtime/docs-auth-session.schemas.d.ts +16 -5
  102. package/dist/esm/runtime/docs-auth-session.schemas.d.ts.map +1 -1
  103. package/dist/esm/runtime/docs-auth-session.schemas.js +16 -5
  104. package/dist/esm/runtime/docs-auth-session.schemas.js.map +1 -1
  105. package/dist/esm/runtime/frontend-provider-registry.techdoc.d.ts +35 -13
  106. package/dist/esm/runtime/frontend-provider-registry.techdoc.d.ts.map +1 -1
  107. package/dist/esm/runtime/frontend-provider-registry.techdoc.js +28 -19
  108. package/dist/esm/runtime/frontend-provider-registry.techdoc.js.map +1 -1
  109. package/dist/esm/runtime/index.d.ts +1 -0
  110. package/dist/esm/runtime/index.d.ts.map +1 -1
  111. package/dist/esm/runtime/index.js +1 -0
  112. package/dist/esm/runtime/index.js.map +1 -1
  113. package/dist/esm/runtime/use-docs-auth-session.d.ts +9 -7
  114. package/dist/esm/runtime/use-docs-auth-session.d.ts.map +1 -1
  115. package/dist/esm/runtime/use-docs-auth-session.js +9 -7
  116. package/dist/esm/runtime/use-docs-auth-session.js.map +1 -1
  117. package/dist/esm/runtime/use-docs-provider-sdks.d.ts +21 -0
  118. package/dist/esm/runtime/use-docs-provider-sdks.d.ts.map +1 -0
  119. package/dist/esm/runtime/use-docs-provider-sdks.js +49 -0
  120. package/dist/esm/runtime/use-docs-provider-sdks.js.map +1 -0
  121. package/dist/tsconfig.build.tsbuildinfo +1 -1
  122. package/package.json +6 -5
  123. package/dist/esm/.builder.pid +0 -9
@@ -41,6 +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, ERROR_DEFINITIONS, ErrorHandling_Strategy, ErrorSeverity, ErrorType, M2M_WEBHOOK_DELIVERY_CONTRACT, Resources_PaginationRequestSchema, } from '@wildo-ai/saas-models';
44
45
  import { OpenApiSection, } from '../openapi/openapi-generation-output.schemas.js';
45
46
  import { OpenApiGenerationInputSchema, OPENAPI_RESOURCE_IDENTITY_CONTRACT_VERSION, OperationProjectionAuthenticationMode, OperationProjectionVariantType, } from './operation-projection.schemas.js';
46
47
  import { stripImplementationNoise } from './spec-to-operation-doc.js';
@@ -344,6 +345,58 @@ function extractPathParameters(path) {
344
345
  const matches = path.matchAll(/\{([^}]+)\}/gu);
345
346
  return Array.from(matches, (match) => match[1]);
346
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
+ }
347
400
  function isPlainObject(value) {
348
401
  return typeof value === 'object' && value !== null && !Array.isArray(value);
349
402
  }
@@ -403,7 +456,7 @@ function buildQueryParametersFromRequestSchema(op) {
403
456
  // operation. The flat trio names are excluded too so a DTO can never collide with
404
457
  // them. Ops WITHOUT the pagination contract keep every DTO property untouched.
405
458
  const paginationAxisProperties = op.pagination !== undefined
406
- ? new Set(['pagination', 'sorting', 'filters', 'page', 'limit', 'sort'])
459
+ ? new Set(['pagination', 'sorting', 'filters', COLLECTION_QUERY_PARAMETERS.page, COLLECTION_QUERY_PARAMETERS.limit, COLLECTION_QUERY_PARAMETERS.sort])
407
460
  : new Set();
408
461
  return Object.entries(properties)
409
462
  .filter(([name]) => !paginationAxisProperties.has(name))
@@ -522,7 +575,7 @@ function buildCollectionFilterQueryParameters(op) {
522
575
  // nested fallback name, so it is surfaced under the wire name, not verbatim.
523
576
  if ('searchRequest' in filterProperties) {
524
577
  parameters.push(attachParameterExamples(op, {
525
- name: 'q',
578
+ name: COLLECTION_QUERY_PARAMETERS.search,
526
579
  in: 'query',
527
580
  required: false,
528
581
  schema: { type: 'string' },
@@ -574,8 +627,9 @@ function buildCollectionFilterQueryParameters(op) {
574
627
  * must not import `@wildo-ai/saas-backend-lib`); keep in sync with the
575
628
  * repository defaults.
576
629
  */
577
- const PAGINATION_DEFAULT_PAGE = 1;
578
- const PAGINATION_DEFAULT_LIMIT = 20;
630
+ // Defaults are read off the shared pagination request schema, never restated.
631
+ const PAGINATION_DEFAULT_PAGE = Resources_PaginationRequestSchema.shape.page.parse(undefined);
632
+ const PAGINATION_DEFAULT_LIMIT = Resources_PaginationRequestSchema.shape.limit.parse(undefined);
579
633
  /**
580
634
  * Emits the flat `page` / `limit` / `sort` query parameters for a paginated
581
635
  * collection operation (`op.pagination` present — LIST / SEARCH-like; see the
@@ -613,21 +667,21 @@ function buildPaginationQueryParameters(op) {
613
667
  + '(e.g. `createdAt:desc`). Defaults to `createdAt:desc` (newest first).';
614
668
  return [
615
669
  {
616
- name: 'page',
670
+ name: COLLECTION_QUERY_PARAMETERS.page,
617
671
  in: 'query',
618
672
  required: false,
619
673
  schema: { type: 'integer', minimum: 1, default: PAGINATION_DEFAULT_PAGE },
620
674
  description: '1-based page number of the collection slice.',
621
675
  },
622
676
  {
623
- name: 'limit',
677
+ name: COLLECTION_QUERY_PARAMETERS.limit,
624
678
  in: 'query',
625
679
  required: false,
626
680
  schema: { type: 'integer', minimum: 1, maximum: pagination.maxLimit, default: PAGINATION_DEFAULT_LIMIT },
627
681
  description: `Maximum number of items per page (up to ${pagination.maxLimit}).`,
628
682
  },
629
683
  {
630
- name: 'sort',
684
+ name: COLLECTION_QUERY_PARAMETERS.sort,
631
685
  in: 'query',
632
686
  required: false,
633
687
  schema: { type: 'string' },
@@ -736,6 +790,26 @@ function buildOperationObject(op, operationId, errorResponseSchema) {
736
790
  // Auth (Tier-1 framework knowledge). The source projector resolves access
737
791
  // semantics once; this generator only transports that fact into standard
738
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
+ }
739
813
  const security = buildOperationSecurity(op);
740
814
  operationObject['security'] = security;
741
815
  if (op.access.authenticationMode !== OperationProjectionAuthenticationMode.PUBLIC && op.access.roleRequirements.length > 0) {
@@ -749,6 +823,7 @@ function buildOperationObject(op, operationId, errorResponseSchema) {
749
823
  in: 'path',
750
824
  required: true,
751
825
  schema: { type: 'string' },
826
+ description: describePathParameter(op.path, paramName, op.resourceIdentifier),
752
827
  })));
753
828
  }
754
829
  if (op.stepUpAuthentication !== undefined) {
@@ -888,6 +963,34 @@ const ENGINE_METADATA_PROPERTY_SCHEMAS = {
888
963
  + 'conflict-free merges of concurrent edits. Opaque to clients — safe to ignore.',
889
964
  },
890
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
+ });
891
994
  /**
892
995
  * The structural signature of a MATERIALIZED resource document in a response
893
996
  * schema is the `_id` property. The serializer attaches `_version` /
@@ -953,11 +1056,47 @@ function injectEngineMetadataIntoEntitySchemas(schema) {
953
1056
  * (`stableStringify`) still collapses them onto one component. Non-object
954
1057
  * schemas (`null` / scalar) pass through unchanged.
955
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
+ }
956
1092
  function augmentResponseSchemaWithEngineMetadata(responseBodySchema) {
957
1093
  if (!isPlainObject(responseBodySchema)) {
958
1094
  return responseBodySchema;
959
1095
  }
960
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);
961
1100
  const properties = clone['properties'];
962
1101
  const dataSchema = isPlainObject(properties) ? properties['data'] : undefined;
963
1102
  if (isPlainObject(dataSchema) && (dataSchema['type'] === 'array' || 'items' in dataSchema)) {
@@ -1091,27 +1230,151 @@ function buildResponsesObject(op, responseBodySchema, errorResponseSchema) {
1091
1230
  // 401 contract. A resource-owned 401 is more specific (for example a
1092
1231
  // step-up flow) and deliberately takes precedence.
1093
1232
  if (op.access.authenticationMode !== OperationProjectionAuthenticationMode.PUBLIC && responses['401'] === undefined) {
1094
- const description = (() => {
1095
- switch (op.access.authenticationMode) {
1096
- case OperationProjectionAuthenticationMode.ANONYMOUS_SESSION:
1097
- return 'The anonymous-session token is missing, malformed, expired, or invalid.';
1098
- case OperationProjectionAuthenticationMode.ANONYMOUS_OR_AUTHENTICATED:
1099
- return 'No valid anonymous-session token, bearer token, or API key was supplied.';
1100
- case OperationProjectionAuthenticationMode.AUTHENTICATED:
1101
- return 'No valid bearer token or API key was supplied.';
1102
- default: {
1103
- const exhaustiveAuthenticationMode = op.access.authenticationMode;
1104
- throw new Error(`OpenAPI generator: unsupported authentication mode ${String(exhaustiveAuthenticationMode)}`);
1105
- }
1106
- }
1107
- })();
1108
- responses['401'] = {
1109
- description,
1110
- content: { 'application/json': { schema: errorResponseSchema } },
1111
- };
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}` };
1112
1244
  }
1113
1245
  return responses;
1114
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
+ }
1115
1378
  /**
1116
1379
  * Builds the OpenAPI request-body `examples` map from authored examples that
1117
1380
  * carry a `request` payload. Returns undefined when there are none.
@@ -1173,6 +1436,23 @@ function stableStringify(value) {
1173
1436
  }
1174
1437
  return JSON.stringify(value) ?? 'null';
1175
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 = {}));
1176
1456
  /**
1177
1457
  * Component name for an operation's request/response body. Mirrors the
1178
1458
  * verb-first operationId namer (shares `pascal`): `<Resource><BaseVerb><Quals><Role>`,
@@ -1193,15 +1473,84 @@ function componentSchemaName(op, role) {
1193
1473
  .join('');
1194
1474
  return `${pascal(op.resourceIdentifier)}${pascal(op.baseOperationIdentifier)}${schemaQualifiers}${role}`;
1195
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
+ }
1196
1538
  function createSchemaRegistry() {
1197
1539
  const nameBySemanticIdentityAndContent = new Map();
1198
1540
  const contentByName = new Map();
1199
1541
  const schemas = {};
1200
1542
  return {
1201
- ref(schema, desiredName) {
1543
+ ref(schema, desiredName, description) {
1202
1544
  if (!isHoistableSchema(schema)) {
1203
1545
  return schema;
1204
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
+ */
1205
1554
  const canonical = stableStringify(schema);
1206
1555
  const semanticIdentityAndContent = `${desiredName}\u0000${canonical}`;
1207
1556
  const existingName = nameBySemanticIdentityAndContent.get(semanticIdentityAndContent);
@@ -1216,7 +1565,17 @@ function createSchemaRegistry() {
1216
1565
  }
1217
1566
  nameBySemanticIdentityAndContent.set(semanticIdentityAndContent, name);
1218
1567
  contentByName.set(name, canonical);
1219
- 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;
1220
1579
  return { $ref: `#/components/schemas/${name}` };
1221
1580
  },
1222
1581
  schemas() {
@@ -1232,12 +1591,29 @@ function createSchemaRegistry() {
1232
1591
  * inline. All success statuses share one response schema object, so they
1233
1592
  * resolve to the same `$ref`. Mutates `operationObject` in place.
1234
1593
  */
1235
- 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) {
1236
1612
  const requestBody = operationObject['requestBody'];
1237
1613
  if (isPlainObject(requestBody) && isPlainObject(requestBody['content'])) {
1238
1614
  const media = requestBody['content']['application/json'];
1239
1615
  if (isPlainObject(media) && isHoistableSchema(media['schema'])) {
1240
- 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));
1241
1617
  }
1242
1618
  }
1243
1619
  const responses = operationObject['responses'];
@@ -1248,7 +1624,7 @@ function hoistOperationSchemas(operationObject, op, registry) {
1248
1624
  }
1249
1625
  const media = entry['content']['application/json'];
1250
1626
  if (isPlainObject(media) && isHoistableSchema(media['schema'])) {
1251
- 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));
1252
1628
  }
1253
1629
  }
1254
1630
  }
@@ -1342,6 +1718,44 @@ function buildOpenApiDocument(section, operations, input) {
1342
1718
  const sectionLabel = section === OpenApiSection.API_REFERENCE
1343
1719
  ? 'API reference'
1344
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();
1345
1759
  const paths = {};
1346
1760
  const ownershipByPathAndVerb = new Map();
1347
1761
  const tagSet = new Set();
@@ -1358,7 +1772,15 @@ function buildOpenApiDocument(section, operations, input) {
1358
1772
  // The schema comes from the same model the backend error handler serializes.
1359
1773
  // Register before per-operation hoisting so every non-success response shares
1360
1774
  // the stable, named component instead of producing resource-local copies.
1361
- 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.');
1362
1784
  for (const op of sortedOps) {
1363
1785
  if (paths[op.path] === undefined) {
1364
1786
  paths[op.path] = {};
@@ -1375,7 +1797,40 @@ function buildOpenApiDocument(section, operations, input) {
1375
1797
  operationIdUseCount.set(baseOperationId, priorUses + 1);
1376
1798
  const operationId = priorUses === 0 ? baseOperationId : `${baseOperationId}${priorUses + 1}`;
1377
1799
  const operationObject = buildOperationObject(op, operationId, errorResponseSchema);
1378
- 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
+ }
1379
1834
  paths[op.path][op.httpVerb] = operationObject;
1380
1835
  ownershipByPathAndVerb.set(collisionKey, `${op.resourceIdentifier}.${op.operationKey}`);
1381
1836
  tagSet.add(op.resourceIdentifier);
@@ -1394,6 +1849,7 @@ function buildOpenApiDocument(section, operations, input) {
1394
1849
  }
1395
1850
  resourceTagsByName.set(tag.name, tag);
1396
1851
  }
1852
+ const webhooks = buildWebhooksObject(sortedOps, canonicalReadResponseRefByResource);
1397
1853
  return {
1398
1854
  openapi: '3.1.0',
1399
1855
  info: {
@@ -1427,6 +1883,9 @@ function buildOpenApiDocument(section, operations, input) {
1427
1883
  return description !== undefined && description.length > 0 ? { ...tag, description } : tag;
1428
1884
  }),
1429
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 } : {}),
1430
1889
  // `components.schemas` = hoisted, content-deduped request/response body
1431
1890
  // models (named `<Resource><Verb><Quals>Request|Response`); `securitySchemes`
1432
1891
  // = framework-universal auth, referenced by each op's `security`.
@@ -1434,6 +1893,12 @@ function buildOpenApiDocument(section, operations, input) {
1434
1893
  ...(Object.keys(schemaRegistry.schemas()).length > 0
1435
1894
  ? { schemas: schemaRegistry.schemas() }
1436
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
+ : {}),
1437
1902
  securitySchemes: SECURITY_SCHEMES,
1438
1903
  },
1439
1904
  };