@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
@@ -1,1196 +1,4 @@
1
- import { fieldSignature, symbolList } from "./shared.js";
1
+ // @ts-check
2
2
 
3
- const JSON_SCHEMA_DRAFT = "https://json-schema.org/draft/2020-12/schema";
4
-
5
- function indexStatements(graph) {
6
- const byId = new Map();
7
- for (const statement of graph.statements) {
8
- byId.set(statement.id, statement);
9
- }
10
- return byId;
11
- }
12
-
13
- function scalarSchema(typeName, byId) {
14
- switch (typeName) {
15
- case "string":
16
- case "text":
17
- return { type: "string" };
18
- case "integer":
19
- return { type: "integer" };
20
- case "number":
21
- return { type: "number" };
22
- case "boolean":
23
- return { type: "boolean" };
24
- case "datetime":
25
- return { type: "string", format: "date-time" };
26
- case "uuid":
27
- return { type: "string", format: "uuid" };
28
- default: {
29
- const target = byId.get(typeName);
30
- if (target?.kind === "enum") {
31
- return { type: "string", enum: target.values };
32
- }
33
- return { type: "string", "x-topogram-type": typeName };
34
- }
35
- }
36
- }
37
-
38
- function coerceDefaultValue(value, schema) {
39
- if (value == null) {
40
- return undefined;
41
- }
42
-
43
- if (schema.type === "integer") {
44
- const parsed = Number.parseInt(value, 10);
45
- return Number.isNaN(parsed) ? value : parsed;
46
- }
47
-
48
- if (schema.type === "number") {
49
- const parsed = Number.parseFloat(value);
50
- return Number.isNaN(parsed) ? value : parsed;
51
- }
52
-
53
- if (schema.type === "boolean") {
54
- if (value === "true") return true;
55
- if (value === "false") return false;
56
- }
57
-
58
- return value;
59
- }
60
-
61
- function schemaForField(field, byId) {
62
- const schema = scalarSchema(field.fieldType, byId);
63
- const defaultValue = coerceDefaultValue(field.defaultValue, schema);
64
- return defaultValue === undefined ? schema : { ...schema, default: defaultValue };
65
- }
66
-
67
- function generateShapeJsonSchema(shape, byId) {
68
- const fields = shape.projectedFields || shape.fields || [];
69
- const properties = {};
70
- const required = [];
71
-
72
- for (const field of fields) {
73
- properties[field.name] = schemaForField(field, byId);
74
- if (field.requiredness === "required") {
75
- required.push(field.name);
76
- }
77
- }
78
-
79
- const schema = {
80
- $schema: JSON_SCHEMA_DRAFT,
81
- $id: `topogram:shape:${shape.id}`,
82
- title: shape.name || shape.id,
83
- description: shape.description || undefined,
84
- type: "object",
85
- properties,
86
- additionalProperties: false
87
- };
88
-
89
- if (required.length > 0) {
90
- schema.required = required;
91
- }
92
-
93
- return schema;
94
- }
95
-
96
- function getCapability(graph, capabilityId) {
97
- const byId = indexStatements(graph);
98
- const capability = byId.get(capabilityId);
99
- if (!capability || capability.kind !== "capability") {
100
- throw new Error(`No capability found with id '${capabilityId}'`);
101
- }
102
- return capability;
103
- }
104
-
105
- function routeSegmentFromCapabilityId(id) {
106
- return id.replace(/^cap_/, "").replace(/_/g, "-");
107
- }
108
-
109
- function methodFromCapability(capability) {
110
- if (capability.creates.length > 0) return "POST";
111
- if (capability.updates.length > 0) return "PATCH";
112
- if (capability.deletes.length > 0) return "DELETE";
113
- return "GET";
114
- }
115
-
116
- function pathFromCapability(capability) {
117
- const resourceRef =
118
- capability.creates[0]?.target ||
119
- capability.updates[0]?.target ||
120
- capability.deletes[0]?.target ||
121
- capability.reads[0]?.target;
122
-
123
- const resourceSegment = resourceRef?.id ? resourceRef.id.replace(/^entity_/, "").replace(/_/g, "-") : "resource";
124
- const opSegment = routeSegmentFromCapabilityId(capability.id);
125
-
126
- if (capability.creates.length > 0) {
127
- return `/${resourceSegment}`;
128
- }
129
- if (capability.updates.length > 0 || capability.deletes.length > 0) {
130
- return `/${resourceSegment}/{id}`;
131
- }
132
- if (capability.id.startsWith("cap_list_")) {
133
- return `/${resourceSegment}`;
134
- }
135
- return `/${resourceSegment}/${opSegment}`;
136
- }
137
-
138
- function normalizeResponseMetadata(responseEntry) {
139
- return {
140
- mode: responseEntry?.mode || null,
141
- itemShapeId: responseEntry?.item?.id || null,
142
- ordering: responseEntry?.sort
143
- ? {
144
- field: responseEntry.sort.field,
145
- direction: responseEntry.sort.direction
146
- }
147
- : null,
148
- cursor: responseEntry?.cursor
149
- ? {
150
- requestAfter: responseEntry.cursor.requestAfter,
151
- responseNext: responseEntry.cursor.responseNext,
152
- responsePrev: responseEntry.cursor.responsePrev || null
153
- }
154
- : null,
155
- limit: responseEntry?.limit
156
- ? {
157
- field: responseEntry.limit.field,
158
- defaultValue: responseEntry.limit.defaultValue,
159
- maxValue: responseEntry.limit.maxValue
160
- }
161
- : null,
162
- total: responseEntry?.total
163
- ? {
164
- included: responseEntry.total.included
165
- }
166
- : null
167
- };
168
- }
169
-
170
- function apiMetadataForCapability(graph, capability) {
171
- const projections = graph.byKind.projection || [];
172
-
173
- for (const projection of projections) {
174
- const httpEntry = (projection.http || []).find((entry) => entry.capability?.id === capability.id);
175
- if (!httpEntry) continue;
176
-
177
- const responseEntry = (projection.httpResponses || []).find((entry) => entry.capability?.id === capability.id);
178
- return {
179
- projection: {
180
- id: projection.id,
181
- name: projection.name || projection.id
182
- },
183
- method: httpEntry.method || methodFromCapability(capability),
184
- path: httpEntry.path || pathFromCapability(capability),
185
- success: httpEntry.success || (capability.creates.length > 0 ? 201 : 200),
186
- auth: httpEntry.auth || "none",
187
- request: httpEntry.request || (capability.input.length > 0 ? "body" : "none"),
188
- errorMappings: (projection.httpErrors || [])
189
- .filter((entry) => entry.capability?.id === capability.id)
190
- .map((entry) => ({
191
- code: entry.code,
192
- status: entry.status
193
- })),
194
- fieldBindings: (projection.httpFields || []).filter((entry) => entry.capability?.id === capability.id),
195
- preconditions: (projection.httpPreconditions || [])
196
- .filter((entry) => entry.capability?.id === capability.id)
197
- .map((entry) => ({
198
- header: entry.header,
199
- required: entry.required,
200
- error: entry.error,
201
- source: entry.source,
202
- code: entry.code
203
- })),
204
- idempotency: (projection.httpIdempotency || [])
205
- .filter((entry) => entry.capability?.id === capability.id)
206
- .map((entry) => ({
207
- header: entry.header,
208
- required: entry.required,
209
- error: entry.error,
210
- code: entry.code
211
- })),
212
- cache: (projection.httpCache || [])
213
- .filter((entry) => entry.capability?.id === capability.id)
214
- .map((entry) => ({
215
- responseHeader: entry.responseHeader,
216
- requestHeader: entry.requestHeader,
217
- required: entry.required,
218
- notModified: entry.notModified,
219
- source: entry.source,
220
- code: entry.code
221
- })),
222
- delete: (projection.httpDelete || [])
223
- .filter((entry) => entry.capability?.id === capability.id)
224
- .map((entry) => ({
225
- mode: entry.mode,
226
- field: entry.field,
227
- value: entry.value,
228
- response: entry.response
229
- })),
230
- async: (projection.httpAsync || [])
231
- .filter((entry) => entry.capability?.id === capability.id)
232
- .map((entry) => ({
233
- mode: entry.mode,
234
- accepted: entry.accepted,
235
- locationHeader: entry.locationHeader,
236
- retryAfterHeader: entry.retryAfterHeader,
237
- statusPath: entry.statusPath,
238
- statusCapability: entry.statusCapability,
239
- job: entry.job
240
- })),
241
- status: (projection.httpStatus || [])
242
- .filter((entry) => entry.capability?.id === capability.id)
243
- .map((entry) => ({
244
- asyncFor: entry.asyncFor,
245
- stateField: entry.stateField,
246
- completed: entry.completed,
247
- failed: entry.failed,
248
- expired: entry.expired,
249
- downloadCapability: entry.downloadCapability,
250
- downloadField: entry.downloadField,
251
- errorField: entry.errorField
252
- })),
253
- download: (projection.httpDownload || [])
254
- .filter((entry) => entry.capability?.id === capability.id)
255
- .map((entry) => ({
256
- asyncFor: entry.asyncFor,
257
- media: entry.media,
258
- filename: entry.filename,
259
- disposition: entry.disposition
260
- })),
261
- authz: (projection.httpAuthz || [])
262
- .filter((entry) => entry.capability?.id === capability.id)
263
- .map((entry) => ({
264
- role: entry.role,
265
- permission: entry.permission,
266
- claim: entry.claim,
267
- claimValue: entry.claimValue,
268
- ownership: entry.ownership,
269
- ownershipField: entry.ownershipField
270
- })),
271
- callbacks: (projection.httpCallbacks || [])
272
- .filter((entry) => entry.capability?.id === capability.id)
273
- .map((entry) => ({
274
- event: entry.event,
275
- targetField: entry.targetField,
276
- method: entry.method,
277
- payload: entry.payload,
278
- success: entry.success
279
- })),
280
- response: normalizeResponseMetadata(responseEntry)
281
- };
282
- }
283
-
284
- return {
285
- projection: null,
286
- method: methodFromCapability(capability),
287
- path: pathFromCapability(capability),
288
- success: capability.creates.length > 0 ? 201 : 200,
289
- auth: "none",
290
- request: capability.input.length > 0 ? "body" : "none",
291
- errorMappings: [],
292
- fieldBindings: [],
293
- preconditions: [],
294
- idempotency: [],
295
- cache: [],
296
- delete: [],
297
- async: [],
298
- status: [],
299
- download: [],
300
- authz: [],
301
- callbacks: [],
302
- response: normalizeResponseMetadata(null)
303
- };
304
- }
305
-
306
- function fieldTransportBindings(contract, apiMetadata, direction) {
307
- const bindings = (apiMetadata.fieldBindings || []).filter((binding) => binding.direction === direction);
308
- const byField = new Map(bindings.map((binding) => [binding.field, binding]));
309
- const inferredBindings = new Map();
310
-
311
- if (direction === "input" && apiMetadata.response?.cursor?.requestAfter) {
312
- inferredBindings.set(apiMetadata.response.cursor.requestAfter, { location: "query", wireName: "after" });
313
- }
314
- if (direction === "input" && apiMetadata.response?.limit?.field) {
315
- inferredBindings.set(apiMetadata.response.limit.field, { location: "query", wireName: "limit" });
316
- }
317
-
318
- return contract.fields.map((field) => {
319
- const binding = byField.get(field.name) || byField.get(field.sourceName) || inferredBindings.get(field.name);
320
- return {
321
- ...field,
322
- transport: {
323
- location: binding?.location || (direction === "input" ? apiMetadata.request : "body"),
324
- wireName: binding?.wireName || field.name
325
- }
326
- };
327
- });
328
- }
329
-
330
- function splitFieldsByLocation(fields) {
331
- return {
332
- path: fields.filter((field) => field.transport.location === "path"),
333
- query: fields.filter((field) => field.transport.location === "query"),
334
- header: fields.filter((field) => field.transport.location === "header"),
335
- body: fields.filter((field) => field.transport.location === "body")
336
- };
337
- }
338
-
339
- function fieldContract(field, byId) {
340
- return {
341
- name: field.name,
342
- sourceName: field.sourceName ?? field.name,
343
- required: field.requiredness === "required",
344
- schema: schemaForField(field, byId)
345
- };
346
- }
347
-
348
- function contractFromShape(shape, byId, direction) {
349
- const fields = (shape.projectedFields || shape.fields || []).map((field) => fieldContract(field, byId));
350
- return {
351
- type: direction === "request" ? "api_request_contract" : "api_response_contract",
352
- shape: {
353
- id: shape.id,
354
- name: shape.name || shape.id
355
- },
356
- fields,
357
- required: fields.filter((field) => field.required).map((field) => field.name),
358
- jsonSchema: generateShapeJsonSchema(shape, byId)
359
- };
360
- }
361
-
362
- function isCollectionCapability(capability, apiMetadata) {
363
- if (["collection", "paged", "cursor"].includes(apiMetadata?.response?.mode)) return true;
364
- if (apiMetadata?.response?.mode === "item") return false;
365
- if ((apiMetadata?.method || "").toUpperCase() !== "GET") return false;
366
- return capability.id.startsWith("cap_list_");
367
- }
368
-
369
- function policyConstraintsForCapability(graph, capability) {
370
- const rules = graph.byKind.rule || [];
371
- return rules
372
- .filter((rule) => rule.appliesTo.some((target) => target.id === capability.id))
373
- .map((rule) => ({
374
- type: "api_policy_constraint",
375
- rule: {
376
- id: rule.id,
377
- name: rule.name || rule.id
378
- },
379
- requirement: rule.requirementNode || null,
380
- condition: rule.conditionNode || null,
381
- severity: rule.severity
382
- }));
383
- }
384
-
385
- function apiErrorCasesForCapability(capability, policyConstraints, apiMetadata) {
386
- const errors = [];
387
- const overrideMap = new Map((apiMetadata.errorMappings || []).map((mapping) => [mapping.code, mapping.status]));
388
-
389
- for (const policy of policyConstraints) {
390
- errors.push({
391
- type: "api_error_case",
392
- code: policy.rule.id,
393
- status: overrideMap.get(policy.rule.id) || (policy.severity === "error" ? 400 : 422),
394
- source: "policy"
395
- });
396
- }
397
-
398
- if (capability.input.length > 0) {
399
- errors.push({
400
- type: "api_error_case",
401
- code: `${capability.id}_invalid_request`,
402
- status: overrideMap.get(`${capability.id}_invalid_request`) || 400,
403
- source: "request_contract"
404
- });
405
- }
406
-
407
- if (apiMetadata.response?.mode === "cursor") {
408
- errors.push({
409
- type: "api_error_case",
410
- code: `${capability.id}_invalid_cursor`,
411
- status: overrideMap.get(`${capability.id}_invalid_cursor`) || 400,
412
- source: "cursor_contract"
413
- });
414
- errors.push({
415
- type: "api_error_case",
416
- code: `${capability.id}_invalid_limit`,
417
- status: overrideMap.get(`${capability.id}_invalid_limit`) || 400,
418
- source: "cursor_contract"
419
- });
420
- }
421
-
422
- for (const precondition of apiMetadata.preconditions || []) {
423
- errors.push({
424
- type: "api_error_case",
425
- code: precondition.code,
426
- status: precondition.error || 412,
427
- source: "precondition"
428
- });
429
- }
430
-
431
- for (const idempotency of apiMetadata.idempotency || []) {
432
- errors.push({
433
- type: "api_error_case",
434
- code: idempotency.code,
435
- status: idempotency.error || 409,
436
- source: "idempotency"
437
- });
438
- }
439
-
440
- const seenCodes = new Set(errors.map((error) => error.code));
441
- for (const mapping of apiMetadata.errorMappings || []) {
442
- if (seenCodes.has(mapping.code)) continue;
443
- errors.push({
444
- type: "api_error_case",
445
- code: mapping.code,
446
- status: mapping.status,
447
- source: "projection_mapping"
448
- });
449
- }
450
-
451
- return errors;
452
- }
453
-
454
- function cloneSchema(value) {
455
- return JSON.parse(JSON.stringify(value));
456
- }
457
-
458
- function responseContractForCapability(shape, byId, capability, apiMetadata) {
459
- const baseContract = contractFromShape(shape, byId, "response");
460
- const responseMode = apiMetadata?.response?.mode || (isCollectionCapability(capability, apiMetadata) ? "collection" : "item");
461
-
462
- if (responseMode === "item") {
463
- return { ...baseContract, mode: "item", collection: false, itemJsonSchema: null, pagination: null };
464
- }
465
-
466
- if (responseMode === "paged") {
467
- return {
468
- ...baseContract,
469
- mode: "paged",
470
- collection: true,
471
- itemJsonSchema: baseContract.jsonSchema,
472
- pagination: {
473
- itemsProperty: "items",
474
- pageProperty: "page",
475
- pageSizeProperty: "page_size",
476
- totalProperty: "total"
477
- },
478
- jsonSchema: {
479
- type: "object",
480
- additionalProperties: false,
481
- required: ["items", "page", "page_size", "total"],
482
- properties: {
483
- items: { type: "array", items: cloneSchema(baseContract.jsonSchema) },
484
- page: { type: "integer" },
485
- page_size: { type: "integer" },
486
- total: { type: "integer" }
487
- }
488
- }
489
- };
490
- }
491
-
492
- if (responseMode === "cursor") {
493
- const totalIncluded = apiMetadata.response.total?.included === true;
494
- return {
495
- ...baseContract,
496
- mode: "cursor",
497
- collection: true,
498
- itemJsonSchema: baseContract.jsonSchema,
499
- pagination: null,
500
- itemShape: { id: shape.id, name: shape.name || shape.id },
501
- ordering: apiMetadata.response.ordering,
502
- cursor: apiMetadata.response.cursor,
503
- limit: apiMetadata.response.limit,
504
- total: apiMetadata.response.total,
505
- jsonSchema: {
506
- type: "object",
507
- additionalProperties: false,
508
- required: ["items", apiMetadata.response.cursor?.responseNext || "next_cursor"],
509
- properties: {
510
- items: { type: "array", items: cloneSchema(baseContract.jsonSchema) },
511
- [apiMetadata.response.cursor?.responseNext || "next_cursor"]: { type: "string" },
512
- ...(apiMetadata.response.cursor?.responsePrev
513
- ? { [apiMetadata.response.cursor.responsePrev]: { type: "string" } }
514
- : {}),
515
- ...(totalIncluded ? { total: { type: "integer" } } : {})
516
- }
517
- }
518
- };
519
- }
520
-
521
- return {
522
- ...baseContract,
523
- mode: "collection",
524
- collection: true,
525
- itemJsonSchema: baseContract.jsonSchema,
526
- pagination: null,
527
- jsonSchema: { type: "array", items: cloneSchema(baseContract.jsonSchema) }
528
- };
529
- }
530
-
531
- function buildApiContractForCapability(graph, capability, byId) {
532
- const inputShape = capability.input[0]?.id ? byId.get(capability.input[0].id) : null;
533
- const policyConstraints = policyConstraintsForCapability(graph, capability);
534
- const apiMetadata = apiMetadataForCapability(graph, capability);
535
- const outputShapeId = apiMetadata.response.itemShapeId || capability.output[0]?.id || null;
536
- const outputShape = outputShapeId ? byId.get(outputShapeId) : null;
537
- const requestContract = inputShape ? contractFromShape(inputShape, byId, "request") : null;
538
- const responseContract = outputShape ? responseContractForCapability(outputShape, byId, capability, apiMetadata) : null;
539
- const requestFields = requestContract ? fieldTransportBindings(requestContract, apiMetadata, "input") : [];
540
- const responseFields = responseContract ? fieldTransportBindings(responseContract, apiMetadata, "output") : [];
541
-
542
- return {
543
- type: "api_contract_graph",
544
- capability: {
545
- id: capability.id,
546
- name: capability.name || capability.id,
547
- description: capability.description || null
548
- },
549
- endpoint: {
550
- type: "api_endpoint",
551
- operationId: capability.id,
552
- method: apiMetadata.method,
553
- path: apiMetadata.path,
554
- successStatus: apiMetadata.success,
555
- auth: apiMetadata.auth,
556
- requestPlacement: apiMetadata.request,
557
- projection: apiMetadata.projection,
558
- preconditions: apiMetadata.preconditions || [],
559
- idempotency: apiMetadata.idempotency || [],
560
- cache: apiMetadata.cache || [],
561
- delete: apiMetadata.delete || [],
562
- async: apiMetadata.async || [],
563
- status: apiMetadata.status || [],
564
- download: apiMetadata.download || [],
565
- authz: apiMetadata.authz || [],
566
- callbacks: apiMetadata.callbacks || [],
567
- actors: capability.actors.map((actor, index) => ({
568
- type: "api_actor",
569
- id: actor.id,
570
- kind: actor.target?.kind || null,
571
- order: index
572
- }))
573
- },
574
- requestContract: requestContract
575
- ? { ...requestContract, fields: requestFields, transport: splitFieldsByLocation(requestFields) }
576
- : null,
577
- responseContract: responseContract
578
- ? { ...responseContract, fields: responseFields, transport: splitFieldsByLocation(responseFields) }
579
- : null,
580
- policy: policyConstraints,
581
- errors: apiErrorCasesForCapability(capability, policyConstraints, apiMetadata)
582
- };
583
- }
584
-
585
- export function generateApiContractGraph(graph, options = {}) {
586
- const byId = indexStatements(graph);
587
- const capabilities = graph.byKind.capability || [];
588
-
589
- if (options.capabilityId) {
590
- return buildApiContractForCapability(graph, getCapability(graph, options.capabilityId), byId);
591
- }
592
-
593
- const output = {};
594
- for (const capability of capabilities) {
595
- output[capability.id] = buildApiContractForCapability(graph, capability, byId);
596
- }
597
- return output;
598
- }
599
-
600
- export function generateApiContractDebug(graph, options = {}) {
601
- const capabilities = options.capabilityId ? [getCapability(graph, options.capabilityId)] : graph.byKind.capability || [];
602
- const byId = indexStatements(graph);
603
- const lines = [];
604
-
605
- lines.push("# API Contract Debug");
606
- lines.push("");
607
- lines.push(`Generated from \`${graph.root}\``);
608
- lines.push("");
609
-
610
- for (const capability of capabilities) {
611
- const contract = buildApiContractForCapability(graph, capability, byId);
612
- lines.push(`## \`${capability.id}\` - ${capability.name || capability.id}`);
613
- lines.push("");
614
- if (capability.description) {
615
- lines.push(capability.description);
616
- lines.push("");
617
- }
618
- lines.push(`Endpoint: \`${contract.endpoint.method} ${contract.endpoint.path}\``);
619
- lines.push(`Success: \`${contract.endpoint.successStatus}\``);
620
- lines.push(`Auth: \`${contract.endpoint.auth}\``);
621
- lines.push(`Request placement: \`${contract.endpoint.requestPlacement}\``);
622
- if (contract.endpoint.projection?.id) {
623
- lines.push(`Projection: \`${contract.endpoint.projection.id}\``);
624
- }
625
- if (contract.endpoint.preconditions?.length > 0) {
626
- lines.push(`Preconditions: ${contract.endpoint.preconditions.map((precondition) => `\`${precondition.header}\``).join(", ")}`);
627
- }
628
- if (contract.endpoint.idempotency?.length > 0) {
629
- lines.push(`Idempotency: ${contract.endpoint.idempotency.map((rule) => `\`${rule.header}\``).join(", ")}`);
630
- }
631
- if (contract.endpoint.cache?.length > 0) {
632
- lines.push(`Cache: ${contract.endpoint.cache.map((rule) => `\`${rule.responseHeader}\` via \`${rule.requestHeader}\` -> ${rule.notModified}`).join(", ")}`);
633
- }
634
- if (contract.endpoint.delete?.length > 0) {
635
- lines.push(`Delete: ${contract.endpoint.delete.map((rule) => `\`${rule.mode}\`${rule.field ? ` via \`${rule.field}=${rule.value}\`` : ""} response \`${rule.response}\``).join(", ")}`);
636
- }
637
- if (contract.endpoint.async?.length > 0) {
638
- lines.push(`Async: ${contract.endpoint.async.map((rule) => `\`${rule.mode}\` accepted ${rule.accepted} status \`${rule.statusPath}\`${rule.statusCapability?.id ? ` via \`${rule.statusCapability.id}\`` : ""}`).join(", ")}`);
639
- }
640
- if (contract.endpoint.status?.length > 0) {
641
- lines.push(`Status: ${contract.endpoint.status.map((rule) => `state \`${rule.stateField}\` complete \`${rule.completed}\` fail \`${rule.failed}\`${rule.downloadCapability?.id ? ` download \`${rule.downloadCapability.id}\`` : ""}`).join(", ")}`);
642
- }
643
- if (contract.endpoint.download?.length > 0) {
644
- lines.push(`Download: ${contract.endpoint.download.map((rule) => `\`${rule.media}\` ${rule.disposition}${rule.filename ? ` filename \`${rule.filename}\`` : ""}`).join(", ")}`);
645
- }
646
- if (contract.endpoint.authz?.length > 0) {
647
- lines.push(`Authorization: ${contract.endpoint.authz.map((rule) => [
648
- rule.role ? `role \`${rule.role}\`` : null,
649
- rule.permission ? `permission \`${rule.permission}\`` : null,
650
- rule.claim ? `claim \`${rule.claim}\`${rule.claimValue ? ` = \`${rule.claimValue}\`` : ""}` : null,
651
- rule.ownership ? `ownership \`${rule.ownership}\`` : null
652
- ].filter(Boolean).join(", ")).join(" | ")}`);
653
- }
654
- if (contract.endpoint.callbacks?.length > 0) {
655
- lines.push(`Callbacks: ${contract.endpoint.callbacks.map((rule) => `\`${rule.event}\` -> \`${rule.method}\` via \`${rule.targetField}\``).join(", ")}`);
656
- }
657
- lines.push(`Actors: ${symbolList(capability.actors)}`);
658
- lines.push("");
659
-
660
- lines.push("Request contract:");
661
- if (!contract.requestContract) {
662
- lines.push("- _none_");
663
- } else {
664
- lines.push(`- shape: \`${contract.requestContract.shape.id}\``);
665
- for (const field of contract.requestContract.fields) {
666
- lines.push(`- ${fieldSignature({
667
- name: field.transport?.wireName || field.name,
668
- sourceName: field.sourceName,
669
- fieldType: field.schema["x-topogram-type"] || field.schema.type || "unknown",
670
- requiredness: field.required ? "required" : "optional",
671
- defaultValue: field.schema.default ?? null
672
- })} in \`${field.transport?.location}\``);
673
- }
674
- }
675
- lines.push("");
676
-
677
- lines.push("Response contract:");
678
- if (!contract.responseContract) {
679
- lines.push("- _none_");
680
- } else {
681
- lines.push(`- shape: \`${contract.responseContract.shape.id}\``);
682
- lines.push(`- mode: \`${contract.responseContract.mode}\``);
683
- if (contract.responseContract.pagination) {
684
- lines.push(`- envelope: \`${contract.responseContract.pagination.itemsProperty}\`, \`${contract.responseContract.pagination.pageProperty}\`, \`${contract.responseContract.pagination.pageSizeProperty}\`, \`${contract.responseContract.pagination.totalProperty}\``);
685
- }
686
- if (contract.responseContract.cursor) {
687
- lines.push(`- cursor: request \`${contract.responseContract.cursor.requestAfter}\`, next \`${contract.responseContract.cursor.responseNext}\`${contract.responseContract.cursor.responsePrev ? `, prev \`${contract.responseContract.cursor.responsePrev}\`` : ""}`);
688
- }
689
- if (contract.responseContract.limit) {
690
- lines.push(`- limit: field \`${contract.responseContract.limit.field}\`, default ${contract.responseContract.limit.defaultValue}, max ${contract.responseContract.limit.maxValue}`);
691
- }
692
- if (contract.responseContract.ordering) {
693
- lines.push(`- sort: \`${contract.responseContract.ordering.field} ${contract.responseContract.ordering.direction}\``);
694
- }
695
- if (contract.responseContract.total) {
696
- lines.push(`- total included: \`${contract.responseContract.total.included}\``);
697
- }
698
- for (const field of contract.responseContract.fields) {
699
- lines.push(`- ${fieldSignature({
700
- name: field.transport?.wireName || field.name,
701
- sourceName: field.sourceName,
702
- fieldType: field.schema["x-topogram-type"] || field.schema.type || "unknown",
703
- requiredness: field.required ? "required" : "optional",
704
- defaultValue: field.schema.default ?? null
705
- })} in \`${field.transport?.location}\``);
706
- }
707
- }
708
- lines.push("");
709
-
710
- lines.push("Policy constraints:");
711
- if (contract.policy.length === 0) {
712
- lines.push("- _none_");
713
- } else {
714
- for (const policy of contract.policy) {
715
- lines.push(`- \`${policy.rule.id}\` (${policy.severity})`);
716
- }
717
- }
718
- lines.push("");
719
-
720
- lines.push("Error cases:");
721
- for (const error of contract.errors) {
722
- lines.push(`- \`${error.code}\` -> ${error.status}`);
723
- }
724
- lines.push("");
725
- }
726
-
727
- return `${lines.join("\n").trimEnd()}\n`;
728
- }
729
-
730
- function componentNameFromShape(shapeId, suffix) {
731
- const base = shapeId.split("_").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
732
- return `${base}${suffix}`;
733
- }
734
-
735
- function componentNameFromErrorCode(code, suffix = "Error") {
736
- const base = code
737
- .split(/[^A-Za-z0-9]+/)
738
- .filter(Boolean)
739
- .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
740
- .join("");
741
- return `${base}${suffix}`;
742
- }
743
-
744
- function refSchema(name) {
745
- return { $ref: `#/components/schemas/${name}` };
746
- }
747
-
748
- function openApiParameter(field, location) {
749
- return {
750
- name: field.transport?.wireName || field.name,
751
- in: location,
752
- required: location === "path" ? true : field.required,
753
- schema: cloneSchema(field.schema)
754
- };
755
- }
756
-
757
- function requestBodySchema(contract) {
758
- const requestFields = contract.requestContract?.transport?.body || [];
759
- const schema = { type: "object", properties: {}, additionalProperties: false };
760
- const required = [];
761
-
762
- for (const field of requestFields) {
763
- schema.properties[field.transport?.wireName || field.name] = cloneSchema(field.schema);
764
- if (field.required) {
765
- required.push(field.transport?.wireName || field.name);
766
- }
767
- }
768
-
769
- if (required.length > 0) {
770
- schema.required = required;
771
- }
772
- return schema;
773
- }
774
-
775
- function responseSchemaForContract(contract, componentNames) {
776
- if (!contract.responseContract) return null;
777
-
778
- if (contract.responseContract.mode === "paged" || contract.responseContract.mode === "cursor") {
779
- const envelopeSchemaName = componentNames.responseEnvelope.get(contract.capability.id);
780
- if (envelopeSchemaName) return refSchema(envelopeSchemaName);
781
- }
782
-
783
- const responseSchemaName = componentNames.response.get(contract.responseContract.shape.id);
784
- if (!responseSchemaName) return cloneSchema(contract.responseContract.jsonSchema);
785
-
786
- if (contract.responseContract.collection) {
787
- return { type: "array", items: refSchema(responseSchemaName) };
788
- }
789
-
790
- return refSchema(responseSchemaName);
791
- }
792
-
793
- function successHeadersForContract(contract) {
794
- const headerEntries = [
795
- ...(contract.endpoint.cache || []).map((rule) => [rule.responseHeader, { schema: { type: "string" } }]),
796
- ...(contract.endpoint.async || []).flatMap((rule) => [
797
- [rule.locationHeader, { schema: { type: "string" } }],
798
- [rule.retryAfterHeader, { schema: { type: "integer" } }]
799
- ]),
800
- ...(contract.endpoint.download || []).flatMap((rule) =>
801
- rule.disposition ? [["Content-Disposition", { schema: { type: "string" } }]] : []
802
- )
803
- ].filter(([name]) => Boolean(name));
804
-
805
- return headerEntries.length > 0 ? Object.fromEntries(headerEntries) : undefined;
806
- }
807
-
808
- function asyncLinksForContract(contract) {
809
- const links = {};
810
- for (const rule of contract.endpoint.async || []) {
811
- if (!rule.statusCapability?.id) continue;
812
- links[`${rule.statusCapability.id}Status`] = {
813
- operationId: rule.statusCapability.id,
814
- parameters: { job_id: "$response.body#/job_id" },
815
- description: `Follow ${rule.statusCapability.id} to monitor the async job`
816
- };
817
- }
818
- return Object.keys(links).length > 0 ? links : undefined;
819
- }
820
-
821
- function statusLinksForContract(contract) {
822
- const links = {};
823
- for (const rule of contract.endpoint.status || []) {
824
- if (!rule.downloadCapability?.id) continue;
825
- links[`${rule.downloadCapability.id}Download`] = {
826
- operationId: rule.downloadCapability.id,
827
- parameters: { job_id: "$response.body#/job_id" },
828
- description: `Use ${rule.downloadCapability.id} when the job is complete`
829
- };
830
- }
831
- return Object.keys(links).length > 0 ? links : undefined;
832
- }
833
-
834
- function authorizationExtension(contract) {
835
- if (!contract.endpoint.authz || contract.endpoint.authz.length === 0) return undefined;
836
- return contract.endpoint.authz.map((rule) => ({
837
- ...(rule.role ? { role: rule.role } : {}),
838
- ...(rule.permission ? { permission: rule.permission } : {}),
839
- ...(rule.claim ? { claim: rule.claim } : {}),
840
- ...(rule.claimValue ? { claimValue: rule.claimValue } : {}),
841
- ...(rule.ownership ? { ownership: rule.ownership } : {}),
842
- ...(rule.ownershipField ? { ownershipField: rule.ownershipField } : {})
843
- }));
844
- }
845
-
846
- function callbackExpressionForField(contract, callback) {
847
- const field = contract.requestContract?.fields?.find(
848
- (item) => item.name === callback.targetField || item.sourceName === callback.targetField
849
- );
850
- const location = field?.transport?.location || contract.endpoint.requestPlacement;
851
- const wireName = field?.transport?.wireName || callback.targetField;
852
-
853
- if (location === "query") return `{$request.query.${wireName}}`;
854
- if (location === "header") return `{$request.header.${wireName}}`;
855
- return `{$request.body#/${wireName}}`;
856
- }
857
-
858
- function callbackRequestBodySchema(callback, componentNames) {
859
- const payloadShapeId = callback.payload?.id || null;
860
- if (!payloadShapeId) return { type: "object" };
861
- const schemaName = componentNames.response.get(payloadShapeId);
862
- return schemaName ? refSchema(schemaName) : { type: "object" };
863
- }
864
-
865
- function callbacksObjectForContract(contract, componentNames) {
866
- if (!contract.endpoint.callbacks || contract.endpoint.callbacks.length === 0) return undefined;
867
- const callbacks = {};
868
- for (const callback of contract.endpoint.callbacks) {
869
- callbacks[callback.event] = {
870
- [callbackExpressionForField(contract, callback)]: {
871
- [callback.method.toLowerCase()]: {
872
- requestBody: {
873
- required: true,
874
- content: {
875
- "application/json": {
876
- schema: callbackRequestBodySchema(callback, componentNames)
877
- }
878
- }
879
- },
880
- responses: {
881
- [String(callback.success || 202)]: { description: "Callback accepted" }
882
- }
883
- }
884
- }
885
- };
886
- }
887
- return callbacks;
888
- }
889
-
890
- function operationFromContract(contract, componentNames) {
891
- const operation = {
892
- operationId: contract.endpoint.operationId,
893
- summary: contract.capability.name,
894
- responses: {}
895
- };
896
-
897
- if (contract.capability.description) {
898
- operation.description = contract.capability.description;
899
- }
900
-
901
- const authzExtension = authorizationExtension(contract);
902
- if (authzExtension) {
903
- operation["x-topogram-authorization"] = authzExtension;
904
- }
905
-
906
- if (contract.endpoint.auth && contract.endpoint.auth !== "none") {
907
- operation.security = [{ bearerAuth: [] }];
908
- }
909
-
910
- const requestFields = contract.requestContract?.transport || { path: [], query: [], header: [], body: [] };
911
- const parameters = [
912
- ...requestFields.path.map((field) => openApiParameter(field, "path")),
913
- ...requestFields.query.map((field) => openApiParameter(field, "query")),
914
- ...requestFields.header.map((field) => openApiParameter(field, "header")),
915
- ...(contract.endpoint.preconditions || []).map((precondition) => ({
916
- name: precondition.header,
917
- in: "header",
918
- required: precondition.required,
919
- schema: { type: "string" }
920
- })),
921
- ...(contract.endpoint.idempotency || []).map((rule) => ({
922
- name: rule.header,
923
- in: "header",
924
- required: rule.required,
925
- schema: { type: "string" }
926
- })),
927
- ...(contract.endpoint.cache || []).map((rule) => ({
928
- name: rule.requestHeader,
929
- in: "header",
930
- required: rule.required,
931
- schema: { type: "string" }
932
- }))
933
- ];
934
- if (parameters.length > 0) operation.parameters = parameters;
935
-
936
- const callbacks = callbacksObjectForContract(contract, componentNames);
937
- if (callbacks) {
938
- operation.callbacks = callbacks;
939
- }
940
-
941
- if (requestFields.body.length > 0 && contract.requestContract) {
942
- const requestSchemaName = componentNames.request.get(contract.capability.id);
943
- operation.requestBody = {
944
- required: requestFields.body.some((field) => field.required),
945
- content: {
946
- "application/json": {
947
- schema: requestSchemaName ? refSchema(requestSchemaName) : { type: "object" }
948
- }
949
- }
950
- };
951
- }
952
-
953
- if (contract.endpoint.download?.length > 0) {
954
- const rule = contract.endpoint.download[0];
955
- operation.responses[String(contract.endpoint.successStatus)] = {
956
- description: "Success",
957
- headers: successHeadersForContract(contract),
958
- content: {
959
- [rule.media || "application/octet-stream"]: {
960
- schema: { type: "string", format: "binary" }
961
- }
962
- }
963
- };
964
- } else if (contract.responseContract) {
965
- operation.responses[String(contract.endpoint.successStatus)] = {
966
- description: contract.endpoint.async?.length > 0 ? "Accepted" : "Success",
967
- headers: successHeadersForContract(contract),
968
- content: {
969
- "application/json": {
970
- schema: responseSchemaForContract(contract, componentNames) || { type: "object" }
971
- }
972
- },
973
- ...(contract.endpoint.async?.length > 0 ? { links: asyncLinksForContract(contract) } : {}),
974
- ...(contract.endpoint.status?.length > 0 ? { links: statusLinksForContract(contract) } : {})
975
- };
976
- } else {
977
- operation.responses[String(contract.endpoint.successStatus)] = {
978
- description: contract.endpoint.async?.length > 0 ? "Accepted" : "Success",
979
- headers: successHeadersForContract(contract)
980
- };
981
- }
982
-
983
- for (const cacheRule of contract.endpoint.cache || []) {
984
- const status = String(cacheRule.notModified || 304);
985
- operation.responses[status] = {
986
- description: "Not Modified",
987
- headers: {
988
- [cacheRule.responseHeader]: { schema: { type: "string" } }
989
- }
990
- };
991
- }
992
-
993
- for (const [status, errors] of groupedErrorsByStatus(contract.errors)) {
994
- if (operation.responses[status]) continue;
995
- if (errors.length === 1) {
996
- operation.responses[status] = { $ref: `#/components/responses/${errorResponseComponentName(errors[0])}` };
997
- continue;
998
- }
999
- operation.responses[status] = {
1000
- description: `Error (${status})`,
1001
- content: {
1002
- "application/json": {
1003
- schema: { oneOf: errors.map((error) => refSchema(errorSchemaComponentName(error))) }
1004
- }
1005
- }
1006
- };
1007
- }
1008
-
1009
- return operation;
1010
- }
1011
-
1012
- function errorResponseComponentName(error) {
1013
- return `${componentNameFromErrorCode(error.code)}Response`;
1014
- }
1015
-
1016
- function errorSchemaComponentName(error) {
1017
- return componentNameFromErrorCode(error.code);
1018
- }
1019
-
1020
- function titleFromIdentifier(id) {
1021
- return id.replace(/[_-]+/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
1022
- }
1023
-
1024
- function errorResponseDescription(error) {
1025
- return `${titleFromIdentifier(error.code)} (${error.status})`;
1026
- }
1027
-
1028
- function groupedErrorsByStatus(errors) {
1029
- const grouped = new Map();
1030
- for (const error of errors) {
1031
- const status = String(error.status);
1032
- if (!grouped.has(status)) grouped.set(status, []);
1033
- grouped.get(status).push(error);
1034
- }
1035
- return grouped;
1036
- }
1037
-
1038
- export function generateOpenApi(graph, options = {}) {
1039
- const contractGraph = generateApiContractGraph(graph, options);
1040
- const contracts = options.capabilityId ? [contractGraph] : Object.values(contractGraph);
1041
- const byId = indexStatements(graph);
1042
- const document = {
1043
- openapi: "3.1.0",
1044
- info: {
1045
- title: "Topogram API",
1046
- version: "0.1.0"
1047
- },
1048
- paths: {},
1049
- components: {
1050
- schemas: {
1051
- ErrorResponse: {
1052
- type: "object",
1053
- additionalProperties: false,
1054
- required: ["code", "message"],
1055
- properties: {
1056
- code: { type: "string" },
1057
- message: { type: "string" },
1058
- details: { type: "object", additionalProperties: true }
1059
- }
1060
- }
1061
- },
1062
- responses: {},
1063
- securitySchemes: {
1064
- bearerAuth: { type: "http", scheme: "bearer" }
1065
- }
1066
- }
1067
- };
1068
-
1069
- const componentNames = {
1070
- request: new Map(),
1071
- response: new Map(),
1072
- responseEnvelope: new Map()
1073
- };
1074
-
1075
- for (const contract of contracts) {
1076
- if (contract.requestContract && contract.requestContract.transport.body.length > 0) {
1077
- const requestSchemaName = componentNameFromShape(contract.requestContract.shape.id, "Request");
1078
- componentNames.request.set(contract.capability.id, requestSchemaName);
1079
- document.components.schemas[requestSchemaName] = requestBodySchema(contract);
1080
- }
1081
-
1082
- if (contract.responseContract) {
1083
- const responseSchemaName = componentNameFromShape(contract.responseContract.shape.id, "Response");
1084
- componentNames.response.set(contract.responseContract.shape.id, responseSchemaName);
1085
- if (!document.components.schemas[responseSchemaName]) {
1086
- document.components.schemas[responseSchemaName] = cloneSchema(
1087
- contract.responseContract.collection ? contract.responseContract.itemJsonSchema : contract.responseContract.jsonSchema
1088
- );
1089
- }
1090
-
1091
- if (
1092
- (contract.responseContract.mode === "paged" || contract.responseContract.mode === "cursor") &&
1093
- (contract.responseContract.pagination || contract.responseContract.cursor)
1094
- ) {
1095
- const envelopeSuffix = contract.responseContract.mode === "cursor" ? "CursorPageResponse" : "PageResponse";
1096
- const envelopeSchemaName = componentNameFromShape(contract.responseContract.shape.id, envelopeSuffix);
1097
- componentNames.responseEnvelope.set(contract.capability.id, envelopeSchemaName);
1098
- if (!document.components.schemas[envelopeSchemaName]) {
1099
- if (contract.responseContract.mode === "paged") {
1100
- const pagination = contract.responseContract.pagination;
1101
- document.components.schemas[envelopeSchemaName] = {
1102
- type: "object",
1103
- additionalProperties: false,
1104
- required: [
1105
- pagination.itemsProperty,
1106
- pagination.pageProperty,
1107
- pagination.pageSizeProperty,
1108
- pagination.totalProperty
1109
- ],
1110
- properties: {
1111
- [pagination.itemsProperty]: { type: "array", items: refSchema(responseSchemaName) },
1112
- [pagination.pageProperty]: { type: "integer" },
1113
- [pagination.pageSizeProperty]: { type: "integer" },
1114
- [pagination.totalProperty]: { type: "integer" }
1115
- }
1116
- };
1117
- } else {
1118
- const cursor = contract.responseContract.cursor;
1119
- const includeTotal = contract.responseContract.total?.included === true;
1120
- document.components.schemas[envelopeSchemaName] = {
1121
- type: "object",
1122
- additionalProperties: false,
1123
- required: ["items", cursor.responseNext],
1124
- properties: {
1125
- items: { type: "array", items: refSchema(responseSchemaName) },
1126
- [cursor.responseNext]: { type: "string" },
1127
- ...(cursor.responsePrev ? { [cursor.responsePrev]: { type: "string" } } : {}),
1128
- ...(includeTotal ? { total: { type: "integer" } } : {})
1129
- }
1130
- };
1131
- }
1132
- }
1133
- }
1134
- }
1135
-
1136
- for (const callback of contract.endpoint.callbacks || []) {
1137
- const payloadShapeId = callback.payload?.id || null;
1138
- if (!payloadShapeId) continue;
1139
- const payloadShape = byId.get(payloadShapeId);
1140
- if (!payloadShape) continue;
1141
- const responseSchemaName = componentNameFromShape(payloadShapeId, "Response");
1142
- componentNames.response.set(payloadShapeId, responseSchemaName);
1143
- if (!document.components.schemas[responseSchemaName]) {
1144
- document.components.schemas[responseSchemaName] = cloneSchema(generateShapeJsonSchema(payloadShape, byId));
1145
- }
1146
- }
1147
-
1148
- for (const error of contract.errors) {
1149
- const schemaName = errorSchemaComponentName(error);
1150
- if (!document.components.schemas[schemaName]) {
1151
- document.components.schemas[schemaName] = {
1152
- allOf: [
1153
- refSchema("ErrorResponse"),
1154
- {
1155
- type: "object",
1156
- properties: {
1157
- code: { type: "string", const: error.code }
1158
- }
1159
- }
1160
- ],
1161
- title: titleFromIdentifier(error.code)
1162
- };
1163
- }
1164
-
1165
- const componentName = errorResponseComponentName(error);
1166
- if (!document.components.responses[componentName]) {
1167
- document.components.responses[componentName] = {
1168
- description: errorResponseDescription(error),
1169
- content: {
1170
- "application/json": {
1171
- schema: refSchema(schemaName)
1172
- }
1173
- }
1174
- };
1175
- }
1176
- }
1177
- }
1178
-
1179
- for (const contract of contracts) {
1180
- const pathKey = contract.endpoint.path.replace(/:([A-Za-z0-9_]+)/g, "{$1}");
1181
- if (!document.paths[pathKey]) {
1182
- document.paths[pathKey] = {};
1183
- }
1184
- document.paths[pathKey][contract.endpoint.method.toLowerCase()] = operationFromContract(contract, componentNames);
1185
- }
1186
-
1187
- const needsSecurity = contracts.some((contract) => contract.endpoint.auth && contract.endpoint.auth !== "none");
1188
- if (!needsSecurity) {
1189
- delete document.components.securitySchemes;
1190
- if (Object.keys(document.components.schemas || {}).length === 0) delete document.components.schemas;
1191
- if (Object.keys(document.components.responses || {}).length === 0) delete document.components.responses;
1192
- if (Object.keys(document.components).length === 0) delete document.components;
1193
- }
1194
-
1195
- return document;
1196
- }
3
+ export { generateApiContractDebug, generateApiContractGraph } from "./api/contracts.js";
4
+ export { generateOpenApi } from "./api/openapi.js";