@topogram/cli 0.3.64 → 0.3.65

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 (245) hide show
  1. package/package.json +1 -1
  2. package/src/adoption/plan/index.js +703 -0
  3. package/src/adoption/plan.js +12 -703
  4. package/src/agent-ops/query-builders/auth.js +375 -0
  5. package/src/agent-ops/query-builders/change-risk/change-plan.js +123 -0
  6. package/src/agent-ops/query-builders/change-risk/import-plan.js +49 -0
  7. package/src/agent-ops/query-builders/change-risk/maintained.js +286 -0
  8. package/src/agent-ops/query-builders/change-risk/review-packets.js +123 -0
  9. package/src/agent-ops/query-builders/change-risk/risk.js +189 -0
  10. package/src/agent-ops/query-builders/change-risk.js +25 -0
  11. package/src/agent-ops/query-builders/common.js +149 -0
  12. package/src/agent-ops/query-builders/maintained-risk.js +539 -0
  13. package/src/agent-ops/query-builders/maintained-shared.js +120 -0
  14. package/src/agent-ops/query-builders/multi-agent.js +547 -0
  15. package/src/agent-ops/query-builders/projection-impacts.js +514 -0
  16. package/src/agent-ops/query-builders/work-packets.js +417 -0
  17. package/src/agent-ops/query-builders/workflow-context-shared.js +300 -0
  18. package/src/agent-ops/query-builders/workflow-context.js +398 -0
  19. package/src/agent-ops/query-builders/workflow-presets-core.js +676 -0
  20. package/src/agent-ops/query-builders/workflow-presets.js +341 -0
  21. package/src/agent-ops/query-builders.d.ts +26 -26
  22. package/src/agent-ops/query-builders.js +42 -5021
  23. package/src/catalog/constants.js +10 -0
  24. package/src/catalog/copy.js +60 -0
  25. package/src/catalog/diagnostics.js +15 -0
  26. package/src/catalog/entries.js +42 -0
  27. package/src/catalog/files.js +67 -0
  28. package/src/catalog/provenance.js +122 -0
  29. package/src/catalog/source.js +150 -0
  30. package/src/catalog/validation.js +252 -0
  31. package/src/catalog.d.ts +2 -0
  32. package/src/catalog.js +18 -746
  33. package/src/cli/commands/catalog/check.js +31 -0
  34. package/src/cli/commands/catalog/copy.js +59 -0
  35. package/src/cli/commands/catalog/doctor.js +248 -0
  36. package/src/cli/commands/catalog/help.js +21 -0
  37. package/src/cli/commands/catalog/list.js +52 -0
  38. package/src/cli/commands/catalog/runner.js +92 -0
  39. package/src/cli/commands/catalog/shared.js +17 -0
  40. package/src/cli/commands/catalog/show.js +134 -0
  41. package/src/cli/commands/catalog.js +30 -615
  42. package/src/cli/commands/generator-policy/package-info.js +162 -0
  43. package/src/cli/commands/generator-policy/payloads.js +372 -0
  44. package/src/cli/commands/generator-policy/printers.js +159 -0
  45. package/src/cli/commands/generator-policy/runner.js +81 -0
  46. package/src/cli/commands/generator-policy/shared.js +39 -0
  47. package/src/cli/commands/generator-policy.js +15 -783
  48. package/src/cli/commands/import/adopt.js +170 -0
  49. package/src/cli/commands/import/check.js +91 -0
  50. package/src/cli/commands/import/diff.js +84 -0
  51. package/src/cli/commands/import/help.js +47 -0
  52. package/src/cli/commands/import/paths.js +277 -0
  53. package/src/cli/commands/import/plan.js +284 -0
  54. package/src/cli/commands/import/refresh.js +470 -0
  55. package/src/cli/commands/import/status-history.js +196 -0
  56. package/src/cli/commands/import/workspace.js +230 -0
  57. package/src/cli/commands/import.js +33 -1732
  58. package/src/cli/commands/package/constants.js +17 -0
  59. package/src/cli/commands/package/doctor.js +240 -0
  60. package/src/cli/commands/package/help.js +27 -0
  61. package/src/cli/commands/package/lockfile.js +135 -0
  62. package/src/cli/commands/package/npm.js +97 -0
  63. package/src/cli/commands/package/reporting.js +35 -0
  64. package/src/cli/commands/package/runner.js +33 -0
  65. package/src/cli/commands/package/shared.js +9 -0
  66. package/src/cli/commands/package/update-cli.js +252 -0
  67. package/src/cli/commands/package/versions.js +35 -0
  68. package/src/cli/commands/package.js +29 -813
  69. package/src/cli/commands/query/change-plan.js +68 -0
  70. package/src/cli/commands/query/definitions.js +202 -0
  71. package/src/cli/commands/query/import-adopt.js +121 -0
  72. package/src/cli/commands/query/runner/artifacts.js +102 -0
  73. package/src/cli/commands/query/runner/boundaries.js +211 -0
  74. package/src/cli/commands/query/runner/change.js +182 -0
  75. package/src/cli/commands/query/runner/import-adopt.js +111 -0
  76. package/src/cli/commands/query/runner/index.js +31 -0
  77. package/src/cli/commands/query/runner/output.js +12 -0
  78. package/src/cli/commands/query/runner/workflow.js +241 -0
  79. package/src/cli/commands/query/runner.js +3 -0
  80. package/src/cli/commands/query/workflow-context.js +5 -0
  81. package/src/cli/commands/query/workspace.js +274 -0
  82. package/src/cli/commands/query.js +9 -1300
  83. package/src/cli/commands/template/baseline.js +100 -0
  84. package/src/cli/commands/template/check.js +466 -0
  85. package/src/cli/commands/template/constants.js +8 -0
  86. package/src/cli/commands/template/diagnostics.js +26 -0
  87. package/src/cli/commands/template/help.js +28 -0
  88. package/src/cli/commands/template/lifecycle.js +404 -0
  89. package/src/cli/commands/template/list-show.js +287 -0
  90. package/src/cli/commands/template/policy.js +422 -0
  91. package/src/cli/commands/template/shared.js +127 -0
  92. package/src/cli/commands/template/updates.js +352 -0
  93. package/src/cli/commands/template.js +41 -2143
  94. package/src/generator/api/contracts.js +497 -0
  95. package/src/generator/api/metadata.js +221 -0
  96. package/src/generator/api/openapi.js +559 -0
  97. package/src/generator/api/schema.js +124 -0
  98. package/src/generator/api/types.d.ts +98 -0
  99. package/src/generator/api.js +3 -1195
  100. package/src/generator/context/shared/domain-sdlc.js +282 -0
  101. package/src/generator/context/shared/maintained-boundary.js +665 -0
  102. package/src/generator/context/shared/metrics.js +85 -0
  103. package/src/generator/context/shared/primitives.js +64 -0
  104. package/src/generator/context/shared/relationships.js +453 -0
  105. package/src/generator/context/shared/summaries.js +263 -0
  106. package/src/generator/context/shared/types.d.ts +207 -0
  107. package/src/generator/context/shared.d.ts +42 -0
  108. package/src/generator/context/shared.js +80 -1390
  109. package/src/generator/context/slice/core.js +397 -0
  110. package/src/generator/context/slice/sdlc.js +417 -0
  111. package/src/generator/context/slice/ui-packets.js +183 -0
  112. package/src/generator/context/slice.js +2 -859
  113. package/src/generator/registry/index.js +507 -0
  114. package/src/generator/registry.js +18 -504
  115. package/src/generator/runtime/environment/index.js +666 -0
  116. package/src/generator/runtime/environment.js +4 -666
  117. package/src/generator/runtime/runtime-check/index.js +554 -0
  118. package/src/generator/runtime/runtime-check.js +4 -554
  119. package/src/generator/runtime/shared/index.js +572 -0
  120. package/src/generator/runtime/shared.js +19 -570
  121. package/src/generator/shared.d.ts +2 -0
  122. package/src/generator/surfaces/shared.d.ts +3 -0
  123. package/src/generator/widget-conformance/behavior-report.js +258 -0
  124. package/src/generator/widget-conformance/checks.js +371 -0
  125. package/src/generator/widget-conformance/projection-context.js +200 -0
  126. package/src/generator/widget-conformance/report.js +166 -0
  127. package/src/generator/widget-conformance/types.d.ts +121 -0
  128. package/src/generator/widget-conformance.js +3 -824
  129. package/src/import/core/context.d.ts +3 -0
  130. package/src/import/core/contracts.d.ts +1 -0
  131. package/src/import/core/registry.d.ts +4 -0
  132. package/src/import/core/runner/candidates.js +217 -0
  133. package/src/import/core/runner/options.js +22 -0
  134. package/src/import/core/runner/reports.js +50 -0
  135. package/src/import/core/runner/run.js +79 -0
  136. package/src/import/core/runner/tracks.js +150 -0
  137. package/src/import/core/runner/ui-drafts.js +337 -0
  138. package/src/import/core/runner.js +3 -698
  139. package/src/import/core/shared/api-routes.js +221 -0
  140. package/src/import/core/shared/candidates.js +97 -0
  141. package/src/import/core/shared/files.js +177 -0
  142. package/src/import/core/shared/next-app.js +389 -0
  143. package/src/import/core/shared/types.d.ts +51 -0
  144. package/src/import/core/shared/ui-routes.js +230 -0
  145. package/src/import/core/shared.js +60 -861
  146. package/src/new-project/constants.js +128 -0
  147. package/src/new-project/create.js +83 -0
  148. package/src/new-project/json.js +28 -0
  149. package/src/new-project/metadata.js +96 -0
  150. package/src/new-project/package-spec.js +161 -0
  151. package/src/new-project/project-files.js +348 -0
  152. package/src/new-project/template-policy.js +269 -0
  153. package/src/new-project/template-resolution.js +368 -0
  154. package/src/new-project/template-snapshots.js +430 -0
  155. package/src/new-project/template-updates.js +512 -0
  156. package/src/new-project/types.d.ts +83 -0
  157. package/src/new-project.js +6 -2277
  158. package/src/parser.d.ts +87 -1
  159. package/src/parser.js +118 -0
  160. package/src/policy/review-boundaries.d.ts +15 -0
  161. package/src/project-config/index.js +564 -0
  162. package/src/project-config.js +19 -561
  163. package/src/resolver/enrich/acceptance-criterion.js +2 -0
  164. package/src/resolver/enrich/bug.js +2 -0
  165. package/src/resolver/enrich/pitch.js +2 -0
  166. package/src/resolver/enrich/requirement.js +2 -0
  167. package/src/resolver/enrich/task.js +2 -0
  168. package/src/resolver/index.js +19 -2089
  169. package/src/resolver/normalize.js +384 -1
  170. package/src/resolver/plans.js +168 -0
  171. package/src/resolver/projections-api.js +494 -0
  172. package/src/resolver/projections-db.js +133 -0
  173. package/src/resolver/projections-ui.js +317 -0
  174. package/src/resolver/shapes.js +251 -0
  175. package/src/resolver/shared.js +278 -0
  176. package/src/resolver/widgets.js +132 -0
  177. package/src/template-trust/constants.js +62 -0
  178. package/src/template-trust/content.js +258 -0
  179. package/src/template-trust/diff.js +92 -0
  180. package/src/template-trust/policy.js +61 -0
  181. package/src/template-trust/record.js +90 -0
  182. package/src/template-trust/status.js +182 -0
  183. package/src/template-trust.js +24 -687
  184. package/src/text-helpers.d.ts +1 -0
  185. package/src/topogram-types.d.ts +69 -0
  186. package/src/validator/common.js +488 -0
  187. package/src/validator/data-model.js +237 -0
  188. package/src/validator/docs.js +167 -0
  189. package/src/validator/expressions.js +146 -1
  190. package/src/validator/index.d.ts +23 -0
  191. package/src/validator/index.js +32 -3585
  192. package/src/validator/kinds.d.ts +41 -0
  193. package/src/validator/kinds.js +2 -0
  194. package/src/validator/model-helpers.js +46 -0
  195. package/src/validator/per-kind/acceptance-criterion.js +5 -0
  196. package/src/validator/per-kind/bug.js +6 -0
  197. package/src/validator/per-kind/domain.js +15 -2
  198. package/src/validator/per-kind/pitch.js +7 -0
  199. package/src/validator/per-kind/requirement.js +5 -0
  200. package/src/validator/per-kind/task.js +7 -0
  201. package/src/validator/per-kind/widget.js +14 -0
  202. package/src/validator/projections/api-http-async.js +410 -0
  203. package/src/validator/projections/api-http-authz.js +88 -0
  204. package/src/validator/projections/api-http-core.js +205 -0
  205. package/src/validator/projections/api-http-policies.js +339 -0
  206. package/src/validator/projections/api-http-responses.js +233 -0
  207. package/src/validator/projections/api-http.js +44 -0
  208. package/src/validator/projections/db.js +353 -0
  209. package/src/validator/projections/generator-defaults.js +45 -0
  210. package/src/validator/projections/helpers.js +87 -0
  211. package/src/validator/projections/ui-helpers.js +214 -0
  212. package/src/validator/projections/ui-navigation.js +344 -0
  213. package/src/validator/projections/ui-structure.js +364 -0
  214. package/src/validator/projections/ui-widgets.js +493 -0
  215. package/src/validator/projections/ui.js +46 -0
  216. package/src/validator/registry.js +48 -1
  217. package/src/validator/utils.d.ts +20 -0
  218. package/src/validator/utils.js +115 -12
  219. package/src/widget-behavior.d.ts +1 -0
  220. package/src/workflows/import-app/api/collect.js +221 -0
  221. package/src/workflows/import-app/api/openapi.js +257 -0
  222. package/src/workflows/import-app/api/routes.js +327 -0
  223. package/src/workflows/import-app/api/sources.js +22 -0
  224. package/src/workflows/import-app/api.js +2 -797
  225. package/src/workflows/reconcile/adoption-plan/build.js +208 -0
  226. package/src/workflows/reconcile/adoption-plan/dependencies.js +75 -0
  227. package/src/workflows/reconcile/adoption-plan/outputs.js +143 -0
  228. package/src/workflows/reconcile/adoption-plan/paths.js +58 -0
  229. package/src/workflows/reconcile/adoption-plan/projection-patches.js +177 -0
  230. package/src/workflows/reconcile/adoption-plan/reasons.js +107 -0
  231. package/src/workflows/reconcile/adoption-plan.js +30 -740
  232. package/src/workflows/reconcile/auth/closures.js +115 -0
  233. package/src/workflows/reconcile/auth/formatters.js +142 -0
  234. package/src/workflows/reconcile/auth/inference.js +330 -0
  235. package/src/workflows/reconcile/auth/roles.js +122 -0
  236. package/src/workflows/reconcile/auth.js +35 -690
  237. package/src/workflows/reconcile/bundle-core/index.js +600 -0
  238. package/src/workflows/reconcile/bundle-core.js +12 -598
  239. package/src/workflows/reconcile/canonical-surface.js +1 -1
  240. package/src/workflows/reconcile/impacts/adoption-plan.js +192 -0
  241. package/src/workflows/reconcile/impacts/indexes.js +101 -0
  242. package/src/workflows/reconcile/impacts/patches.js +252 -0
  243. package/src/workflows/reconcile/impacts/reports.js +80 -0
  244. package/src/workflows/reconcile/impacts.js +14 -623
  245. package/src/workspace-docs.d.ts +29 -0
@@ -0,0 +1,559 @@
1
+ import { generateApiContractGraph } from "./contracts.js";
2
+ import { cloneSchema, generateShapeJsonSchema, indexStatements } from "./schema.js";
3
+
4
+ /**
5
+ * @param {string} shapeId
6
+ * @param {any} suffix
7
+ * @returns {any}
8
+ */
9
+ export function componentNameFromShape(shapeId, suffix) {
10
+ const base = shapeId.split("_").map(/** @param {any} part */ (part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
11
+ return `${base}${suffix}`;
12
+ }
13
+
14
+ /**
15
+ * @param {any} code
16
+ * @param {any} suffix
17
+ * @returns {any}
18
+ */
19
+ export function componentNameFromErrorCode(code, suffix = "Error") {
20
+ const base = code
21
+ .split(/[^A-Za-z0-9]+/)
22
+ .filter(Boolean)
23
+ .map(/** @param {any} part */ (part) => part.charAt(0).toUpperCase() + part.slice(1))
24
+ .join("");
25
+ return `${base}${suffix}`;
26
+ }
27
+
28
+ /**
29
+ * @param {any} name
30
+ * @returns {any}
31
+ */
32
+ export function refSchema(name) {
33
+ return { $ref: `#/components/schemas/${name}` };
34
+ }
35
+
36
+ /**
37
+ * @param {import("./types.d.ts").ApiField} field
38
+ * @param {any} location
39
+ * @returns {any}
40
+ */
41
+ export function openApiParameter(field, location) {
42
+ return {
43
+ name: field.transport?.wireName || field.name,
44
+ in: location,
45
+ required: location === "path" ? true : field.required,
46
+ schema: cloneSchema(field.schema)
47
+ };
48
+ }
49
+
50
+ /**
51
+ * @param {import("./types.d.ts").ApiContract} contract
52
+ * @returns {any}
53
+ */
54
+ export function requestBodySchema(contract) {
55
+ const requestFields = contract.requestContract?.transport?.body || [];
56
+ const schema = /** @type {any} */ ({ type: "object", properties: {}, additionalProperties: false });
57
+ const required = [];
58
+
59
+ for (const field of requestFields) {
60
+ schema.properties[field.transport?.wireName || field.name] = cloneSchema(field.schema);
61
+ if (field.required) {
62
+ required.push(field.transport?.wireName || field.name);
63
+ }
64
+ }
65
+
66
+ if (required.length > 0) {
67
+ schema.required = required;
68
+ }
69
+ return schema;
70
+ }
71
+
72
+ /**
73
+ * @param {import("./types.d.ts").ApiContract} contract
74
+ * @param {any} componentNames
75
+ * @returns {any}
76
+ */
77
+ export function responseSchemaForContract(contract, componentNames) {
78
+ if (!contract.responseContract) return null;
79
+
80
+ if (contract.responseContract.mode === "paged" || contract.responseContract.mode === "cursor") {
81
+ const envelopeSchemaName = componentNames.responseEnvelope.get(contract.capability.id);
82
+ if (envelopeSchemaName) return refSchema(envelopeSchemaName);
83
+ }
84
+
85
+ const responseSchemaName = componentNames.response.get(contract.responseContract.shape.id);
86
+ if (!responseSchemaName) return cloneSchema(contract.responseContract.jsonSchema);
87
+
88
+ if (contract.responseContract.collection) {
89
+ return { type: "array", items: refSchema(responseSchemaName) };
90
+ }
91
+
92
+ return refSchema(responseSchemaName);
93
+ }
94
+
95
+ /**
96
+ * @param {import("./types.d.ts").ApiContract} contract
97
+ * @returns {any}
98
+ */
99
+ export function successHeadersForContract(contract) {
100
+ const headerEntries = [
101
+ ...(contract.endpoint.cache || []).map(/** @param {any} rule */ (rule) => [rule.responseHeader, { schema: { type: "string" } }]),
102
+ ...(contract.endpoint.async || []).flatMap(/** @param {any} rule */ (rule) => [
103
+ [rule.locationHeader, { schema: { type: "string" } }],
104
+ [rule.retryAfterHeader, { schema: { type: "integer" } }]
105
+ ]),
106
+ ...(contract.endpoint.download || []).flatMap(/** @param {any} rule */ (rule) =>
107
+ rule.disposition ? [["Content-Disposition", { schema: { type: "string" } }]] : []
108
+ )
109
+ ].filter(([name]) => Boolean(name));
110
+
111
+ return headerEntries.length > 0 ? Object.fromEntries(headerEntries) : undefined;
112
+ }
113
+
114
+ /**
115
+ * @param {import("./types.d.ts").ApiContract} contract
116
+ * @returns {any}
117
+ */
118
+ export function asyncLinksForContract(contract) {
119
+ const links = /** @type {Record<string, any>} */ ({});
120
+ for (const rule of contract.endpoint.async || []) {
121
+ if (!rule.statusCapability?.id) continue;
122
+ links[`${rule.statusCapability.id}Status`] = {
123
+ operationId: rule.statusCapability.id,
124
+ parameters: { job_id: "$response.body#/job_id" },
125
+ description: `Follow ${rule.statusCapability.id} to monitor the async job`
126
+ };
127
+ }
128
+ return Object.keys(links).length > 0 ? links : undefined;
129
+ }
130
+
131
+ /**
132
+ * @param {import("./types.d.ts").ApiContract} contract
133
+ * @returns {any}
134
+ */
135
+ export function statusLinksForContract(contract) {
136
+ const links = /** @type {Record<string, any>} */ ({});
137
+ for (const rule of contract.endpoint.status || []) {
138
+ if (!rule.downloadCapability?.id) continue;
139
+ links[`${rule.downloadCapability.id}Download`] = {
140
+ operationId: rule.downloadCapability.id,
141
+ parameters: { job_id: "$response.body#/job_id" },
142
+ description: `Use ${rule.downloadCapability.id} when the job is complete`
143
+ };
144
+ }
145
+ return Object.keys(links).length > 0 ? links : undefined;
146
+ }
147
+
148
+ /**
149
+ * @param {import("./types.d.ts").ApiContract} contract
150
+ * @returns {any}
151
+ */
152
+ export function authorizationExtension(contract) {
153
+ if (!contract.endpoint.authz || contract.endpoint.authz.length === 0) return undefined;
154
+ return contract.endpoint.authz.map(/** @param {any} rule */ (rule) => ({
155
+ ...(rule.role ? { role: rule.role } : {}),
156
+ ...(rule.permission ? { permission: rule.permission } : {}),
157
+ ...(rule.claim ? { claim: rule.claim } : {}),
158
+ ...(rule.claimValue ? { claimValue: rule.claimValue } : {}),
159
+ ...(rule.ownership ? { ownership: rule.ownership } : {}),
160
+ ...(rule.ownershipField ? { ownershipField: rule.ownershipField } : {})
161
+ }));
162
+ }
163
+
164
+ /**
165
+ * @param {import("./types.d.ts").ApiContract} contract
166
+ * @param {any} callback
167
+ * @returns {any}
168
+ */
169
+ export function callbackExpressionForField(contract, callback) {
170
+ const field = contract.requestContract?.fields?.find(
171
+ /** @param {any} item */ (item) => item.name === callback.targetField || item.sourceName === callback.targetField
172
+ );
173
+ const location = field?.transport?.location || contract.endpoint.requestPlacement;
174
+ const wireName = field?.transport?.wireName || callback.targetField;
175
+
176
+ if (location === "query") return `{$request.query.${wireName}}`;
177
+ if (location === "header") return `{$request.header.${wireName}}`;
178
+ return `{$request.body#/${wireName}}`;
179
+ }
180
+
181
+ /**
182
+ * @param {any} callback
183
+ * @param {any} componentNames
184
+ * @returns {any}
185
+ */
186
+ export function callbackRequestBodySchema(callback, componentNames) {
187
+ const payloadShapeId = callback.payload?.id || null;
188
+ if (!payloadShapeId) return { type: "object" };
189
+ const schemaName = componentNames.response.get(payloadShapeId);
190
+ return schemaName ? refSchema(schemaName) : { type: "object" };
191
+ }
192
+
193
+ /**
194
+ * @param {import("./types.d.ts").ApiContract} contract
195
+ * @param {any} componentNames
196
+ * @returns {any}
197
+ */
198
+ export function callbacksObjectForContract(contract, componentNames) {
199
+ if (!contract.endpoint.callbacks || contract.endpoint.callbacks.length === 0) return undefined;
200
+ const callbacks = /** @type {Record<string, any>} */ ({});
201
+ for (const callback of contract.endpoint.callbacks) {
202
+ callbacks[callback.event] = {
203
+ [callbackExpressionForField(contract, callback)]: {
204
+ [callback.method.toLowerCase()]: {
205
+ requestBody: {
206
+ required: true,
207
+ content: {
208
+ "application/json": {
209
+ schema: callbackRequestBodySchema(callback, componentNames)
210
+ }
211
+ }
212
+ },
213
+ responses: {
214
+ [String(callback.success || 202)]: { description: "Callback accepted" }
215
+ }
216
+ }
217
+ }
218
+ };
219
+ }
220
+ return callbacks;
221
+ }
222
+
223
+ /**
224
+ * @param {import("./types.d.ts").ApiContract} contract
225
+ * @param {any} componentNames
226
+ * @returns {any}
227
+ */
228
+ export function operationFromContract(contract, componentNames) {
229
+ const operation = /** @type {any} */ ({
230
+ operationId: contract.endpoint.operationId,
231
+ summary: contract.capability.name,
232
+ responses: {}
233
+ });
234
+
235
+ if (contract.capability.description) {
236
+ operation.description = contract.capability.description;
237
+ }
238
+
239
+ const authzExtension = authorizationExtension(contract);
240
+ if (authzExtension) {
241
+ operation["x-topogram-authorization"] = authzExtension;
242
+ }
243
+
244
+ if (contract.endpoint.auth && contract.endpoint.auth !== "none") {
245
+ operation.security = [{ bearerAuth: [] }];
246
+ }
247
+
248
+ const requestFields = contract.requestContract?.transport || { path: [], query: [], header: [], body: [] };
249
+ const parameters = [
250
+ ...requestFields.path.map(/** @param {import("./types.d.ts").ApiField} field */ (field) => openApiParameter(field, "path")),
251
+ ...requestFields.query.map(/** @param {import("./types.d.ts").ApiField} field */ (field) => openApiParameter(field, "query")),
252
+ ...requestFields.header.map(/** @param {import("./types.d.ts").ApiField} field */ (field) => openApiParameter(field, "header")),
253
+ ...(contract.endpoint.preconditions || []).map(/** @param {any} precondition */ (precondition) => ({
254
+ name: precondition.header,
255
+ in: "header",
256
+ required: precondition.required,
257
+ schema: { type: "string" }
258
+ })),
259
+ ...(contract.endpoint.idempotency || []).map(/** @param {any} rule */ (rule) => ({
260
+ name: rule.header,
261
+ in: "header",
262
+ required: rule.required,
263
+ schema: { type: "string" }
264
+ })),
265
+ ...(contract.endpoint.cache || []).map(/** @param {any} rule */ (rule) => ({
266
+ name: rule.requestHeader,
267
+ in: "header",
268
+ required: rule.required,
269
+ schema: { type: "string" }
270
+ }))
271
+ ];
272
+ if (parameters.length > 0) operation.parameters = parameters;
273
+
274
+ const callbacks = callbacksObjectForContract(contract, componentNames);
275
+ if (callbacks) {
276
+ operation.callbacks = callbacks;
277
+ }
278
+
279
+ if (requestFields.body.length > 0 && contract.requestContract) {
280
+ const requestSchemaName = componentNames.request.get(contract.capability.id);
281
+ operation.requestBody = {
282
+ required: requestFields.body.some(/** @param {import("./types.d.ts").ApiField} field */ (field) => field.required),
283
+ content: {
284
+ "application/json": {
285
+ schema: requestSchemaName ? refSchema(requestSchemaName) : { type: "object" }
286
+ }
287
+ }
288
+ };
289
+ }
290
+
291
+ if (contract.endpoint.download?.length > 0) {
292
+ const rule = contract.endpoint.download[0];
293
+ operation.responses[String(contract.endpoint.successStatus)] = {
294
+ description: "Success",
295
+ headers: successHeadersForContract(contract),
296
+ content: {
297
+ [rule.media || "application/octet-stream"]: {
298
+ schema: { type: "string", format: "binary" }
299
+ }
300
+ }
301
+ };
302
+ } else if (contract.responseContract) {
303
+ operation.responses[String(contract.endpoint.successStatus)] = {
304
+ description: contract.endpoint.async?.length > 0 ? "Accepted" : "Success",
305
+ headers: successHeadersForContract(contract),
306
+ content: {
307
+ "application/json": {
308
+ schema: responseSchemaForContract(contract, componentNames) || { type: "object" }
309
+ }
310
+ },
311
+ ...(contract.endpoint.async?.length > 0 ? { links: asyncLinksForContract(contract) } : {}),
312
+ ...(contract.endpoint.status?.length > 0 ? { links: statusLinksForContract(contract) } : {})
313
+ };
314
+ } else {
315
+ operation.responses[String(contract.endpoint.successStatus)] = {
316
+ description: contract.endpoint.async?.length > 0 ? "Accepted" : "Success",
317
+ headers: successHeadersForContract(contract)
318
+ };
319
+ }
320
+
321
+ for (const cacheRule of contract.endpoint.cache || []) {
322
+ const status = String(cacheRule.notModified || 304);
323
+ operation.responses[status] = {
324
+ description: "Not Modified",
325
+ headers: {
326
+ [cacheRule.responseHeader]: { schema: { type: "string" } }
327
+ }
328
+ };
329
+ }
330
+
331
+ for (const [status, errors] of groupedErrorsByStatus(contract.errors)) {
332
+ if (operation.responses[status]) continue;
333
+ if (errors.length === 1) {
334
+ operation.responses[status] = { $ref: `#/components/responses/${errorResponseComponentName(errors[0])}` };
335
+ continue;
336
+ }
337
+ operation.responses[status] = {
338
+ description: `Error (${status})`,
339
+ content: {
340
+ "application/json": {
341
+ schema: { oneOf: errors.map(/** @param {any} error */ (error) => refSchema(errorSchemaComponentName(error))) }
342
+ }
343
+ }
344
+ };
345
+ }
346
+
347
+ return operation;
348
+ }
349
+
350
+ /**
351
+ * @param {any} error
352
+ * @returns {any}
353
+ */
354
+ export function errorResponseComponentName(error) {
355
+ return `${componentNameFromErrorCode(error.code)}Response`;
356
+ }
357
+
358
+ /**
359
+ * @param {any} error
360
+ * @returns {any}
361
+ */
362
+ export function errorSchemaComponentName(error) {
363
+ return componentNameFromErrorCode(error.code);
364
+ }
365
+
366
+ /**
367
+ * @param {any} id
368
+ * @returns {any}
369
+ */
370
+ export function titleFromIdentifier(id) {
371
+ return id.replace(/[_-]+/g, " ").replace(/\b\w/g, /** @param {any} char */ (char) => char.toUpperCase());
372
+ }
373
+
374
+ /**
375
+ * @param {any} error
376
+ * @returns {any}
377
+ */
378
+ export function errorResponseDescription(error) {
379
+ return `${titleFromIdentifier(error.code)} (${error.status})`;
380
+ }
381
+
382
+ /**
383
+ * @param {any} errors
384
+ * @returns {any}
385
+ */
386
+ export function groupedErrorsByStatus(errors) {
387
+ const grouped = new Map();
388
+ for (const error of errors) {
389
+ const status = String(error.status);
390
+ if (!grouped.has(status)) grouped.set(status, []);
391
+ grouped.get(status).push(error);
392
+ }
393
+ return grouped;
394
+ }
395
+
396
+ /**
397
+ * @param {import("./types.d.ts").ApiGraph} graph
398
+ * @param {import("./types.d.ts").ApiOptions} options
399
+ * @returns {any}
400
+ */
401
+ export function generateOpenApi(graph, options = {}) {
402
+ const contractGraph = generateApiContractGraph(graph, options);
403
+ const contracts = options.capabilityId ? [contractGraph] : Object.values(contractGraph);
404
+ const byId = indexStatements(graph);
405
+ const document = /** @type {any} */ ({
406
+ openapi: "3.1.0",
407
+ info: {
408
+ title: "Topogram API",
409
+ version: "0.1.0"
410
+ },
411
+ paths: {},
412
+ components: {
413
+ schemas: {
414
+ ErrorResponse: {
415
+ type: "object",
416
+ additionalProperties: false,
417
+ required: ["code", "message"],
418
+ properties: {
419
+ code: { type: "string" },
420
+ message: { type: "string" },
421
+ details: { type: "object", additionalProperties: true }
422
+ }
423
+ }
424
+ },
425
+ responses: {},
426
+ securitySchemes: {
427
+ bearerAuth: { type: "http", scheme: "bearer" }
428
+ }
429
+ }
430
+ });
431
+
432
+ const componentNames = {
433
+ request: new Map(),
434
+ response: new Map(),
435
+ responseEnvelope: new Map()
436
+ };
437
+
438
+ for (const contract of contracts) {
439
+ if (contract.requestContract && contract.requestContract.transport.body.length > 0) {
440
+ const requestSchemaName = componentNameFromShape(contract.requestContract.shape.id, "Request");
441
+ componentNames.request.set(contract.capability.id, requestSchemaName);
442
+ document.components.schemas[requestSchemaName] = requestBodySchema(contract);
443
+ }
444
+
445
+ if (contract.responseContract) {
446
+ const responseSchemaName = componentNameFromShape(contract.responseContract.shape.id, "Response");
447
+ componentNames.response.set(contract.responseContract.shape.id, responseSchemaName);
448
+ if (!document.components.schemas[responseSchemaName]) {
449
+ document.components.schemas[responseSchemaName] = cloneSchema(
450
+ contract.responseContract.collection ? contract.responseContract.itemJsonSchema : contract.responseContract.jsonSchema
451
+ );
452
+ }
453
+
454
+ if (
455
+ (contract.responseContract.mode === "paged" || contract.responseContract.mode === "cursor") &&
456
+ (contract.responseContract.pagination || contract.responseContract.cursor)
457
+ ) {
458
+ const envelopeSuffix = contract.responseContract.mode === "cursor" ? "CursorPageResponse" : "PageResponse";
459
+ const envelopeSchemaName = componentNameFromShape(contract.responseContract.shape.id, envelopeSuffix);
460
+ componentNames.responseEnvelope.set(contract.capability.id, envelopeSchemaName);
461
+ if (!document.components.schemas[envelopeSchemaName]) {
462
+ if (contract.responseContract.mode === "paged") {
463
+ const pagination = contract.responseContract.pagination;
464
+ document.components.schemas[envelopeSchemaName] = {
465
+ type: "object",
466
+ additionalProperties: false,
467
+ required: [
468
+ pagination.itemsProperty,
469
+ pagination.pageProperty,
470
+ pagination.pageSizeProperty,
471
+ pagination.totalProperty
472
+ ],
473
+ properties: {
474
+ [pagination.itemsProperty]: { type: "array", items: refSchema(responseSchemaName) },
475
+ [pagination.pageProperty]: { type: "integer" },
476
+ [pagination.pageSizeProperty]: { type: "integer" },
477
+ [pagination.totalProperty]: { type: "integer" }
478
+ }
479
+ };
480
+ } else {
481
+ const cursor = contract.responseContract.cursor;
482
+ const includeTotal = contract.responseContract.total?.included === true;
483
+ document.components.schemas[envelopeSchemaName] = {
484
+ type: "object",
485
+ additionalProperties: false,
486
+ required: ["items", cursor.responseNext],
487
+ properties: {
488
+ items: { type: "array", items: refSchema(responseSchemaName) },
489
+ [cursor.responseNext]: { type: "string" },
490
+ ...(cursor.responsePrev ? { [cursor.responsePrev]: { type: "string" } } : {}),
491
+ ...(includeTotal ? { total: { type: "integer" } } : {})
492
+ }
493
+ };
494
+ }
495
+ }
496
+ }
497
+ }
498
+
499
+ for (const callback of contract.endpoint.callbacks || []) {
500
+ const payloadShapeId = callback.payload?.id || null;
501
+ if (!payloadShapeId) continue;
502
+ const payloadShape = byId.get(payloadShapeId);
503
+ if (!payloadShape) continue;
504
+ const responseSchemaName = componentNameFromShape(payloadShapeId, "Response");
505
+ componentNames.response.set(payloadShapeId, responseSchemaName);
506
+ if (!document.components.schemas[responseSchemaName]) {
507
+ document.components.schemas[responseSchemaName] = cloneSchema(generateShapeJsonSchema(payloadShape, byId));
508
+ }
509
+ }
510
+
511
+ for (const error of contract.errors) {
512
+ const schemaName = errorSchemaComponentName(error);
513
+ if (!document.components.schemas[schemaName]) {
514
+ document.components.schemas[schemaName] = {
515
+ allOf: [
516
+ refSchema("ErrorResponse"),
517
+ {
518
+ type: "object",
519
+ properties: {
520
+ code: { type: "string", const: error.code }
521
+ }
522
+ }
523
+ ],
524
+ title: titleFromIdentifier(error.code)
525
+ };
526
+ }
527
+
528
+ const componentName = errorResponseComponentName(error);
529
+ if (!document.components.responses[componentName]) {
530
+ document.components.responses[componentName] = {
531
+ description: errorResponseDescription(error),
532
+ content: {
533
+ "application/json": {
534
+ schema: refSchema(schemaName)
535
+ }
536
+ }
537
+ };
538
+ }
539
+ }
540
+ }
541
+
542
+ for (const contract of contracts) {
543
+ const pathKey = contract.endpoint.path.replace(/:([A-Za-z0-9_]+)/g, "{$1}");
544
+ if (!document.paths[pathKey]) {
545
+ document.paths[pathKey] = {};
546
+ }
547
+ document.paths[pathKey][contract.endpoint.method.toLowerCase()] = operationFromContract(contract, componentNames);
548
+ }
549
+
550
+ const needsSecurity = contracts.some(/** @param {import("./types.d.ts").ApiContract} contract */ (contract) => contract.endpoint.auth && contract.endpoint.auth !== "none");
551
+ if (!needsSecurity) {
552
+ delete document.components.securitySchemes;
553
+ if (Object.keys(document.components.schemas || {}).length === 0) delete document.components.schemas;
554
+ if (Object.keys(document.components.responses || {}).length === 0) delete document.components.responses;
555
+ if (Object.keys(document.components).length === 0) delete document.components;
556
+ }
557
+
558
+ return document;
559
+ }
@@ -0,0 +1,124 @@
1
+ export const JSON_SCHEMA_DRAFT = "https://json-schema.org/draft/2020-12/schema";
2
+
3
+ /**
4
+ * @param {import("./types.d.ts").ApiGraph} graph
5
+ * @returns {any}
6
+ */
7
+ export function indexStatements(graph) {
8
+ const byId = new Map();
9
+ for (const statement of graph.statements) {
10
+ byId.set(statement.id, statement);
11
+ }
12
+ return byId;
13
+ }
14
+
15
+ /**
16
+ * @param {any} typeName
17
+ * @param {any} byId
18
+ * @returns {any}
19
+ */
20
+ export function scalarSchema(typeName, byId) {
21
+ switch (typeName) {
22
+ case "string":
23
+ case "text":
24
+ return { type: "string" };
25
+ case "integer":
26
+ return { type: "integer" };
27
+ case "number":
28
+ return { type: "number" };
29
+ case "boolean":
30
+ return { type: "boolean" };
31
+ case "datetime":
32
+ return { type: "string", format: "date-time" };
33
+ case "uuid":
34
+ return { type: "string", format: "uuid" };
35
+ default: {
36
+ const target = byId.get(typeName);
37
+ if (target?.kind === "enum") {
38
+ return { type: "string", enum: target.values };
39
+ }
40
+ return { type: "string", "x-topogram-type": typeName };
41
+ }
42
+ }
43
+ }
44
+
45
+ /**
46
+ * @param {any} value
47
+ * @param {import("./types.d.ts").JsonSchema} schema
48
+ * @returns {any}
49
+ */
50
+ export function coerceDefaultValue(value, schema) {
51
+ if (value == null) {
52
+ return undefined;
53
+ }
54
+
55
+ if (schema.type === "integer") {
56
+ const parsed = Number.parseInt(value, 10);
57
+ return Number.isNaN(parsed) ? value : parsed;
58
+ }
59
+
60
+ if (schema.type === "number") {
61
+ const parsed = Number.parseFloat(value);
62
+ return Number.isNaN(parsed) ? value : parsed;
63
+ }
64
+
65
+ if (schema.type === "boolean") {
66
+ if (value === "true") return true;
67
+ if (value === "false") return false;
68
+ }
69
+
70
+ return value;
71
+ }
72
+
73
+ /**
74
+ * @param {import("./types.d.ts").ApiField} field
75
+ * @param {any} byId
76
+ * @returns {any}
77
+ */
78
+ export function schemaForField(field, byId) {
79
+ const schema = scalarSchema(field.fieldType, byId);
80
+ const defaultValue = coerceDefaultValue(field.defaultValue, schema);
81
+ return defaultValue === undefined ? schema : { ...schema, default: defaultValue };
82
+ }
83
+
84
+ /**
85
+ * @param {import("./types.d.ts").ApiShape} shape
86
+ * @param {any} byId
87
+ * @returns {any}
88
+ */
89
+ export function generateShapeJsonSchema(shape, byId) {
90
+ const fields = shape.projectedFields || shape.fields || [];
91
+ const properties = /** @type {Record<string, any>} */ ({});
92
+ const required = [];
93
+
94
+ for (const field of fields) {
95
+ properties[field.name] = schemaForField(field, byId);
96
+ if (field.requiredness === "required") {
97
+ required.push(field.name);
98
+ }
99
+ }
100
+
101
+ const schema = /** @type {any} */ ({
102
+ $schema: JSON_SCHEMA_DRAFT,
103
+ $id: `topogram:shape:${shape.id}`,
104
+ title: shape.name || shape.id,
105
+ description: shape.description || undefined,
106
+ type: "object",
107
+ properties,
108
+ additionalProperties: false
109
+ });
110
+
111
+ if (required.length > 0) {
112
+ schema.required = required;
113
+ }
114
+
115
+ return schema;
116
+ }
117
+
118
+ /**
119
+ * @param {any} value
120
+ * @returns {any}
121
+ */
122
+ export function cloneSchema(value) {
123
+ return JSON.parse(JSON.stringify(value));
124
+ }