Showing the changes to packages/kernel (8 of 23 files). Show all
Unified · Split
| @@ -0,0 +1,34 @@ | ||
| 1 | +# IR v1 | |
| 2 | + | |
| 3 | +`@executor/ir` is the greenfield semantic center for imported sources. | |
| 4 | + | |
| 5 | +## Boundary | |
| 6 | + | |
| 7 | +- `CatalogSnapshotV1` is the canonical persisted import artifact. | |
| 8 | +- `CatalogV1` is the normalized semantic graph. | |
| 9 | +- `CatalogFragmentV1` is the adapter/importer output contract. | |
| 10 | +- `ToolDescriptor`, search docs, inspect views, and `InvocationPlan` are projections. | |
| 11 | + | |
| 12 | +## Direction | |
| 13 | + | |
| 14 | +Importers should move toward: | |
| 15 | + | |
| 16 | +`source -> CatalogFragmentV1 -> merge -> CatalogV1 -> projectors -> InvocationPlan` | |
| 17 | + | |
| 18 | +The existing runtime-specific compiled payload blobs are no longer the target semantic center. | |
| 19 | + | |
| 20 | +## Package Surface | |
| 21 | + | |
| 22 | +Use either: | |
| 23 | + | |
| 24 | +- `@executor/ir` | |
| 25 | +- `@executor/control-plane/ir` as a compatibility shim | |
| 26 | +- `import { ir } from "@executor/control-plane"` | |
| 27 | + | |
| 28 | +## Guarantees | |
| 29 | + | |
| 30 | +- provenance is required on entities | |
| 31 | +- synthetic entities are marked explicitly | |
| 32 | +- lossy normalization is represented by diagnostics | |
| 33 | +- inheritance is preserved in scopes and resolved at projection/planning time | |
| 34 | +- invocation plans remain connection-bound runtime artifacts, not canonical IR | |
| @@ -0,0 +1,27 @@ | ||
| 1 | +{ | |
| 2 | + "name": "@executor/ir", | |
| 3 | + "private": true, | |
| 4 | + "type": "module", | |
| 5 | + "exports": { | |
| 6 | + ".": "./src/index.ts", | |
| 7 | + "./catalog": "./src/catalog.ts", | |
| 8 | + "./ids": "./src/ids.ts", | |
| 9 | + "./model": "./src/model.ts" | |
| 10 | + }, | |
| 11 | + "scripts": { | |
| 12 | + "typecheck": "bunx tsc --noEmit -p tsconfig.json", | |
| 13 | + "test": "vitest run", | |
| 14 | + "test:watch": "vitest" | |
| 15 | + }, | |
| 16 | + "dependencies": { | |
| 17 | + "@executor/codemode-core": "workspace:*", | |
| 18 | + "effect": "catalog:" | |
| 19 | + }, | |
| 20 | + "devDependencies": { | |
| 21 | + "@effect/vitest": "catalog:", | |
| 22 | + "@types/node": "catalog:", | |
| 23 | + "bun-types": "catalog:", | |
| 24 | + "vitest": "catalog:" | |
| 25 | + }, | |
| 26 | + "version": "0.0.0" | |
| 27 | +} | |
| @@ -0,0 +1,748 @@ | ||
| 1 | +import { describe, expect, it } from "@effect/vitest"; | |
| 2 | + | |
| 3 | +import { | |
| 4 | + buildInvocationPlan, | |
| 5 | + createCatalogSnapshotV1, | |
| 6 | + createEmptyCatalogV1, | |
| 7 | + decodeCatalogSnapshotV1, | |
| 8 | + mergeCatalogFragments, | |
| 9 | + projectCatalogForAgentSdk, | |
| 10 | + projectSymbolExpandedView, | |
| 11 | + projectSymbolShallowView, | |
| 12 | + validateCatalogInvariants, | |
| 13 | +} from "./catalog"; | |
| 14 | +import { | |
| 15 | + CapabilityIdSchema, | |
| 16 | + DiagnosticIdSchema, | |
| 17 | + DocumentIdSchema, | |
| 18 | + ExecutableIdSchema, | |
| 19 | + ExampleSymbolIdSchema, | |
| 20 | + HeaderSymbolIdSchema, | |
| 21 | + ParameterSymbolIdSchema, | |
| 22 | + RequestBodySymbolIdSchema, | |
| 23 | + ResourceIdSchema, | |
| 24 | + ResponseSetIdSchema, | |
| 25 | + ResponseSymbolIdSchema, | |
| 26 | + ScopeIdSchema, | |
| 27 | + SecuritySchemeSymbolIdSchema, | |
| 28 | + ShapeSymbolIdSchema, | |
| 29 | +} from "./ids"; | |
| 30 | +import type { CatalogV1, CatalogFragmentV1, ProvenanceRef } from "./model"; | |
| 31 | + | |
| 32 | +const put = <K extends string, V>(record: Record<K, V>, key: K, value: V) => { | |
| 33 | + record[key] = value; | |
| 34 | +}; | |
| 35 | + | |
| 36 | +const expectNullableShapeContains = ( | |
| 37 | + catalog: CatalogV1, | |
| 38 | + nullableShapeId: ReturnType<typeof ShapeSymbolIdSchema.make>, | |
| 39 | + expectedInnerShapeId: ReturnType<typeof ShapeSymbolIdSchema.make>, | |
| 40 | +) => { | |
| 41 | + const shape = catalog.symbols[nullableShapeId]; | |
| 42 | + if (shape?.kind !== "shape" || shape.node.type !== "anyOf") { | |
| 43 | + throw new Error("Expected nullable anyOf shape"); | |
| 44 | + } | |
| 45 | + | |
| 46 | + expect(shape.node.items).toContain(expectedInnerShapeId); | |
| 47 | +}; | |
| 48 | + | |
| 49 | +const docId = DocumentIdSchema.make("doc_primary"); | |
| 50 | +const baseProvenance = (pointer: string): ProvenanceRef[] => [{ | |
| 51 | + relation: "declared", | |
| 52 | + documentId: docId, | |
| 53 | + pointer, | |
| 54 | +}]; | |
| 55 | + | |
| 56 | +const createHttpCatalog = (input: { | |
| 57 | + collideIds?: boolean; | |
| 58 | + method?: string; | |
| 59 | +} = {}): CatalogV1 => { | |
| 60 | + const catalog = createEmptyCatalogV1(); | |
| 61 | + | |
| 62 | + const resourceId = ResourceIdSchema.make("res_primary"); | |
| 63 | + const scopeId = ScopeIdSchema.make("scope_service"); | |
| 64 | + const authSchemeId = SecuritySchemeSymbolIdSchema.make("sym_security_oauth"); | |
| 65 | + const bodyShapeId = ShapeSymbolIdSchema.make("shape_event_body"); | |
| 66 | + const resultShapeId = ShapeSymbolIdSchema.make("shape_event_result"); | |
| 67 | + const stringShapeId = ShapeSymbolIdSchema.make("shape_string"); | |
| 68 | + const idPathParamId = ParameterSymbolIdSchema.make("sym_param_path_event_id"); | |
| 69 | + const calendarParamId = ParameterSymbolIdSchema.make("sym_param_path_calendar_id"); | |
| 70 | + const queryParamId = ParameterSymbolIdSchema.make( | |
| 71 | + input.collideIds ? "sym_param_query_id" : "sym_param_query_send_updates", | |
| 72 | + ); | |
| 73 | + const requestBodyId = RequestBodySymbolIdSchema.make("sym_request_body_event"); | |
| 74 | + const responseId = ResponseSymbolIdSchema.make("sym_response_200"); | |
| 75 | + const responseSetId = ResponseSetIdSchema.make("response_set_events_update"); | |
| 76 | + const executableId = ExecutableIdSchema.make("exec_http_events_update"); | |
| 77 | + const capabilityId = CapabilityIdSchema.make("cap_events_update"); | |
| 78 | + const exampleId = ExampleSymbolIdSchema.make("sym_example_event_body"); | |
| 79 | + | |
| 80 | + put(catalog.documents as Record<typeof docId, CatalogV1["documents"][typeof docId]>, docId, { | |
| 81 | + id: docId, | |
| 82 | + kind: "openapi", | |
| 83 | + title: "Calendar API", | |
| 84 | + fetchedAt: "2026-03-14T00:00:00.000Z", | |
| 85 | + rawRef: "memory://calendar/openapi.json", | |
| 86 | + }); | |
| 87 | + | |
| 88 | + put(catalog.resources as Record<typeof resourceId, CatalogV1["resources"][typeof resourceId]>, resourceId, { | |
| 89 | + id: resourceId, | |
| 90 | + documentId: docId, | |
| 91 | + canonicalUri: "https://example.test/openapi.json", | |
| 92 | + baseUri: "https://example.test/openapi.json", | |
| 93 | + anchors: {}, | |
| 94 | + dynamicAnchors: {}, | |
| 95 | + synthetic: false, | |
| 96 | + provenance: baseProvenance("#"), | |
| 97 | + }); | |
| 98 | + | |
| 99 | + put(catalog.scopes as Record<typeof scopeId, CatalogV1["scopes"][typeof scopeId]>, scopeId, { | |
| 100 | + id: scopeId, | |
| 101 | + kind: "service", | |
| 102 | + name: "Calendar", | |
| 103 | + namespace: "google.calendar", | |
| 104 | + docs: { | |
| 105 | + summary: "Calendar service", | |
| 106 | + description: "Manage calendar events.", | |
| 107 | + }, | |
| 108 | + synthetic: false, | |
| 109 | + provenance: baseProvenance("#/servers/0"), | |
| 110 | + }); | |
| 111 | + | |
| 112 | + put(catalog.symbols as Record<typeof authSchemeId, CatalogV1["symbols"][typeof authSchemeId]>, authSchemeId, { | |
| 113 | + id: authSchemeId, | |
| 114 | + kind: "securityScheme", | |
| 115 | + schemeType: "oauth2", | |
| 116 | + docs: { | |
| 117 | + summary: "OAuth 2.0", | |
| 118 | + }, | |
| 119 | + oauth: { | |
| 120 | + scopes: { | |
| 121 | + "calendar.events": "Manage calendar events", | |
| 122 | + }, | |
| 123 | + }, | |
| 124 | + synthetic: false, | |
| 125 | + provenance: baseProvenance("#/components/securitySchemes/oauth2"), | |
| 126 | + }); | |
| 127 | + | |
| 128 | + put(catalog.symbols as Record<typeof stringShapeId, CatalogV1["symbols"][typeof stringShapeId]>, stringShapeId, { | |
| 129 | + id: stringShapeId, | |
| 130 | + kind: "shape", | |
| 131 | + resourceId, | |
| 132 | + title: "String", | |
| 133 | + node: { | |
| 134 | + type: "scalar", | |
| 135 | + scalar: "string", | |
| 136 | + }, | |
| 137 | + synthetic: false, | |
| 138 | + provenance: baseProvenance("#/components/schemas/String"), | |
| 139 | + }); | |
| 140 | + | |
| 141 | + put(catalog.symbols as Record<typeof bodyShapeId, CatalogV1["symbols"][typeof bodyShapeId]>, bodyShapeId, { | |
| 142 | + id: bodyShapeId, | |
| 143 | + kind: "shape", | |
| 144 | + resourceId, | |
| 145 | + title: "EventUpdateBody", | |
| 146 | + node: { | |
| 147 | + type: "object", | |
| 148 | + fields: { | |
| 149 | + summary: { | |
| 150 | + shapeId: stringShapeId, | |
| 151 | + }, | |
| 152 | + }, | |
| 153 | + additionalProperties: false, | |
| 154 | + }, | |
| 155 | + synthetic: false, | |
| 156 | + provenance: baseProvenance("#/components/schemas/EventUpdateBody"), | |
| 157 | + }); | |
| 158 | + | |
| 159 | + put(catalog.symbols as Record<typeof resultShapeId, CatalogV1["symbols"][typeof resultShapeId]>, resultShapeId, { | |
| 160 | + id: resultShapeId, | |
| 161 | + kind: "shape", | |
| 162 | + resourceId, | |
| 163 | + title: "Event", | |
| 164 | + node: { | |
| 165 | + type: "object", | |
| 166 | + fields: { | |
| 167 | + id: { | |
| 168 | + shapeId: stringShapeId, | |
| 169 | + }, | |
| 170 | + summary: { | |
| 171 | + shapeId: stringShapeId, | |
| 172 | + }, | |
| 173 | + }, | |
| 174 | + required: ["id"], | |
| 175 | + additionalProperties: false, | |
| 176 | + }, | |
| 177 | + synthetic: false, | |
| 178 | + provenance: baseProvenance("#/components/schemas/Event"), | |
| 179 | + }); | |
| 180 | + | |
| 181 | + put(catalog.symbols as Record<typeof exampleId, CatalogV1["symbols"][typeof exampleId]>, exampleId, { | |
| 182 | + id: exampleId, | |
| 183 | + kind: "example", | |
| 184 | + exampleKind: "value", | |
| 185 | + name: "Event body example", | |
| 186 | + value: { | |
| 187 | + summary: "Team sync", | |
| 188 | + }, | |
| 189 | + synthetic: false, | |
| 190 | + provenance: baseProvenance("#/components/examples/EventBody"), | |
| 191 | + }); | |
| 192 | + | |
| 193 | + put(catalog.symbols as Record<typeof calendarParamId, CatalogV1["symbols"][typeof calendarParamId]>, calendarParamId, { | |
| 194 | + id: calendarParamId, | |
| 195 | + kind: "parameter", | |
| 196 | + name: input.collideIds ? "id" : "calendarId", | |
| 197 | + location: "path", | |
| 198 | + required: true, | |
| 199 | + docs: { | |
| 200 | + summary: "Calendar identifier", | |
| 201 | + description: "The target calendar.", | |
| 202 | + }, | |
| 203 | + schemaShapeId: stringShapeId, | |
| 204 | + synthetic: false, | |
| 205 | + provenance: baseProvenance("#/paths/~1events~1{calendarId}/parameters/0"), | |
| 206 | + }); | |
| 207 | + | |
| 208 | + put(catalog.symbols as Record<typeof idPathParamId, CatalogV1["symbols"][typeof idPathParamId]>, idPathParamId, { | |
| 209 | + id: idPathParamId, | |
| 210 | + kind: "parameter", | |
| 211 | + name: "eventId", | |
| 212 | + location: "path", | |
| 213 | + required: true, | |
| 214 | + docs: { | |
| 215 | + summary: "Event identifier", | |
| 216 | + }, | |
| 217 | + schemaShapeId: stringShapeId, | |
| 218 | + synthetic: false, | |
| 219 | + provenance: baseProvenance("#/paths/~1events~1{eventId}/parameters/0"), | |
| 220 | + }); | |
| 221 | + | |
| 222 | + put(catalog.symbols as Record<typeof queryParamId, CatalogV1["symbols"][typeof queryParamId]>, queryParamId, { | |
| 223 | + id: queryParamId, | |
| 224 | + kind: "parameter", | |
| 225 | + name: input.collideIds ? "id" : "sendUpdates", | |
| 226 | + location: "query", | |
| 227 | + docs: { | |
| 228 | + summary: "Notification mode", | |
| 229 | + }, | |
| 230 | + schemaShapeId: stringShapeId, | |
| 231 | + synthetic: false, | |
| 232 | + provenance: baseProvenance("#/paths/~1events/parameters/0"), | |
| 233 | + }); | |
| 234 | + | |
| 235 | + put(catalog.symbols as Record<typeof requestBodyId, CatalogV1["symbols"][typeof requestBodyId]>, requestBodyId, { | |
| 236 | + id: requestBodyId, | |
| 237 | + kind: "requestBody", | |
| 238 | + required: true, | |
| 239 | + docs: { | |
| 240 | + summary: "Event payload", | |
| 241 | + }, | |
| 242 | + contents: [ | |
| 243 | + { | |
| 244 | + mediaType: "application/json", | |
| 245 | + shapeId: bodyShapeId, | |
| 246 | + exampleIds: [exampleId], | |
| 247 | + }, | |
| 248 | + ], | |
| 249 | + synthetic: false, | |
| 250 | + provenance: baseProvenance("#/paths/~1events/requestBody"), | |
| 251 | + }); | |
| 252 | + | |
| 253 | + put(catalog.symbols as Record<typeof responseId, CatalogV1["symbols"][typeof responseId]>, responseId, { | |
| 254 | + id: responseId, | |
| 255 | + kind: "response", | |
| 256 | + docs: { | |
| 257 | + summary: "Updated event", | |
| 258 | + }, | |
| 259 | + contents: [ | |
| 260 | + { | |
| 261 | + mediaType: "application/json", | |
| 262 | + shapeId: resultShapeId, | |
| 263 | + }, | |
| 264 | + ], | |
| 265 | + synthetic: false, | |
| 266 | + provenance: baseProvenance("#/paths/~1events/responses/200"), | |
| 267 | + }); | |
| 268 | + | |
| 269 | + put(catalog.responseSets as Record<typeof responseSetId, CatalogV1["responseSets"][typeof responseSetId]>, responseSetId, { | |
| 270 | + id: responseSetId, | |
| 271 | + variants: [ | |
| 272 | + { | |
| 273 | + match: { | |
| 274 | + kind: "exact", | |
| 275 | + status: 200, | |
| 276 | + }, | |
| 277 | + responseId, | |
| 278 | + traits: ["success"], | |
| 279 | + }, | |
| 280 | + ], | |
| 281 | + synthetic: false, | |
| 282 | + provenance: baseProvenance("#/paths/~1events/responses"), | |
| 283 | + }); | |
| 284 | + | |
| 285 | + put(catalog.executables as Record<typeof executableId, CatalogV1["executables"][typeof executableId]>, executableId, { | |
| 286 | + id: executableId, | |
| 287 | + protocol: "http", | |
| 288 | + capabilityId, | |
| 289 | + scopeId, | |
| 290 | + method: input.method ?? "PATCH", | |
| 291 | + pathTemplate: "/calendars/{calendarId}/events/{eventId}", | |
| 292 | + pathParameterIds: [calendarParamId, idPathParamId], | |
| 293 | + queryParameterIds: [queryParamId], | |
| 294 | + requestBodyId, | |
| 295 | + responseSetId, | |
| 296 | + synthetic: false, | |
| 297 | + provenance: baseProvenance("#/paths/~1events/patch"), | |
| 298 | + }); | |
| 299 | + | |
| 300 | + put(catalog.capabilities as Record<typeof capabilityId, CatalogV1["capabilities"][typeof capabilityId]>, capabilityId, { | |
| 301 | + id: capabilityId, | |
| 302 | + serviceScopeId: scopeId, | |
| 303 | + surface: { | |
| 304 | + toolPath: ["google", "calendar", "events", "update"], | |
| 305 | + title: "Update event", | |
| 306 | + summary: "Update a calendar event.", | |
| 307 | + description: "Updates an existing event by ID.", | |
| 308 | + tags: ["calendar", "events"], | |
| 309 | + }, | |
| 310 | + semantics: { | |
| 311 | + effect: "write", | |
| 312 | + safe: false, | |
| 313 | + idempotent: false, | |
| 314 | + destructive: false, | |
| 315 | + }, | |
| 316 | + auth: { | |
| 317 | + kind: "scheme", | |
| 318 | + schemeId: authSchemeId, | |
| 319 | + scopes: ["calendar.events"], | |
| 320 | + }, | |
| 321 | + interaction: { | |
| 322 | + approval: { | |
| 323 | + mayRequire: true, | |
| 324 | + reasons: ["write"], | |
| 325 | + }, | |
| 326 | + elicitation: { | |
| 327 | + mayRequest: false, | |
| 328 | + }, | |
| 329 | + resume: { | |
| 330 | + supported: true, | |
| 331 | + }, | |
| 332 | + }, | |
| 333 | + executableIds: [executableId], | |
| 334 | + preferredExecutableId: executableId, | |
| 335 | + synthetic: false, | |
| 336 | + provenance: baseProvenance("#/paths/~1events/patch"), | |
| 337 | + }); | |
| 338 | + | |
| 339 | + return catalog; | |
| 340 | +}; | |
| 341 | + | |
| 342 | +const createGraphqlCatalog = (): CatalogV1 => { | |
| 343 | + const catalog = createEmptyCatalogV1(); | |
| 344 | + | |
| 345 | + const resourceId = ResourceIdSchema.make("res_graphql"); | |
| 346 | + const scopeId = ScopeIdSchema.make("scope_graphql_service"); | |
| 347 | + const argShapeId = ShapeSymbolIdSchema.make("shape_graphql_args"); | |
| 348 | + const inputShapeId = ShapeSymbolIdSchema.make("shape_graphql_input"); | |
| 349 | + const selectShapeId = ShapeSymbolIdSchema.make("shape_graphql_select"); | |
| 350 | + const resultShapeId = ShapeSymbolIdSchema.make("shape_graphql_result"); | |
| 351 | + const stringShapeId = ShapeSymbolIdSchema.make("shape_graphql_string"); | |
| 352 | + const responseId = ResponseSymbolIdSchema.make("sym_graphql_response"); | |
| 353 | + const responseSetId = ResponseSetIdSchema.make("response_set_graphql"); | |
| 354 | + const executableId = ExecutableIdSchema.make("exec_graphql_update"); | |
| 355 | + const capabilityId = CapabilityIdSchema.make("cap_graphql_update"); | |
| 356 | + | |
| 357 | + put(catalog.documents as Record<typeof docId, CatalogV1["documents"][typeof docId]>, docId, { | |
| 358 | + id: docId, | |
| 359 | + kind: "graphql-schema", | |
| 360 | + title: "GraphQL schema", | |
| 361 | + fetchedAt: "2026-03-14T00:00:00.000Z", | |
| 362 | + rawRef: "memory://graphql/schema.json", | |
| 363 | + }); | |
| 364 | + | |
| 365 | + put(catalog.resources as Record<typeof resourceId, CatalogV1["resources"][typeof resourceId]>, resourceId, { | |
| 366 | + id: resourceId, | |
| 367 | + documentId: docId, | |
| 368 | + canonicalUri: "https://example.test/graphql", | |
| 369 | + baseUri: "https://example.test/graphql", | |
| 370 | + anchors: {}, | |
| 371 | + dynamicAnchors: {}, | |
| 372 | + synthetic: false, | |
| 373 | + provenance: baseProvenance("#"), | |
| 374 | + }); | |
| 375 | + | |
| 376 | + put(catalog.scopes as Record<typeof scopeId, CatalogV1["scopes"][typeof scopeId]>, scopeId, { | |
| 377 | + id: scopeId, | |
| 378 | + kind: "service", | |
| 379 | + name: "GraphQL", | |
| 380 | + synthetic: false, | |
| 381 | + provenance: baseProvenance("#/schema"), | |
| 382 | + }); | |
| 383 | + | |
| 384 | + put(catalog.symbols as Record<typeof stringShapeId, CatalogV1["symbols"][typeof stringShapeId]>, stringShapeId, { | |
| 385 | + id: stringShapeId, | |
| 386 | + kind: "shape", | |
| 387 | + resourceId, | |
| 388 | + title: "String", | |
| 389 | + node: { | |
| 390 | + type: "scalar", | |
| 391 | + scalar: "string", | |
| 392 | + }, | |
| 393 | + synthetic: false, | |
| 394 | + provenance: baseProvenance("#/$defs/String"), | |
| 395 | + }); | |
| 396 | + | |
| 397 | + put(catalog.symbols as Record<typeof inputShapeId, CatalogV1["symbols"][typeof inputShapeId]>, inputShapeId, { | |
| 398 | + id: inputShapeId, | |
| 399 | + kind: "shape", | |
| 400 | + resourceId, | |
| 401 | + title: "UserInput", | |
| 402 | + node: { | |
| 403 | + type: "object", | |
| 404 | + fields: { | |
| 405 | + name: { | |
| 406 | + shapeId: stringShapeId, | |
| 407 | + }, | |
| 408 | + }, | |
| 409 | + additionalProperties: false, | |
| 410 | + }, | |
| 411 | + synthetic: false, | |
| 412 | + provenance: baseProvenance("#/$defs/UserInput"), | |
| 413 | + }); | |
| 414 | + | |
| 415 | + put(catalog.symbols as Record<typeof argShapeId, CatalogV1["symbols"][typeof argShapeId]>, argShapeId, { | |
| 416 | + id: argShapeId, | |
| 417 | + kind: "shape", | |
| 418 | + resourceId, | |
| 419 | + title: "UpdateUserArgs", | |
| 420 | + node: { | |
| 421 | + type: "object", | |
| 422 | + fields: { | |
| 423 | + id: { | |
| 424 | + shapeId: stringShapeId, | |
| 425 | + }, | |
| 426 | + input: { | |
| 427 | + shapeId: inputShapeId, | |
| 428 | + }, | |
| 429 | + }, | |
| 430 | + required: ["id", "input"], | |
| 431 | + additionalProperties: false, | |
| 432 | + }, | |
| 433 | + synthetic: false, | |
| 434 | + provenance: baseProvenance("#/$defs/UpdateUserArgs"), | |
| 435 | + }); | |
| 436 | + | |
| 437 | + put(catalog.symbols as Record<typeof selectShapeId, CatalogV1["symbols"][typeof selectShapeId]>, selectShapeId, { | |
| 438 | + id: selectShapeId, | |
| 439 | + kind: "shape", | |
| 440 | + resourceId, | |
| 441 | + title: "UserSelection", | |
| 442 | + node: { | |
| 443 | + type: "object", | |
| 444 | + fields: { | |
| 445 | + id: { | |
| 446 | + shapeId: stringShapeId, | |
| 447 | + }, | |
| 448 | + name: { | |
| 449 | + shapeId: stringShapeId, | |
| 450 | + }, | |
| 451 | + }, | |
| 452 | + additionalProperties: false, | |
| 453 | + }, | |
| 454 | + synthetic: false, | |
| 455 | + provenance: baseProvenance("#/$defs/UserSelection"), | |
| 456 | + }); | |
| 457 | + | |
| 458 | + put(catalog.symbols as Record<typeof resultShapeId, CatalogV1["symbols"][typeof resultShapeId]>, resultShapeId, { | |
| 459 | + id: resultShapeId, | |
| 460 | + kind: "shape", | |
| 461 | + resourceId, | |
| 462 | + title: "User", | |
| 463 | + node: { | |
| 464 | + type: "object", | |
| 465 | + fields: { | |
| 466 | + id: { | |
| 467 | + shapeId: stringShapeId, | |
| 468 | + }, | |
| 469 | + name: { | |
| 470 | + shapeId: stringShapeId, | |
| 471 | + }, | |
| 472 | + }, | |
| 473 | + required: ["id"], | |
| 474 | + additionalProperties: false, | |
| 475 | + }, | |
| 476 | + synthetic: false, | |
| 477 | + provenance: baseProvenance("#/$defs/User"), | |
| 478 | + }); | |
| 479 | + | |
| 480 | + put(catalog.symbols as Record<typeof responseId, CatalogV1["symbols"][typeof responseId]>, responseId, { | |
| 481 | + id: responseId, | |
| 482 | + kind: "response", | |
| 483 | + contents: [ | |
| 484 | + { | |
| 485 | + mediaType: "application/json", | |
| 486 | + shapeId: resultShapeId, | |
| 487 | + }, | |
| 488 | + ], | |
| 489 | + synthetic: false, | |
| 490 | + provenance: baseProvenance("#/responses/default"), | |
| 491 | + }); | |
| 492 | + | |
| 493 | + put(catalog.responseSets as Record<typeof responseSetId, CatalogV1["responseSets"][typeof responseSetId]>, responseSetId, { | |
| 494 | + id: responseSetId, | |
| 495 | + variants: [ | |
| 496 | + { | |
| 497 | + match: { | |
| 498 | + kind: "default", | |
| 499 | + }, | |
| 500 | + responseId, | |
| 501 | + traits: ["success"], | |
| 502 | + }, | |
| 503 | + ], | |
| 504 | + synthetic: false, | |
| 505 | + provenance: baseProvenance("#/responses"), | |
| 506 | + }); | |
| 507 | + | |
| 508 | + put(catalog.executables as Record<typeof executableId, CatalogV1["executables"][typeof executableId]>, executableId, { | |
| 509 | + id: executableId, | |
| 510 | + protocol: "graphql", | |
| 511 | + capabilityId, | |
| 512 | + scopeId, | |
| 513 | + toolKind: "field", | |
| 514 | + operationType: "mutation", | |
| 515 | + rootField: "updateUser", | |
| 516 | + argumentShapeId: argShapeId, | |
| 517 | + resultShapeId, | |
| 518 | + selectionShapeId: selectShapeId, | |
| 519 | + selectionMode: "caller", | |
| 520 | + responseSetId, | |
| 521 | + synthetic: false, | |
| 522 | + provenance: baseProvenance("#/mutations/updateUser"), | |
| 523 | + }); | |
| 524 | + | |
| 525 | + put(catalog.capabilities as Record<typeof capabilityId, CatalogV1["capabilities"][typeof capabilityId]>, capabilityId, { | |
| 526 | + id: capabilityId, | |
| 527 | + serviceScopeId: scopeId, | |
| 528 | + surface: { | |
| 529 | + toolPath: ["graphql", "users", "update"], | |
| 530 | + title: "Update user", | |
| 531 | + summary: "Update a user.", | |
| 532 | + }, | |
| 533 | + semantics: { | |
| 534 | + effect: "write", | |
| 535 | + }, | |
| 536 | + auth: { | |
| 537 | + kind: "none", | |
| 538 | + }, | |
| 539 | + interaction: { | |
| 540 | + approval: { | |
| 541 | + mayRequire: true, | |
| 542 | + reasons: ["write"], | |
| 543 | + }, | |
| 544 | + elicitation: { | |
| 545 | + mayRequest: false, | |
| 546 | + }, | |
| 547 | + resume: { | |
| 548 | + supported: false, | |
| 549 | + }, | |
| 550 | + }, | |
| 551 | + executableIds: [executableId], | |
| 552 | + preferredExecutableId: executableId, | |
| 553 | + synthetic: false, | |
| 554 | + provenance: baseProvenance("#/mutations/updateUser"), | |
| 555 | + }); | |
| 556 | + | |
| 557 | + return catalog; | |
| 558 | +}; | |
| 559 | + | |
| 560 | +describe("IR catalog", () => { | |
| 561 | + it("decodes a snapshot through the canonical schema", () => { | |
| 562 | + const catalog = createHttpCatalog(); | |
| 563 | + const snapshot = createCatalogSnapshotV1({ | |
| 564 | + import: { | |
| 565 | + sourceKind: "openapi", | |
| 566 | + adapterKey: "openapi", | |
| 567 | + importerVersion: "1.0.0", | |
| 568 | + importedAt: "2026-03-14T00:00:00.000Z", | |
| 569 | + sourceConfigHash: "hash_source", | |
| 570 | + }, | |
| 571 | + catalog, | |
| 572 | + }); | |
| 573 | + | |
| 574 | + const decoded = decodeCatalogSnapshotV1(snapshot); | |
| 575 | + expect(decoded.version).toBe("ir.v1.snapshot"); | |
| 576 | + expect(decoded.catalog.capabilities[CapabilityIdSchema.make("cap_events_update")]?.surface.title).toBe( | |
| 577 | + "Update event", | |
| 578 | + ); | |
| 579 | + }); | |
| 580 | + | |
| 581 | + it("merges fragments and emits diagnostics on conflicting IDs", () => { | |
| 582 | + const fragmentA: CatalogFragmentV1 = { | |
| 583 | + version: "ir.v1.fragment", | |
| 584 | + documents: { | |
| 585 | + [docId]: { | |
| 586 | + id: docId, | |
| 587 | + kind: "openapi", | |
| 588 | + title: "A", | |
| 589 | + fetchedAt: "2026-03-14T00:00:00.000Z", | |
| 590 | + rawRef: "memory://a", | |
| 591 | + }, | |
| 592 | + }, | |
| 593 | + }; | |
| 594 | + const fragmentB: CatalogFragmentV1 = { | |
| 595 | + version: "ir.v1.fragment", | |
| 596 | + documents: { | |
| 597 | + [docId]: { | |
| 598 | + id: docId, | |
| 599 | + kind: "openapi", | |
| 600 | + title: "B", | |
| 601 | + fetchedAt: "2026-03-14T00:00:00.000Z", | |
| 602 | + rawRef: "memory://b", | |
| 603 | + }, | |
| 604 | + }, | |
| 605 | + }; | |
| 606 | + | |
| 607 | + const merged = mergeCatalogFragments([fragmentA, fragmentB]); | |
| 608 | + expect(merged.documents[docId]?.title).toBe("A"); | |
| 609 | + expect(Object.values(merged.diagnostics).some((diagnostic) => diagnostic.code === "merge_conflict_preserved_first")).toBe(true); | |
| 610 | + }); | |
| 611 | + | |
| 612 | + it("reports invariant violations for unresolved shapes without diagnostics", () => { | |
| 613 | + const catalog = createEmptyCatalogV1(); | |
| 614 | + const shapeId = ShapeSymbolIdSchema.make("shape_unresolved"); | |
| 615 | + | |
| 616 | + put(catalog.documents as Record<typeof docId, CatalogV1["documents"][typeof docId]>, docId, { | |
| 617 | + id: docId, | |
| 618 | + kind: "custom", | |
| 619 | + fetchedAt: "2026-03-14T00:00:00.000Z", | |
| 620 | + rawRef: "memory://custom", | |
| 621 | + }); | |
| 622 | + | |
| 623 | + put(catalog.symbols as Record<typeof shapeId, CatalogV1["symbols"][typeof shapeId]>, shapeId, { | |
| 624 | + id: shapeId, | |
| 625 | + kind: "shape", | |
| 626 | + title: "Unresolved", | |
| 627 | + node: { | |
| 628 | + type: "unknown", | |
| 629 | + reason: "unresolved reference #/components/schemas/Missing", | |
| 630 | + }, | |
| 631 | + synthetic: false, | |
| 632 | + provenance: baseProvenance("#/components/schemas/Missing"), | |
| 633 | + }); | |
| 634 | + | |
| 635 | + const violations = validateCatalogInvariants(catalog); | |
| 636 | + expect(violations.map((violation) => violation.code)).toContain("missing_resource_context"); | |
| 637 | + expect(violations.map((violation) => violation.code)).toContain("missing_unresolved_ref_diagnostic"); | |
| 638 | + }); | |
| 639 | + | |
| 640 | + it("projects HTTP capabilities into ergonomic call and result shapes", () => { | |
| 641 | + const projected = projectCatalogForAgentSdk({ | |
| 642 | + catalog: createHttpCatalog(), | |
| 643 | + }); | |
| 644 | + | |
| 645 | + const descriptor = projected.toolDescriptors[CapabilityIdSchema.make("cap_events_update")]; | |
| 646 | + expect(descriptor.toolPath).toEqual(["google", "calendar", "events", "update"]); | |
| 647 | + | |
| 648 | + const callShape = projected.catalog.symbols[descriptor.callShapeId]; | |
| 649 | + expect(callShape?.kind).toBe("shape"); | |
| 650 | + if (callShape?.kind !== "shape" || callShape.node.type !== "object") { | |
| 651 | + throw new Error("Expected projected call shape object"); | |
| 652 | + } | |
| 653 | + | |
| 654 | + expect(Object.keys(callShape.node.fields)).toEqual([ | |
| 655 | + "calendarId", | |
| 656 | + "eventId", | |
| 657 | + "sendUpdates", | |
| 658 | + "body", | |
| 659 | + ]); | |
| 660 | + expect(callShape.node.required).toEqual(["calendarId", "eventId", "body"]); | |
| 661 | + | |
| 662 | + const resultShape = descriptor.resultShapeId | |
| 663 | + ? projected.catalog.symbols[descriptor.resultShapeId] | |
| 664 | + : undefined; | |
| 665 | + if (resultShape?.kind !== "shape" || resultShape.node.type !== "object") { | |
| 666 | + throw new Error("Expected projected HTTP result envelope"); | |
| 667 | + } | |
| 668 | + expect(Object.keys(resultShape.node.fields)).toEqual(["data", "error", "headers", "status"]); | |
| 669 | + expectNullableShapeContains( | |
| 670 | + projected.catalog, | |
| 671 | + resultShape.node.fields.data.shapeId, | |
| 672 | + ShapeSymbolIdSchema.make("shape_event_result"), | |
| 673 | + ); | |
| 674 | + | |
| 675 | + const searchDoc = projected.searchDocs[CapabilityIdSchema.make("cap_events_update")]; | |
| 676 | + expect(searchDoc.authHints).toContain("oauth2"); | |
| 677 | + expect(searchDoc.tags).toEqual(["calendar", "events"]); | |
| 678 | + }); | |
| 679 | + | |
| 680 | + it("groups colliding HTTP parameter names into path/query containers", () => { | |
| 681 | + const projected = projectCatalogForAgentSdk({ | |
| 682 | + catalog: createHttpCatalog({ collideIds: true }), | |
| 683 | + }); | |
| 684 | + | |
| 685 | + const descriptor = projected.toolDescriptors[CapabilityIdSchema.make("cap_events_update")]; | |
| 686 | + const callShape = projected.catalog.symbols[descriptor.callShapeId]; | |
| 687 | + if (callShape?.kind !== "shape" || callShape.node.type !== "object") { | |
| 688 | + throw new Error("Expected projected call shape object"); | |
| 689 | + } | |
| 690 | + | |
| 691 | + expect(Object.keys(callShape.node.fields)).toContain("path"); | |
| 692 | + expect(Object.keys(callShape.node.fields)).toContain("query"); | |
| 693 | + expect(Object.keys(callShape.node.fields)).not.toContain("id"); | |
| 694 | + expect(Object.values(projected.catalog.diagnostics).some((diagnostic) => diagnostic.code === "projection_collision_grouped_fields")).toBe(true); | |
| 695 | + }); | |
| 696 | + | |
| 697 | + it("keeps GraphQL selection explicit for caller-driven outputs", () => { | |
| 698 | + const projected = projectCatalogForAgentSdk({ | |
| 699 | + catalog: createGraphqlCatalog(), | |
| 700 | + }); | |
| 701 | + | |
| 702 | + const descriptor = projected.toolDescriptors[CapabilityIdSchema.make("cap_graphql_update")]; | |
| 703 | + const callShape = projected.catalog.symbols[descriptor.callShapeId]; | |
| 704 | + if (callShape?.kind !== "shape" || callShape.node.type !== "object") { | |
| 705 | + throw new Error("Expected projected GraphQL call shape object"); | |
| 706 | + } | |
| 707 | + | |
| 708 | + expect(Object.keys(callShape.node.fields)).toEqual(["id", "input", "select"]); | |
| 709 | + const resultShape = descriptor.resultShapeId | |
| 710 | + ? projected.catalog.symbols[descriptor.resultShapeId] | |
| 711 | + : undefined; | |
| 712 | + if (resultShape?.kind !== "shape" || resultShape.node.type !== "object") { | |
| 713 | + throw new Error("Expected projected GraphQL result envelope"); | |
| 714 | + } | |
| 715 | + expect(Object.keys(resultShape.node.fields)).toEqual(["data", "error", "headers", "status"]); | |
| 716 | + expectNullableShapeContains( | |
| 717 | + projected.catalog, | |
| 718 | + resultShape.node.fields.data.shapeId, | |
| 719 | + ShapeSymbolIdSchema.make("shape_graphql_result"), | |
| 720 | + ); | |
| 721 | + }); | |
| 722 | + | |
| 723 | + it("builds inspect views and invocation plans from the projected graph", () => { | |
| 724 | + const projected = projectCatalogForAgentSdk({ | |
| 725 | + catalog: createHttpCatalog(), | |
| 726 | + }); | |
| 727 | + const capabilityId = CapabilityIdSchema.make("cap_events_update"); | |
| 728 | + const descriptor = projected.toolDescriptors[capabilityId]; | |
| 729 | + const shallow = projectSymbolShallowView(projected.catalog, descriptor.callShapeId); | |
| 730 | + const expanded = projectSymbolExpandedView(projected.catalog, descriptor.callShapeId); | |
| 731 | + const plan = buildInvocationPlan({ | |
| 732 | + catalog: projected.catalog, | |
| 733 | + capabilityId, | |
| 734 | + connectionBindingId: "binding_primary", | |
| 735 | + resolvedEndpoint: "https://example.test", | |
| 736 | + requestPlan: { | |
| 737 | + method: "PATCH", | |
| 738 | + path: "/calendars/primary/events/abc", | |
| 739 | + }, | |
| 740 | + }); | |
| 741 | + | |
| 742 | + expect(shallow.edges.some((edge) => edge.label === "field:body")).toBe(true); | |
| 743 | + expect(expanded.symbol.id).toBe(descriptor.callShapeId); | |
| 744 | + expect(plan.connectionBindingId).toBe("binding_primary"); | |
| 745 | + expect(plan.resolvedScopeChain).toEqual([ScopeIdSchema.make("scope_service")]); | |
| 746 | + expect(plan.resolvedInteraction?.requiresApproval).toBe(true); | |
| 747 | + }); | |
| 748 | +}); | |
| @@ -0,0 +1,2018 @@ | ||
| 1 | +import { sha256Hex } from "@executor/codemode-core"; | |
| 2 | +import * as ParseResult from "effect/ParseResult"; | |
| 3 | +import { Schema } from "effect"; | |
| 4 | + | |
| 5 | +import { | |
| 6 | + CapabilityIdSchema, | |
| 7 | + type CapabilityId, | |
| 8 | + DiagnosticIdSchema, | |
| 9 | + type DiagnosticId, | |
| 10 | + DocumentIdSchema, | |
| 11 | + type DocumentId, | |
| 12 | + ExecutableIdSchema, | |
| 13 | + type ExecutableId, | |
| 14 | + ExampleSymbolIdSchema, | |
| 15 | + type ExampleSymbolId, | |
| 16 | + HeaderSymbolIdSchema, | |
| 17 | + type HeaderSymbolId, | |
| 18 | + ParameterSymbolIdSchema, | |
| 19 | + type ParameterSymbolId, | |
| 20 | + type RequestBodySymbolId, | |
| 21 | + ResourceIdSchema, | |
| 22 | + type ResourceId, | |
| 23 | + type ResponseSymbolId, | |
| 24 | + type ResponseSetId, | |
| 25 | + ScopeIdSchema, | |
| 26 | + type ScopeId, | |
| 27 | + type ShapeSymbolId, | |
| 28 | + ShapeSymbolIdSchema, | |
| 29 | + type SymbolId, | |
| 30 | +} from "./ids"; | |
| 31 | +import { | |
| 32 | + CatalogFragmentV1Schema, | |
| 33 | + type CatalogFragmentV1, | |
| 34 | + CatalogSnapshotV1Schema, | |
| 35 | + CatalogV1Schema, | |
| 36 | + type CatalogV1, | |
| 37 | + type Capability, | |
| 38 | + type ContentSpec, | |
| 39 | + type DocumentationBlock, | |
| 40 | + type Executable, | |
| 41 | + type GraphQLExecutable, | |
| 42 | + type HttpExecutable, | |
| 43 | + type ImportDiagnostic, | |
| 44 | + type ImportMetadata, | |
| 45 | + type InteractionSpec, | |
| 46 | + type McpExecutable, | |
| 47 | + type ParameterSymbol, | |
| 48 | + type HeaderSymbol, | |
| 49 | + type ResponseSet, | |
| 50 | + type ResponseSymbol, | |
| 51 | + type Scope, | |
| 52 | + type ScopeDefaults, | |
| 53 | + type ShapeSymbol, | |
| 54 | + type StatusMatch, | |
| 55 | + type Symbol as IrSymbol, | |
| 56 | +} from "./model"; | |
| 57 | + | |
| 58 | +export interface ToolDescriptor { | |
| 59 | + toolPath: string[]; | |
| 60 | + capabilityId: CapabilityId; | |
| 61 | + title?: string; | |
| 62 | + summary?: string; | |
| 63 | + effect: Capability["semantics"]["effect"]; | |
| 64 | + interaction: { | |
| 65 | + mayRequireApproval: boolean; | |
| 66 | + mayElicit: boolean; | |
| 67 | + }; | |
| 68 | + callShapeId: ShapeSymbolId; | |
| 69 | + resultShapeId?: ShapeSymbolId; | |
| 70 | + responseSetId: ResponseSetId; | |
| 71 | + diagnosticCounts: { | |
| 72 | + warning: number; | |
| 73 | + error: number; | |
| 74 | + }; | |
| 75 | +} | |
| 76 | + | |
| 77 | +export interface ToolSearchDoc { | |
| 78 | + capabilityId: CapabilityId; | |
| 79 | + toolPath: string[]; | |
| 80 | + title?: string; | |
| 81 | + summary?: string; | |
| 82 | + tags?: string[]; | |
| 83 | + protocolHints: string[]; | |
| 84 | + authHints: string[]; | |
| 85 | + effect: Capability["semantics"]["effect"]; | |
| 86 | +} | |
| 87 | + | |
| 88 | +export interface CapabilitySummaryView { | |
| 89 | + capabilityId: CapabilityId; | |
| 90 | + toolPath: string[]; | |
| 91 | + summary?: string; | |
| 92 | + executableIds: ExecutableId[]; | |
| 93 | + auth: Capability["auth"]; | |
| 94 | + interaction: InteractionSpec; | |
| 95 | + callShapeId: ShapeSymbolId; | |
| 96 | + resultShapeId?: ShapeSymbolId; | |
| 97 | + responseSetId: ResponseSetId; | |
| 98 | + diagnosticIds?: DiagnosticId[]; | |
| 99 | +} | |
| 100 | + | |
| 101 | +export interface SymbolShallowView { | |
| 102 | + symbolId: SymbolId; | |
| 103 | + kind: IrSymbol["kind"]; | |
| 104 | + title?: string; | |
| 105 | + summary?: string; | |
| 106 | + edges: Array<{ label: string; targetId: SymbolId }>; | |
| 107 | +} | |
| 108 | + | |
| 109 | +export interface SymbolExpandedView { | |
| 110 | + symbolId: SymbolId; | |
| 111 | + symbol: IrSymbol; | |
| 112 | + diagnostics?: ImportDiagnostic[]; | |
| 113 | +} | |
| 114 | + | |
| 115 | +export interface InvocationPlan { | |
| 116 | + capabilityId: CapabilityId; | |
| 117 | + executableId: ExecutableId; | |
| 118 | + connectionBindingId: string; | |
| 119 | + resolvedScopeChain: ScopeId[]; | |
| 120 | + resolvedEndpoint?: string; | |
| 121 | + resolvedAuth?: unknown; | |
| 122 | + resolvedInteraction?: { | |
| 123 | + requiresApproval: boolean; | |
| 124 | + mayPauseForElicitation: boolean; | |
| 125 | + resumable: boolean; | |
| 126 | + }; | |
| 127 | + requestPlan: unknown; | |
| 128 | + provenance: CatalogV1["capabilities"][CapabilityId]["provenance"]; | |
| 129 | +} | |
| 130 | + | |
| 131 | +export interface ProjectedCatalog { | |
| 132 | + catalog: CatalogV1; | |
| 133 | + toolDescriptors: Record<CapabilityId, ToolDescriptor>; | |
| 134 | + searchDocs: Record<CapabilityId, ToolSearchDoc>; | |
| 135 | + capabilityViews: Record<CapabilityId, CapabilitySummaryView>; | |
| 136 | +} | |
| 137 | + | |
| 138 | +export interface CatalogInvariantViolation { | |
| 139 | + code: | |
| 140 | + | "missing_symbol_provenance" | |
| 141 | + | "missing_entity_provenance" | |
| 142 | + | "missing_document" | |
| 143 | + | "missing_provenance_document" | |
| 144 | + | "missing_resource_context" | |
| 145 | + | "missing_reference_target" | |
| 146 | + | "missing_unresolved_ref_diagnostic" | |
| 147 | + | "missing_executable" | |
| 148 | + | "missing_response_set" | |
| 149 | + | "missing_scope" | |
| 150 | + | "missing_service_scope" | |
| 151 | + | "invalid_preferred_executable" | |
| 152 | + | "missing_projection_synthetic_marker"; | |
| 153 | + message: string; | |
| 154 | + entityId?: string; | |
| 155 | +} | |
| 156 | + | |
| 157 | +export type DecodeCatalogError = Error; | |
| 158 | + | |
| 159 | +type MutableScopeDefaults = { | |
| 160 | + servers?: ScopeDefaults["servers"]; | |
| 161 | + auth?: ScopeDefaults["auth"]; | |
| 162 | + parameterIds?: ParameterSymbolId[]; | |
| 163 | + headerIds?: HeaderSymbolId[]; | |
| 164 | + variables?: Record<string, string>; | |
| 165 | +}; | |
| 166 | + | |
| 167 | +type CatalogSnapshotV1 = { | |
| 168 | + version: "ir.v1.snapshot"; | |
| 169 | + import: ImportMetadata; | |
| 170 | + catalog: CatalogV1; | |
| 171 | +}; | |
| 172 | + | |
| 173 | +const decodeCatalogSync = Schema.decodeUnknownSync( | |
| 174 | + CatalogV1Schema as Schema.Schema<CatalogV1, unknown, never>, | |
| 175 | +); | |
| 176 | +const decodeCatalogSnapshotSync = Schema.decodeUnknownSync( | |
| 177 | + CatalogSnapshotV1Schema as Schema.Schema<CatalogSnapshotV1, unknown, never>, | |
| 178 | +); | |
| 179 | +const decodeCatalogFragmentSync = Schema.decodeUnknownSync( | |
| 180 | + CatalogFragmentV1Schema as Schema.Schema<CatalogFragmentV1, unknown, never>, | |
| 181 | +); | |
| 182 | + | |
| 183 | +const stableStringify = (value: unknown): string => { | |
| 184 | + if (value === null || typeof value !== "object") { | |
| 185 | + return JSON.stringify(value); | |
| 186 | + } | |
| 187 | + | |
| 188 | + if (Array.isArray(value)) { | |
| 189 | + return `[${value.map((entry) => stableStringify(entry)).join(",")}]`; | |
| 190 | + } | |
| 191 | + | |
| 192 | + const entries = Object.entries(value as Record<string, unknown>) | |
| 193 | + .sort(([left], [right]) => left.localeCompare(right)) | |
| 194 | + .map(([key, entry]) => `${JSON.stringify(key)}:${stableStringify(entry)}`); | |
| 195 | + return `{${entries.join(",")}}`; | |
| 196 | +}; | |
| 197 | + | |
| 198 | +const stableHash = (value: unknown): string => sha256Hex(stableStringify(value)).slice(0, 16); | |
| 199 | + | |
| 200 | +const unique = <T>(values: readonly T[]): T[] => [...new Set(values)]; | |
| 201 | + | |
| 202 | +const cloneCatalog = (catalog: CatalogV1): CatalogV1 => ({ | |
| 203 | + version: catalog.version, | |
| 204 | + documents: { ...catalog.documents }, | |
| 205 | + resources: { ...catalog.resources }, | |
| 206 | + scopes: { ...catalog.scopes }, | |
| 207 | + symbols: { ...catalog.symbols }, | |
| 208 | + capabilities: { ...catalog.capabilities }, | |
| 209 | + executables: { ...catalog.executables }, | |
| 210 | + responseSets: { ...catalog.responseSets }, | |
| 211 | + diagnostics: { ...catalog.diagnostics }, | |
| 212 | +}); | |
| 213 | + | |
| 214 | +const mutableRecord = <K extends string, V>(value: Readonly<Record<K, V>>): Record<K, V> => | |
| 215 | + value as unknown as Record<K, V>; | |
| 216 | + | |
| 217 | +const createStableEntityId = <A extends string>( | |
| 218 | + make: (value: string) => A, | |
| 219 | + prefix: string, | |
| 220 | + value: unknown, | |
| 221 | +): A => make(`${prefix}_${stableHash(value)}`); | |
| 222 | + | |
| 223 | +const emptyCatalog = (): CatalogV1 => ({ | |
| 224 | + version: "ir.v1", | |
| 225 | + documents: {}, | |
| 226 | + resources: {}, | |
| 227 | + scopes: {}, | |
| 228 | + symbols: {}, | |
| 229 | + capabilities: {}, | |
| 230 | + executables: {}, | |
| 231 | + responseSets: {}, | |
| 232 | + diagnostics: {}, | |
| 233 | +}); | |
| 234 | + | |
| 235 | +const authHintStrings = (catalog: CatalogV1, auth: Capability["auth"]): string[] => { | |
| 236 | + switch (auth.kind) { | |
| 237 | + case "none": | |
| 238 | + return ["none"]; | |
| 239 | + case "scheme": { | |
| 240 | + const scheme = catalog.symbols[auth.schemeId]; | |
| 241 | + if (!scheme || scheme.kind !== "securityScheme") { | |
| 242 | + return ["unknown"]; | |
| 243 | + } | |
| 244 | + return [ | |
| 245 | + scheme.schemeType, | |
| 246 | + ...((auth.scopes ?? []).map((scope: string) => `scope:${scope}`)), | |
| 247 | + ]; | |
| 248 | + } | |
| 249 | + case "allOf": | |
| 250 | + case "anyOf": | |
| 251 | + return unique(auth.items.flatMap((item: Capability["auth"]) => authHintStrings(catalog, item))); | |
| 252 | + } | |
| 253 | +}; | |
| 254 | + | |
| 255 | +const docsSummary = (docs: DocumentationBlock | undefined): string | undefined => | |
| 256 | + docs?.summary ?? docs?.description; | |
| 257 | + | |
| 258 | +const getShape = (catalog: CatalogV1, shapeId: ShapeSymbolId): ShapeSymbol | undefined => { | |
| 259 | + const symbol = catalog.symbols[shapeId]; | |
| 260 | + return symbol && symbol.kind === "shape" ? symbol : undefined; | |
| 261 | +}; | |
| 262 | + | |
| 263 | +const getParameter = ( | |
| 264 | + catalog: CatalogV1, | |
| 265 | + parameterId: ParameterSymbolId, | |
| 266 | +): ParameterSymbol | undefined => { | |
| 267 | + const symbol = catalog.symbols[parameterId]; | |
| 268 | + return symbol && symbol.kind === "parameter" ? symbol : undefined; | |
| 269 | +}; | |
| 270 | + | |
| 271 | +const getResponseSymbol = ( | |
| 272 | + catalog: CatalogV1, | |
| 273 | + responseId: ResponseSymbolId, | |
| 274 | +): ResponseSymbol | undefined => { | |
| 275 | + const symbol = catalog.symbols[responseId]; | |
| 276 | + return symbol && symbol.kind === "response" ? symbol : undefined; | |
| 277 | +}; | |
| 278 | + | |
| 279 | +const getHeader = ( | |
| 280 | + catalog: CatalogV1, | |
| 281 | + headerId: HeaderSymbolId, | |
| 282 | +): HeaderSymbol | undefined => { | |
| 283 | + const symbol = catalog.symbols[headerId]; | |
| 284 | + return symbol && symbol.kind === "header" ? symbol : undefined; | |
| 285 | +}; | |
| 286 | + | |
| 287 | +const scopeChain = (catalog: CatalogV1, scopeId: ScopeId): Scope[] => { | |
| 288 | + const chain: Scope[] = []; | |
| 289 | + let currentId: ScopeId | undefined = scopeId; | |
| 290 | + | |
| 291 | + while (currentId) { | |
| 292 | + const scope: Scope | undefined = catalog.scopes[currentId]; | |
| 293 | + if (!scope) { | |
| 294 | + break; | |
| 295 | + } | |
| 296 | + chain.unshift(scope); | |
| 297 | + currentId = scope.parentId; | |
| 298 | + } | |
| 299 | + | |
| 300 | + return chain; | |
| 301 | +}; | |
| 302 | + | |
| 303 | +const mergeScopeDefaults = (chain: readonly Scope[]): ScopeDefaults => { | |
| 304 | + const merged: MutableScopeDefaults = {}; | |
| 305 | + | |
| 306 | + for (const scope of chain) { | |
| 307 | + const defaults = scope.defaults; | |
| 308 | + if (!defaults) { | |
| 309 | + continue; | |
| 310 | + } | |
| 311 | + | |
| 312 | + if (defaults.servers) { | |
| 313 | + merged.servers = [...defaults.servers]; | |
| 314 | + } | |
| 315 | + | |
| 316 | + if (defaults.auth) { | |
| 317 | + merged.auth = defaults.auth; | |
| 318 | + } | |
| 319 | + | |
| 320 | + if (defaults.parameterIds) { | |
| 321 | + merged.parameterIds = unique([ | |
| 322 | + ...(merged.parameterIds ?? []), | |
| 323 | + ...defaults.parameterIds, | |
| 324 | + ]); | |
| 325 | + } | |
| 326 | + | |
| 327 | + if (defaults.headerIds) { | |
| 328 | + merged.headerIds = unique([ | |
| 329 | + ...(merged.headerIds ?? []), | |
| 330 | + ...defaults.headerIds, | |
| 331 | + ]); | |
| 332 | + } | |
| 333 | + | |
| 334 | + if (defaults.variables) { | |
| 335 | + merged.variables = { | |
| 336 | + ...(merged.variables ?? {}), | |
| 337 | + ...defaults.variables, | |
| 338 | + }; | |
| 339 | + } | |
| 340 | + } | |
| 341 | + | |
| 342 | + return merged as ScopeDefaults; | |
| 343 | +}; | |
| 344 | + | |
| 345 | +const isJsonMediaType = (mediaType: string): boolean => { | |
| 346 | + const normalized = mediaType.trim().toLowerCase(); | |
| 347 | + return normalized === "application/json" | |
| 348 | + || normalized.endsWith("+json") | |
| 349 | + || normalized === "text/json"; | |
| 350 | +}; | |
| 351 | + | |
| 352 | +const createDiagnostic = ( | |
| 353 | + catalog: CatalogV1, | |
| 354 | + input: Omit<ImportDiagnostic, "id"> & { idSeed: unknown }, | |
| 355 | +): DiagnosticId => { | |
| 356 | + const diagnosticId = createStableEntityId( | |
| 357 | + DiagnosticIdSchema.make, | |
| 358 | + "diag", | |
| 359 | + input.idSeed, | |
| 360 | + ); | |
| 361 | + | |
| 362 | + mutableRecord(catalog.diagnostics)[diagnosticId] = { | |
| 363 | + id: diagnosticId, | |
| 364 | + level: input.level, | |
| 365 | + code: input.code, | |
| 366 | + message: input.message, | |
| 367 | + ...(input.relatedSymbolIds ? { relatedSymbolIds: input.relatedSymbolIds } : {}), | |
| 368 | + provenance: input.provenance, | |
| 369 | + }; | |
| 370 | + | |
| 371 | + return diagnosticId; | |
| 372 | +}; | |
| 373 | + | |
| 374 | +const ensureProjectionResource = ( | |
| 375 | + catalog: CatalogV1, | |
| 376 | + capability: Capability, | |
| 377 | +): ResourceId => { | |
| 378 | + const resourceId = createStableEntityId( | |
| 379 | + ResourceIdSchema.make, | |
| 380 | + "res", | |
| 381 | + { | |
| 382 | + kind: "projection", | |
| 383 | + capabilityId: capability.id, | |
| 384 | + }, | |
| 385 | + ); | |
| 386 | + | |
| 387 | + if (!catalog.resources[resourceId]) { | |
| 388 | + const serviceScope = catalog.scopes[capability.serviceScopeId]; | |
| 389 | + const documentId = serviceScope?.provenance[0]?.documentId | |
| 390 | + ?? DocumentIdSchema.make(`doc_projection_${stableHash(capability.id)}`); | |
| 391 | + | |
| 392 | + if (!catalog.documents[documentId]) { | |
| 393 | + mutableRecord(catalog.documents)[documentId] = { | |
| 394 | + id: documentId, | |
| 395 | + kind: "custom", | |
| 396 | + title: `Projection resource for ${capability.id}`, | |
| 397 | + fetchedAt: new Date(0).toISOString(), | |
| 398 | + rawRef: `synthetic://projection/${capability.id}`, | |
| 399 | + }; | |
| 400 | + } | |
| 401 | + | |
| 402 | + mutableRecord(catalog.resources)[resourceId] = { | |
| 403 | + id: resourceId, | |
| 404 | + documentId, | |
| 405 | + canonicalUri: `synthetic://projection/${capability.id}`, | |
| 406 | + baseUri: `synthetic://projection/${capability.id}`, | |
| 407 | + anchors: {}, | |
| 408 | + dynamicAnchors: {}, | |
| 409 | + synthetic: true, | |
| 410 | + provenance: capability.provenance, | |
| 411 | + }; | |
| 412 | + | |
| 413 | + createDiagnostic(catalog, { | |
| 414 | + idSeed: { | |
| 415 | + code: "synthetic_resource_context_created", | |
| 416 | + capabilityId: capability.id, | |
| 417 | + }, | |
| 418 | + level: "info", | |
| 419 | + code: "synthetic_resource_context_created", | |
| 420 | + message: `Created synthetic projection resource for ${capability.id}`, | |
| 421 | + provenance: capability.provenance, | |
| 422 | + }); | |
| 423 | + } | |
| 424 | + | |
| 425 | + return resourceId; | |
| 426 | +}; | |
| 427 | + | |
| 428 | +const createSyntheticShape = ( | |
| 429 | + catalog: CatalogV1, | |
| 430 | + input: { | |
| 431 | + capability: Capability; | |
| 432 | + label: string; | |
| 433 | + node: ShapeSymbol["node"]; | |
| 434 | + title?: string; | |
| 435 | + docs?: DocumentationBlock; | |
| 436 | + diagnostic?: | |
| 437 | + | { | |
| 438 | + level: ImportDiagnostic["level"]; | |
| 439 | + code: ImportDiagnostic["code"]; | |
| 440 | + message: string; | |
| 441 | + relatedSymbolIds?: SymbolId[]; | |
| 442 | + } | |
| 443 | + | undefined; | |
| 444 | + }, | |
| 445 | +): ShapeSymbolId => { | |
| 446 | + const resourceId = ensureProjectionResource(catalog, input.capability); | |
| 447 | + const shapeId = createStableEntityId( | |
| 448 | + ShapeSymbolIdSchema.make, | |
| 449 | + "shape", | |
| 450 | + { | |
| 451 | + capabilityId: input.capability.id, | |
| 452 | + label: input.label, | |
| 453 | + node: input.node, | |
| 454 | + }, | |
| 455 | + ); | |
| 456 | + | |
| 457 | + if (!catalog.symbols[shapeId]) { | |
| 458 | + const diagnostics = input.diagnostic | |
| 459 | + ? [ | |
| 460 | + createDiagnostic(catalog, { | |
| 461 | + idSeed: { | |
| 462 | + code: input.diagnostic.code, | |
| 463 | + shapeId, | |
| 464 | + }, | |
| 465 | + level: input.diagnostic.level, | |
| 466 | + code: input.diagnostic.code, | |
| 467 | + message: input.diagnostic.message, | |
| 468 | + ...(input.diagnostic.relatedSymbolIds | |
| 469 | + ? { relatedSymbolIds: input.diagnostic.relatedSymbolIds } | |
| 470 | + : {}), | |
| 471 | + provenance: input.capability.provenance, | |
| 472 | + }), | |
| 473 | + ] | |
| 474 | + : undefined; | |
| 475 | + | |
| 476 | + mutableRecord(catalog.symbols)[shapeId] = { | |
| 477 | + id: shapeId, | |
| 478 | + kind: "shape", | |
| 479 | + resourceId, | |
| 480 | + ...(input.title ? { title: input.title } : {}), | |
| 481 | + ...(input.docs ? { docs: input.docs } : {}), | |
| 482 | + node: input.node, | |
| 483 | + synthetic: true, | |
| 484 | + provenance: input.capability.provenance, | |
| 485 | + ...(diagnostics ? { diagnosticIds: diagnostics } : {}), | |
| 486 | + }; | |
| 487 | + } | |
| 488 | + | |
| 489 | + return shapeId; | |
| 490 | +}; | |
| 491 | + | |
| 492 | +const contentShapeId = ( | |
| 493 | + catalog: CatalogV1, | |
| 494 | + capability: Capability, | |
| 495 | + contents: readonly ContentSpec[] | undefined, | |
| 496 | + label: string, | |
| 497 | +): ShapeSymbolId | undefined => { | |
| 498 | + const shapeIds = unique( | |
| 499 | + (contents ?? []) | |
| 500 | + .map((content) => content.shapeId) | |
| 501 | + .filter((shapeId): shapeId is ShapeSymbolId => shapeId !== undefined), | |
| 502 | + ); | |
| 503 | + | |
| 504 | + if (shapeIds.length === 0) { | |
| 505 | + return undefined; | |
| 506 | + } | |
| 507 | + | |
| 508 | + if (shapeIds.length === 1) { | |
| 509 | + return shapeIds[0]; | |
| 510 | + } | |
| 511 | + | |
| 512 | + return createSyntheticShape(catalog, { | |
| 513 | + capability, | |
| 514 | + label, | |
| 515 | + title: `${capability.surface.title ?? capability.id} content union`, | |
| 516 | + node: { | |
| 517 | + type: "anyOf", | |
| 518 | + items: shapeIds, | |
| 519 | + }, | |
| 520 | + diagnostic: { | |
| 521 | + level: "warning", | |
| 522 | + code: "projection_result_shape_synthesized", | |
| 523 | + message: `Synthesized content union for ${capability.id}`, | |
| 524 | + relatedSymbolIds: shapeIds, | |
| 525 | + }, | |
| 526 | + }); | |
| 527 | +}; | |
| 528 | + | |
| 529 | +const parameterShapeId = ( | |
| 530 | + catalog: CatalogV1, | |
| 531 | + capability: Capability, | |
| 532 | + parameter: ParameterSymbol, | |
| 533 | +): ShapeSymbolId => { | |
| 534 | + if (parameter.schemaShapeId) { | |
| 535 | + return parameter.schemaShapeId; | |
| 536 | + } | |
| 537 | + | |
| 538 | + const contentShape = contentShapeId( | |
| 539 | + catalog, | |
| 540 | + capability, | |
| 541 | + parameter.content, | |
| 542 | + `parameter:${parameter.id}`, | |
| 543 | + ); | |
| 544 | + if (contentShape) { | |
| 545 | + return contentShape; | |
| 546 | + } | |
| 547 | + | |
| 548 | + return createSyntheticShape(catalog, { | |
| 549 | + capability, | |
| 550 | + label: `parameter:${parameter.id}:unknown`, | |
| 551 | + title: parameter.name, | |
| 552 | + docs: parameter.docs, | |
| 553 | + node: { | |
| 554 | + type: "unknown", | |
| 555 | + reason: `Missing shape for parameter ${parameter.id}`, | |
| 556 | + }, | |
| 557 | + diagnostic: { | |
| 558 | + level: "warning", | |
| 559 | + code: "projection_call_shape_synthesized", | |
| 560 | + message: `Missing parameter shape for ${parameter.name}; using unknown`, | |
| 561 | + relatedSymbolIds: [parameter.id], | |
| 562 | + }, | |
| 563 | + }); | |
| 564 | +}; | |
| 565 | + | |
| 566 | +const requestBodyShapeId = ( | |
| 567 | + catalog: CatalogV1, | |
| 568 | + capability: Capability, | |
| 569 | + requestBodyId: RequestBodySymbolId | undefined, | |
| 570 | +): ShapeSymbolId | undefined => { | |
| 571 | + if (!requestBodyId) { | |
| 572 | + return undefined; | |
| 573 | + } | |
| 574 | + | |
| 575 | + const requestBody = catalog.symbols[requestBodyId]; | |
| 576 | + if (!requestBody || requestBody.kind !== "requestBody") { | |
| 577 | + return undefined; | |
| 578 | + } | |
| 579 | + | |
| 580 | + return contentShapeId( | |
| 581 | + catalog, | |
| 582 | + capability, | |
| 583 | + requestBody.contents, | |
| 584 | + `requestBody:${requestBody.id}`, | |
| 585 | + ); | |
| 586 | +}; | |
| 587 | + | |
| 588 | +const chooseExecutable = (catalog: CatalogV1, capability: Capability): Executable => { | |
| 589 | + const preferredId = capability.preferredExecutableId; | |
| 590 | + if (preferredId) { | |
| 591 | + const preferred = catalog.executables[preferredId]; | |
| 592 | + if (preferred) { | |
| 593 | + return preferred; | |
| 594 | + } | |
| 595 | + } | |
| 596 | + | |
| 597 | + const executables = capability.executableIds | |
| 598 | + .map((executableId) => catalog.executables[executableId]) | |
| 599 | + .filter((executable): executable is Executable => executable !== undefined); | |
| 600 | + | |
| 601 | + if (executables.length === 0) { | |
| 602 | + throw new Error(`Capability ${capability.id} has no executables`); | |
| 603 | + } | |
| 604 | + | |
| 605 | + return executables[0]; | |
| 606 | +}; | |
| 607 | + | |
| 608 | +const scoreStatusMatch = (match: StatusMatch): number => { | |
| 609 | + switch (match.kind) { | |
| 610 | + case "exact": | |
| 611 | + return match.status === 200 ? 100 : match.status >= 200 && match.status < 300 ? 80 : 10; | |
| 612 | + case "range": | |
| 613 | + return match.value === "2XX" ? 60 : 5; | |
| 614 | + case "default": | |
| 615 | + return 40; | |
| 616 | + } | |
| 617 | +}; | |
| 618 | + | |
| 619 | +const responseContentCandidates = ( | |
| 620 | + response: ResponseSymbol, | |
| 621 | +): Array<{ mediaType: string; shapeId: ShapeSymbolId }> => | |
| 622 | + (response.contents ?? []) | |
| 623 | + .flatMap((content) => | |
| 624 | + content.shapeId ? [{ mediaType: content.mediaType, shapeId: content.shapeId }] : [], | |
| 625 | + ); | |
| 626 | + | |
| 627 | +type RankedResponseVariantEntry = { | |
| 628 | + variant: ResponseSet["variants"][number]; | |
| 629 | + response: ResponseSymbol; | |
| 630 | + score: number; | |
| 631 | +}; | |
| 632 | + | |
| 633 | +const responseVariantEntries = ( | |
| 634 | + catalog: CatalogV1, | |
| 635 | + responseSet: ResponseSet, | |
| 636 | +): RankedResponseVariantEntry[] => | |
| 637 | + [...responseSet.variants] | |
| 638 | + .map((variant) => { | |
| 639 | + const response = getResponseSymbol(catalog, variant.responseId); | |
| 640 | + return response | |
| 641 | + ? { | |
| 642 | + variant, | |
| 643 | + response, | |
| 644 | + score: scoreStatusMatch(variant.match), | |
| 645 | + } | |
| 646 | + : null; | |
| 647 | + }) | |
| 648 | + .filter((entry): entry is RankedResponseVariantEntry => entry !== null); | |
| 649 | + | |
| 650 | +const isSuccessStatusMatch = (match: StatusMatch): boolean => { | |
| 651 | + switch (match.kind) { | |
| 652 | + case "exact": | |
| 653 | + return match.status >= 200 && match.status < 300; | |
| 654 | + case "range": | |
| 655 | + return match.value === "2XX"; | |
| 656 | + case "default": | |
| 657 | + return false; | |
| 658 | + } | |
| 659 | +}; | |
| 660 | + | |
| 661 | +const contentShapeIdFromResponseEntries = ( | |
| 662 | + catalog: CatalogV1, | |
| 663 | + capability: Capability, | |
| 664 | + label: string, | |
| 665 | + title: string, | |
| 666 | + entries: readonly RankedResponseVariantEntry[], | |
| 667 | +): ShapeSymbolId | undefined => { | |
| 668 | + const jsonCandidates = entries.flatMap(({ response }) => | |
| 669 | + responseContentCandidates(response).filter((candidate) => isJsonMediaType(candidate.mediaType)), | |
| 670 | + ); | |
| 671 | + | |
| 672 | + if (jsonCandidates.length > 0) { | |
| 673 | + return contentShapeId( | |
| 674 | + catalog, | |
| 675 | + capability, | |
| 676 | + jsonCandidates.map((candidate) => ({ | |
| 677 | + mediaType: candidate.mediaType, | |
| 678 | + shapeId: candidate.shapeId, | |
| 679 | + })), | |
| 680 | + `${label}:json`, | |
| 681 | + ); | |
| 682 | + } | |
| 683 | + | |
| 684 | + const fallbackShapeIds = unique( | |
| 685 | + entries.flatMap(({ response }) => | |
| 686 | + responseContentCandidates(response).map((candidate) => candidate.shapeId), | |
| 687 | + ), | |
| 688 | + ); | |
| 689 | + | |
| 690 | + if (fallbackShapeIds.length === 0) { | |
| 691 | + return undefined; | |
| 692 | + } | |
| 693 | + | |
| 694 | + if (fallbackShapeIds.length === 1) { | |
| 695 | + return fallbackShapeIds[0]; | |
| 696 | + } | |
| 697 | + | |
| 698 | + return createSyntheticShape(catalog, { | |
| 699 | + capability, | |
| 700 | + label: `${label}:union`, | |
| 701 | + title, | |
| 702 | + node: { | |
| 703 | + type: "anyOf", | |
| 704 | + items: fallbackShapeIds, | |
| 705 | + }, | |
| 706 | + diagnostic: { | |
| 707 | + level: "warning", | |
| 708 | + code: "multi_response_union_synthesized", | |
| 709 | + message: `Synthesized response union for ${capability.id}`, | |
| 710 | + relatedSymbolIds: fallbackShapeIds, | |
| 711 | + }, | |
| 712 | + }); | |
| 713 | +}; | |
| 714 | + | |
| 715 | +const projectResultShapeFromResponses = ( | |
| 716 | + catalog: CatalogV1, | |
| 717 | + capability: Capability, | |
| 718 | + responseSet: ResponseSet, | |
| 719 | +): ShapeSymbolId | undefined => | |
| 720 | + contentShapeIdFromResponseEntries( | |
| 721 | + catalog, | |
| 722 | + capability, | |
| 723 | + `responseSet:${responseSet.id}`, | |
| 724 | + `${capability.surface.title ?? capability.id} result`, | |
| 725 | + responseVariantEntries(catalog, responseSet).filter(({ variant }) => isSuccessStatusMatch(variant.match)), | |
| 726 | + ); | |
| 727 | + | |
| 728 | +const projectErrorShapeFromResponses = ( | |
| 729 | + catalog: CatalogV1, | |
| 730 | + capability: Capability, | |
| 731 | + responseSet: ResponseSet, | |
| 732 | +): ShapeSymbolId | undefined => | |
| 733 | + contentShapeIdFromResponseEntries( | |
| 734 | + catalog, | |
| 735 | + capability, | |
| 736 | + `responseSet:${responseSet.id}:error`, | |
| 737 | + `${capability.surface.title ?? capability.id} error`, | |
| 738 | + responseVariantEntries(catalog, responseSet).filter(({ variant }) => !isSuccessStatusMatch(variant.match)), | |
| 739 | + ); | |
| 740 | + | |
| 741 | +const scalarShape = ( | |
| 742 | + catalog: CatalogV1, | |
| 743 | + capability: Capability, | |
| 744 | + input: { | |
| 745 | + label: string; | |
| 746 | + title: string; | |
| 747 | + scalar: "string" | "number" | "integer" | "boolean" | "null" | "bytes"; | |
| 748 | + }, | |
| 749 | +): ShapeSymbolId => | |
| 750 | + createSyntheticShape(catalog, { | |
| 751 | + capability, | |
| 752 | + label: input.label, | |
| 753 | + title: input.title, | |
| 754 | + node: { | |
| 755 | + type: "scalar", | |
| 756 | + scalar: input.scalar, | |
| 757 | + }, | |
| 758 | + }); | |
| 759 | + | |
| 760 | +const constNullShape = ( | |
| 761 | + catalog: CatalogV1, | |
| 762 | + capability: Capability, | |
| 763 | + label: string, | |
| 764 | +): ShapeSymbolId => | |
| 765 | + createSyntheticShape(catalog, { | |
| 766 | + capability, | |
| 767 | + label, | |
| 768 | + title: "null", | |
| 769 | + node: { | |
| 770 | + type: "const", | |
| 771 | + value: null, | |
| 772 | + }, | |
| 773 | + }); | |
| 774 | + | |
| 775 | +const unknownShape = ( | |
| 776 | + catalog: CatalogV1, | |
| 777 | + capability: Capability, | |
| 778 | + input: { | |
| 779 | + label: string; | |
| 780 | + title: string; | |
| 781 | + reason: string; | |
| 782 | + }, | |
| 783 | +): ShapeSymbolId => | |
| 784 | + createSyntheticShape(catalog, { | |
| 785 | + capability, | |
| 786 | + label: input.label, | |
| 787 | + title: input.title, | |
| 788 | + node: { | |
| 789 | + type: "unknown", | |
| 790 | + reason: input.reason, | |
| 791 | + }, | |
| 792 | + }); | |
| 793 | + | |
| 794 | +const nullableShape = ( | |
| 795 | + catalog: CatalogV1, | |
| 796 | + capability: Capability, | |
| 797 | + input: { | |
| 798 | + label: string; | |
| 799 | + title: string; | |
| 800 | + baseShapeId: ShapeSymbolId; | |
| 801 | + }, | |
| 802 | +): ShapeSymbolId => { | |
| 803 | + const nullShapeId = constNullShape(catalog, capability, `${input.label}:null`); | |
| 804 | + if (input.baseShapeId === nullShapeId) { | |
| 805 | + return nullShapeId; | |
| 806 | + } | |
| 807 | + | |
| 808 | + return createSyntheticShape(catalog, { | |
| 809 | + capability, | |
| 810 | + label: `${input.label}:nullable`, | |
| 811 | + title: input.title, | |
| 812 | + node: { | |
| 813 | + type: "anyOf", | |
| 814 | + items: unique([input.baseShapeId, nullShapeId]), | |
| 815 | + }, | |
| 816 | + }); | |
| 817 | +}; | |
| 818 | + | |
| 819 | +const headersShape = ( | |
| 820 | + catalog: CatalogV1, | |
| 821 | + capability: Capability, | |
| 822 | + label: string, | |
| 823 | +): ShapeSymbolId => { | |
| 824 | + const valueShapeId = scalarShape(catalog, capability, { | |
| 825 | + label: `${label}:value`, | |
| 826 | + title: "Header value", | |
| 827 | + scalar: "string", | |
| 828 | + }); | |
| 829 | + | |
| 830 | + return createSyntheticShape(catalog, { | |
| 831 | + capability, | |
| 832 | + label, | |
| 833 | + title: "Response headers", | |
| 834 | + node: { | |
| 835 | + type: "object", | |
| 836 | + fields: {}, | |
| 837 | + additionalProperties: valueShapeId, | |
| 838 | + }, | |
| 839 | + }); | |
| 840 | +}; | |
| 841 | + | |
| 842 | +const shapeFieldShapeId = ( | |
| 843 | + catalog: CatalogV1, | |
| 844 | + shapeId: ShapeSymbolId | undefined, | |
| 845 | + fieldName: string, | |
| 846 | + seen = new Set<ShapeSymbolId>(), | |
| 847 | +): ShapeSymbolId | undefined => { | |
| 848 | + if (!shapeId || seen.has(shapeId)) { | |
| 849 | + return undefined; | |
| 850 | + } | |
| 851 | + | |
| 852 | + const shape = getShape(catalog, shapeId); | |
| 853 | + if (!shape) { | |
| 854 | + return undefined; | |
| 855 | + } | |
| 856 | + | |
| 857 | + const nextSeen = new Set(seen); | |
| 858 | + nextSeen.add(shapeId); | |
| 859 | + | |
| 860 | + switch (shape.node.type) { | |
| 861 | + case "ref": | |
| 862 | + return shapeFieldShapeId(catalog, shape.node.target, fieldName, nextSeen); | |
| 863 | + case "object": | |
| 864 | + return shape.node.fields[fieldName]?.shapeId; | |
| 865 | + case "allOf": | |
| 866 | + for (const item of shape.node.items) { | |
| 867 | + const found = shapeFieldShapeId(catalog, item, fieldName, nextSeen); | |
| 868 | + if (found) { | |
| 869 | + return found; | |
| 870 | + } | |
| 871 | + } | |
| 872 | + return undefined; | |
| 873 | + default: | |
| 874 | + return undefined; | |
| 875 | + } | |
| 876 | +}; | |
| 877 | + | |
| 878 | +const projectExecutionResultShape = ( | |
| 879 | + catalog: CatalogV1, | |
| 880 | + capability: Capability, | |
| 881 | + executable: Executable, | |
| 882 | + responseSet: ResponseSet, | |
| 883 | +): ShapeSymbolId => { | |
| 884 | + const genericDataShape = unknownShape(catalog, capability, { | |
| 885 | + label: `executionResult:${capability.id}:data:unknown`, | |
| 886 | + title: "Response data", | |
| 887 | + reason: `Execution result data for ${capability.id} is not statically known`, | |
| 888 | + }); | |
| 889 | + const genericErrorShape = unknownShape(catalog, capability, { | |
| 890 | + label: `executionResult:${capability.id}:error:unknown`, | |
| 891 | + title: "Response error", | |
| 892 | + reason: `Execution result error for ${capability.id} is not statically known`, | |
| 893 | + }); | |
| 894 | + const genericHeadersShape = headersShape( | |
| 895 | + catalog, | |
| 896 | + capability, | |
| 897 | + `executionResult:${capability.id}:headers`, | |
| 898 | + ); | |
| 899 | + const genericStatusShape = scalarShape(catalog, capability, { | |
| 900 | + label: `executionResult:${capability.id}:status`, | |
| 901 | + title: "Response status", | |
| 902 | + scalar: "integer", | |
| 903 | + }); | |
| 904 | + | |
| 905 | + let dataShapeId: ShapeSymbolId | undefined; | |
| 906 | + let errorShapeId: ShapeSymbolId | undefined; | |
| 907 | + let headersShapeId: ShapeSymbolId = genericHeadersShape; | |
| 908 | + let statusShapeId: ShapeSymbolId = genericStatusShape; | |
| 909 | + | |
| 910 | + switch (executable.protocol) { | |
| 911 | + case "http": | |
| 912 | + dataShapeId = projectResultShapeFromResponses(catalog, capability, responseSet); | |
| 913 | + errorShapeId = projectErrorShapeFromResponses(catalog, capability, responseSet); | |
| 914 | + break; | |
| 915 | + case "graphql": | |
| 916 | + dataShapeId = | |
| 917 | + shapeFieldShapeId(catalog, executable.resultShapeId, "data") | |
| 918 | + ?? shapeFieldShapeId(catalog, executable.resultShapeId, "body") | |
| 919 | + ?? executable.resultShapeId; | |
| 920 | + errorShapeId = | |
| 921 | + shapeFieldShapeId(catalog, executable.resultShapeId, "error") | |
| 922 | + ?? shapeFieldShapeId(catalog, executable.resultShapeId, "errors"); | |
| 923 | + headersShapeId = | |
| 924 | + shapeFieldShapeId(catalog, executable.resultShapeId, "headers") | |
| 925 | + ?? genericHeadersShape; | |
| 926 | + statusShapeId = | |
| 927 | + shapeFieldShapeId(catalog, executable.resultShapeId, "status") | |
| 928 | + ?? genericStatusShape; | |
| 929 | + break; | |
| 930 | + case "mcp": | |
| 931 | + dataShapeId = executable.outputShapeId; | |
| 932 | + errorShapeId = undefined; | |
| 933 | + statusShapeId = constNullShape( | |
| 934 | + catalog, | |
| 935 | + capability, | |
| 936 | + `executionResult:${capability.id}:status:null`, | |
| 937 | + ); | |
| 938 | + break; | |
| 939 | + } | |
| 940 | + | |
| 941 | + const dataFieldShapeId = nullableShape(catalog, capability, { | |
| 942 | + label: `executionResult:${capability.id}:data`, | |
| 943 | + title: "Result data", | |
| 944 | + baseShapeId: dataShapeId ?? genericDataShape, | |
| 945 | + }); | |
| 946 | + const errorFieldShapeId = nullableShape(catalog, capability, { | |
| 947 | + label: `executionResult:${capability.id}:error`, | |
| 948 | + title: "Result error", | |
| 949 | + baseShapeId: errorShapeId ?? genericErrorShape, | |
| 950 | + }); | |
| 951 | + const statusFieldShapeId = nullableShape(catalog, capability, { | |
| 952 | + label: `executionResult:${capability.id}:status`, | |
| 953 | + title: "Result status", | |
| 954 | + baseShapeId: statusShapeId, | |
| 955 | + }); | |
| 956 | + | |
| 957 | + return createSyntheticShape(catalog, { | |
| 958 | + capability, | |
| 959 | + label: `executionResult:${capability.id}`, | |
| 960 | + title: `${capability.surface.title ?? capability.id} result`, | |
| 961 | + node: { | |
| 962 | + type: "object", | |
| 963 | + fields: { | |
| 964 | + data: { | |
| 965 | + shapeId: dataFieldShapeId, | |
| 966 | + docs: { | |
| 967 | + description: "Successful result payload when available.", | |
| 968 | + }, | |
| 969 | + }, | |
| 970 | + error: { | |
| 971 | + shapeId: errorFieldShapeId, | |
| 972 | + docs: { | |
| 973 | + description: "Error payload when the remote execution completed but failed.", | |
| 974 | + }, | |
| 975 | + }, | |
| 976 | + headers: { | |
| 977 | + shapeId: headersShapeId, | |
| 978 | + docs: { | |
| 979 | + description: "Response headers when available.", | |
| 980 | + }, | |
| 981 | + }, | |
| 982 | + status: { | |
| 983 | + shapeId: statusFieldShapeId, | |
| 984 | + docs: { | |
| 985 | + description: "Transport status code when available.", | |
| 986 | + }, | |
| 987 | + }, | |
| 988 | + }, | |
| 989 | + required: ["data", "error", "headers", "status"], | |
| 990 | + additionalProperties: false, | |
| 991 | + }, | |
| 992 | + diagnostic: { | |
| 993 | + level: "info", | |
| 994 | + code: "projection_result_shape_synthesized", | |
| 995 | + message: `Synthesized execution result envelope for ${capability.id}`, | |
| 996 | + relatedSymbolIds: unique([ | |
| 997 | + dataFieldShapeId, | |
| 998 | + errorFieldShapeId, | |
| 999 | + headersShapeId, | |
| 1000 | + statusFieldShapeId, | |
| 1001 | + ]), | |
| 1002 | + }, | |
| 1003 | + }); | |
| 1004 | +}; | |
| 1005 | + | |
| 1006 | +const groupFieldShape = ( | |
| 1007 | + catalog: CatalogV1, | |
| 1008 | + capability: Capability, | |
| 1009 | + location: "path" | "query" | "headers" | "cookies", | |
| 1010 | + fields: Record<string, { shapeId: ShapeSymbolId; docs?: DocumentationBlock }>, | |
| 1011 | + required: string[], | |
| 1012 | +): ShapeSymbolId => | |
| 1013 | + createSyntheticShape(catalog, { | |
| 1014 | + capability, | |
| 1015 | + label: `group:${capability.id}:${location}`, | |
| 1016 | + title: `${location} parameters`, | |
| 1017 | + node: { | |
| 1018 | + type: "object", | |
| 1019 | + fields: Object.fromEntries( | |
| 1020 | + Object.entries(fields).map(([name, field]) => [ | |
| 1021 | + name, | |
| 1022 | + { | |
| 1023 | + shapeId: field.shapeId, | |
| 1024 | + ...(field.docs ? { docs: field.docs } : {}), | |
| 1025 | + }, | |
| 1026 | + ]), | |
| 1027 | + ), | |
| 1028 | + ...(required.length > 0 ? { required } : {}), | |
| 1029 | + additionalProperties: false, | |
| 1030 | + }, | |
| 1031 | + }); | |
| 1032 | + | |
| 1033 | +const projectHttpCallShape = ( | |
| 1034 | + catalog: CatalogV1, | |
| 1035 | + capability: Capability, | |
| 1036 | + executable: HttpExecutable, | |
| 1037 | +): ShapeSymbolId => { | |
| 1038 | + const chain = scopeChain(catalog, executable.scopeId); | |
| 1039 | + const defaults = mergeScopeDefaults(chain); | |
| 1040 | + | |
| 1041 | + const byLocation: Record< | |
| 1042 | + "path" | "query" | "header" | "cookie", | |
| 1043 | + ParameterSymbol[] | |
| 1044 | + > = { | |
| 1045 | + path: [], | |
| 1046 | + query: [], | |
| 1047 | + header: [], | |
| 1048 | + cookie: [], | |
| 1049 | + }; | |
| 1050 | + | |
| 1051 | + const pushParameter = (parameterId: ParameterSymbolId) => { | |
| 1052 | + const parameter = getParameter(catalog, parameterId); | |
| 1053 | + if (parameter) { | |
| 1054 | + byLocation[parameter.location].push(parameter); | |
| 1055 | + } | |
| 1056 | + }; | |
| 1057 | + | |
| 1058 | + for (const parameterId of defaults.parameterIds ?? []) { | |
| 1059 | + pushParameter(parameterId); | |
| 1060 | + } | |
| 1061 | + for (const parameterId of executable.pathParameterIds ?? []) { | |
| 1062 | + pushParameter(parameterId); | |
| 1063 | + } | |
| 1064 | + for (const parameterId of executable.queryParameterIds ?? []) { | |
| 1065 | + pushParameter(parameterId); | |
| 1066 | + } | |
| 1067 | + for (const parameterId of executable.headerParameterIds ?? []) { | |
| 1068 | + pushParameter(parameterId); | |
| 1069 | + } | |
| 1070 | + for (const parameterId of executable.cookieParameterIds ?? []) { | |
| 1071 | + pushParameter(parameterId); | |
| 1072 | + } | |
| 1073 | + | |
| 1074 | + const explicitHeaders = unique([ | |
| 1075 | + ...(defaults.headerIds ?? []), | |
| 1076 | + ]) | |
| 1077 | + .map((headerId: HeaderSymbolId) => getHeader(catalog, headerId)) | |
| 1078 | + .filter((symbol): symbol is HeaderSymbol => symbol !== undefined); | |
| 1079 | + | |
| 1080 | + const reservedTopLevel = new Set(["body", "args", "select", "path", "query", "headers", "cookies"]); | |
| 1081 | + const allNames = [ | |
| 1082 | + ...byLocation.path.map((parameter) => parameter.name), | |
| 1083 | + ...byLocation.query.map((parameter) => parameter.name), | |
| 1084 | + ...byLocation.header.map((parameter) => parameter.name), | |
| 1085 | + ...byLocation.cookie.map((parameter) => parameter.name), | |
| 1086 | + ]; | |
| 1087 | + | |
| 1088 | + const hasCollision = allNames.some((name, index) => | |
| 1089 | + reservedTopLevel.has(name) || allNames.indexOf(name) !== index | |
| 1090 | + ); | |
| 1091 | + | |
| 1092 | + const topLevelFields: Record<string, { shapeId: ShapeSymbolId; docs?: DocumentationBlock }> = {}; | |
| 1093 | + const topLevelRequired: string[] = []; | |
| 1094 | + | |
| 1095 | + const groupedFieldNames: Array<keyof typeof byLocation> = hasCollision | |
| 1096 | + ? ["path", "query", "header", "cookie"] | |
| 1097 | + : []; | |
| 1098 | + | |
| 1099 | + if (hasCollision) { | |
| 1100 | + createDiagnostic(catalog, { | |
| 1101 | + idSeed: { | |
| 1102 | + code: "projection_collision_grouped_fields", | |
| 1103 | + capabilityId: capability.id, | |
| 1104 | + }, | |
| 1105 | + level: "info", | |
| 1106 | + code: "projection_collision_grouped_fields", | |
| 1107 | + message: `Grouped parameter fields for ${capability.id} to avoid collisions`, | |
| 1108 | + provenance: capability.provenance, | |
| 1109 | + }); | |
| 1110 | + } | |
| 1111 | + | |
| 1112 | + for (const location of Object.keys(byLocation) as Array<keyof typeof byLocation>) { | |
| 1113 | + const parameters = byLocation[location]; | |
| 1114 | + if (parameters.length === 0) { | |
| 1115 | + continue; | |
| 1116 | + } | |
| 1117 | + | |
| 1118 | + if (groupedFieldNames.includes(location)) { | |
| 1119 | + const groupFields: Record<string, { shapeId: ShapeSymbolId; docs?: DocumentationBlock }> = {}; | |
| 1120 | + const groupRequired: string[] = []; | |
| 1121 | + for (const parameter of parameters) { | |
| 1122 | + groupFields[parameter.name] = { | |
| 1123 | + shapeId: parameterShapeId(catalog, capability, parameter), | |
| 1124 | + docs: parameter.docs, | |
| 1125 | + }; | |
| 1126 | + if (parameter.required) { | |
| 1127 | + groupRequired.push(parameter.name); | |
| 1128 | + } | |
| 1129 | + } | |
| 1130 | + | |
| 1131 | + const groupKey = location === "header" ? "headers" : location === "cookie" ? "cookies" : location; | |
| 1132 | + topLevelFields[groupKey] = { | |
| 1133 | + shapeId: groupFieldShape(catalog, capability, groupKey, groupFields, groupRequired), | |
| 1134 | + }; | |
| 1135 | + if (groupRequired.length > 0) { | |
| 1136 | + topLevelRequired.push(groupKey); | |
| 1137 | + } | |
| 1138 | + continue; | |
| 1139 | + } | |
| 1140 | + | |
| 1141 | + for (const parameter of parameters) { | |
| 1142 | + topLevelFields[parameter.name] = { | |
| 1143 | + shapeId: parameterShapeId(catalog, capability, parameter), | |
| 1144 | + docs: parameter.docs, | |
| 1145 | + }; | |
| 1146 | + if (parameter.required) { | |
| 1147 | + topLevelRequired.push(parameter.name); | |
| 1148 | + } | |
| 1149 | + } | |
| 1150 | + } | |
| 1151 | + | |
| 1152 | + for (const header of explicitHeaders) { | |
| 1153 | + if (hasCollision) { | |
| 1154 | + const existing = topLevelFields.headers | |
| 1155 | + ? getShape(catalog, topLevelFields.headers.shapeId) | |
| 1156 | + : undefined; | |
| 1157 | + const fields = existing && existing.node.type === "object" | |
| 1158 | + ? ({ ...existing.node.fields } as Record<string, { shapeId: ShapeSymbolId; docs?: DocumentationBlock }>) | |
| 1159 | + : {}; | |
| 1160 | + fields[header.name] = { | |
| 1161 | + shapeId: header.schemaShapeId | |
| 1162 | + ?? createSyntheticShape(catalog, { | |
| 1163 | + capability, | |
| 1164 | + label: `header:${header.id}:unknown`, | |
| 1165 | + title: header.name, | |
| 1166 | + docs: header.docs, | |
| 1167 | + node: { | |
| 1168 | + type: "unknown", | |
| 1169 | + reason: `Missing shape for header ${header.id}`, | |
| 1170 | + }, | |
| 1171 | + }), | |
| 1172 | + ...(header.docs ? { docs: header.docs } : {}), | |
| 1173 | + }; | |
| 1174 | + topLevelFields.headers = { | |
| 1175 | + shapeId: groupFieldShape( | |
| 1176 | + catalog, | |
| 1177 | + capability, | |
| 1178 | + "headers", | |
| 1179 | + Object.fromEntries( | |
| 1180 | + Object.entries(fields).map(([name, field]) => [name, { shapeId: field.shapeId, docs: field.docs }]), | |
| 1181 | + ), | |
| 1182 | + [], | |
| 1183 | + ), | |
| 1184 | + }; | |
| 1185 | + continue; | |
| 1186 | + } | |
| 1187 | + | |
| 1188 | + topLevelFields[header.name] = { | |
| 1189 | + shapeId: header.schemaShapeId | |
| 1190 | + ?? createSyntheticShape(catalog, { | |
| 1191 | + capability, | |
| 1192 | + label: `header:${header.id}:unknown`, | |
| 1193 | + title: header.name, | |
| 1194 | + docs: header.docs, | |
| 1195 | + node: { | |
| 1196 | + type: "unknown", | |
| 1197 | + reason: `Missing shape for header ${header.id}`, | |
| 1198 | + }, | |
| 1199 | + }), | |
| 1200 | + docs: header.docs, | |
| 1201 | + }; | |
| 1202 | + } | |
| 1203 | + | |
| 1204 | + const bodyShapeId = requestBodyShapeId(catalog, capability, executable.requestBodyId); | |
| 1205 | + if (bodyShapeId) { | |
| 1206 | + topLevelFields.body = { shapeId: bodyShapeId }; | |
| 1207 | + const requestBody = executable.requestBodyId | |
| 1208 | + ? catalog.symbols[executable.requestBodyId] | |
| 1209 | + : undefined; | |
| 1210 | + if (requestBody && requestBody.kind === "requestBody" && requestBody.required) { | |
| 1211 | + topLevelRequired.push("body"); | |
| 1212 | + } | |
| 1213 | + } | |
| 1214 | + | |
| 1215 | + return createSyntheticShape(catalog, { | |
| 1216 | + capability, | |
| 1217 | + label: `call:${capability.id}`, | |
| 1218 | + title: `${capability.surface.title ?? capability.id} call`, | |
| 1219 | + node: { | |
| 1220 | + type: "object", | |
| 1221 | + fields: Object.fromEntries( | |
| 1222 | + Object.entries(topLevelFields).map(([name, field]) => [ | |
| 1223 | + name, | |
| 1224 | + { | |
| 1225 | + shapeId: field.shapeId, | |
| 1226 | + ...(field.docs ? { docs: field.docs } : {}), | |
| 1227 | + }, | |
| 1228 | + ]), | |
| 1229 | + ), | |
| 1230 | + ...(topLevelRequired.length > 0 ? { required: unique(topLevelRequired) } : {}), | |
| 1231 | + additionalProperties: false, | |
| 1232 | + }, | |
| 1233 | + diagnostic: { | |
| 1234 | + level: "info", | |
| 1235 | + code: "projection_call_shape_synthesized", | |
| 1236 | + message: `Synthesized HTTP call shape for ${capability.id}`, | |
| 1237 | + }, | |
| 1238 | + }); | |
| 1239 | +}; | |
| 1240 | + | |
| 1241 | +const projectGraphqlCallShape = ( | |
| 1242 | + catalog: CatalogV1, | |
| 1243 | + capability: Capability, | |
| 1244 | + executable: GraphQLExecutable, | |
| 1245 | +): ShapeSymbolId => { | |
| 1246 | + const argumentShape = getShape(catalog, executable.argumentShapeId); | |
| 1247 | + const reserved = new Set(["body", "args", "select", "path", "query", "headers", "cookies"]); | |
| 1248 | + | |
| 1249 | + const canFlatten = | |
| 1250 | + argumentShape?.node.type === "object" | |
| 1251 | + && Object.keys(argumentShape.node.fields).every((name) => !reserved.has(name)); | |
| 1252 | + | |
| 1253 | + const fields: Record<string, { shapeId: ShapeSymbolId; docs?: DocumentationBlock }> = {}; | |
| 1254 | + const required: string[] = []; | |
| 1255 | + | |
| 1256 | + if (canFlatten && argumentShape.node.type === "object") { | |
| 1257 | + for (const [name, field] of Object.entries(argumentShape.node.fields)) { | |
| 1258 | + fields[name] = { | |
| 1259 | + shapeId: field.shapeId, | |
| 1260 | + docs: field.docs, | |
| 1261 | + }; | |
| 1262 | + } | |
| 1263 | + required.push(...(argumentShape.node.required ?? [])); | |
| 1264 | + } else { | |
| 1265 | + fields.args = { | |
| 1266 | + shapeId: executable.argumentShapeId, | |
| 1267 | + docs: argumentShape?.docs, | |
| 1268 | + }; | |
| 1269 | + required.push("args"); | |
| 1270 | + } | |
| 1271 | + | |
| 1272 | + if (executable.selectionMode === "caller") { | |
| 1273 | + fields.select = { | |
| 1274 | + shapeId: | |
| 1275 | + executable.selectionShapeId | |
| 1276 | + ?? createSyntheticShape(catalog, { | |
| 1277 | + capability, | |
| 1278 | + label: `select:${capability.id}:unknown`, | |
| 1279 | + title: `${capability.surface.title ?? capability.id} selection`, | |
| 1280 | + node: { | |
| 1281 | + type: "unknown", | |
| 1282 | + reason: `Missing selection shape for ${capability.id}`, | |
| 1283 | + }, | |
| 1284 | + diagnostic: { | |
| 1285 | + level: "warning", | |
| 1286 | + code: "selection_shape_missing", | |
| 1287 | + message: `Missing selection shape for ${capability.id}`, | |
| 1288 | + }, | |
| 1289 | + }), | |
| 1290 | + }; | |
| 1291 | + required.push("select"); | |
| 1292 | + } | |
| 1293 | + | |
| 1294 | + return createSyntheticShape(catalog, { | |
| 1295 | + capability, | |
| 1296 | + label: `call:${capability.id}`, | |
| 1297 | + title: `${capability.surface.title ?? capability.id} call`, | |
| 1298 | + node: { | |
| 1299 | + type: "object", | |
| 1300 | + fields: Object.fromEntries( | |
| 1301 | + Object.entries(fields).map(([name, field]) => [ | |
| 1302 | + name, | |
| 1303 | + { | |
| 1304 | + shapeId: field.shapeId, | |
| 1305 | + ...(field.docs ? { docs: field.docs } : {}), | |
| 1306 | + }, | |
| 1307 | + ]), | |
| 1308 | + ), | |
| 1309 | + ...(required.length > 0 ? { required: unique(required) } : {}), | |
| 1310 | + additionalProperties: false, | |
| 1311 | + }, | |
| 1312 | + diagnostic: { | |
| 1313 | + level: "info", | |
| 1314 | + code: "projection_call_shape_synthesized", | |
| 1315 | + message: `Synthesized GraphQL call shape for ${capability.id}`, | |
| 1316 | + }, | |
| 1317 | + }); | |
| 1318 | +}; | |
| 1319 | + | |
| 1320 | +const projectMcpCallShape = ( | |
| 1321 | + catalog: CatalogV1, | |
| 1322 | + capability: Capability, | |
| 1323 | + executable: McpExecutable, | |
| 1324 | +): ShapeSymbolId => { | |
| 1325 | + if (!executable.inputShapeId) { | |
| 1326 | + return createSyntheticShape(catalog, { | |
| 1327 | + capability, | |
| 1328 | + label: `call:${capability.id}:empty`, | |
| 1329 | + title: `${capability.surface.title ?? capability.id} call`, | |
| 1330 | + node: { | |
| 1331 | + type: "object", | |
| 1332 | + fields: {}, | |
| 1333 | + additionalProperties: false, | |
| 1334 | + }, | |
| 1335 | + diagnostic: { | |
| 1336 | + level: "info", | |
| 1337 | + code: "projection_call_shape_synthesized", | |
| 1338 | + message: `Synthesized empty MCP call shape for ${capability.id}`, | |
| 1339 | + }, | |
| 1340 | + }); | |
| 1341 | + } | |
| 1342 | + | |
| 1343 | + const inputShape = getShape(catalog, executable.inputShapeId); | |
| 1344 | + if (inputShape?.node.type === "object") { | |
| 1345 | + return executable.inputShapeId; | |
| 1346 | + } | |
| 1347 | + | |
| 1348 | + return createSyntheticShape(catalog, { | |
| 1349 | + capability, | |
| 1350 | + label: `call:${capability.id}`, | |
| 1351 | + title: `${capability.surface.title ?? capability.id} call`, | |
| 1352 | + node: { | |
| 1353 | + type: "object", | |
| 1354 | + fields: { | |
| 1355 | + input: { | |
| 1356 | + shapeId: executable.inputShapeId, | |
| 1357 | + }, | |
| 1358 | + }, | |
| 1359 | + required: ["input"], | |
| 1360 | + additionalProperties: false, | |
| 1361 | + }, | |
| 1362 | + diagnostic: { | |
| 1363 | + level: "info", | |
| 1364 | + code: "projection_call_shape_synthesized", | |
| 1365 | + message: `Wrapped non-object MCP input shape for ${capability.id}`, | |
| 1366 | + }, | |
| 1367 | + }); | |
| 1368 | +}; | |
| 1369 | + | |
| 1370 | +const projectCapability = ( | |
| 1371 | + catalog: CatalogV1, | |
| 1372 | + capability: Capability, | |
| 1373 | +): ToolDescriptor => { | |
| 1374 | + const executable = chooseExecutable(catalog, capability); | |
| 1375 | + const responseSet = catalog.responseSets[executable.responseSetId]; | |
| 1376 | + | |
| 1377 | + if (!responseSet) { | |
| 1378 | + throw new Error(`Missing response set ${executable.responseSetId} for ${capability.id}`); | |
| 1379 | + } | |
| 1380 | + | |
| 1381 | + let callShapeId: ShapeSymbolId; | |
| 1382 | + let resultShapeId: ShapeSymbolId | undefined; | |
| 1383 | + | |
| 1384 | + switch (executable.protocol) { | |
| 1385 | + case "http": | |
| 1386 | + callShapeId = projectHttpCallShape(catalog, capability, executable); | |
| 1387 | + resultShapeId = projectExecutionResultShape(catalog, capability, executable, responseSet); | |
| 1388 | + break; | |
| 1389 | + case "graphql": | |
| 1390 | + callShapeId = projectGraphqlCallShape(catalog, capability, executable); | |
| 1391 | + resultShapeId = projectExecutionResultShape(catalog, capability, executable, responseSet); | |
| 1392 | + break; | |
| 1393 | + case "mcp": | |
| 1394 | + callShapeId = projectMcpCallShape(catalog, capability, executable); | |
| 1395 | + resultShapeId = projectExecutionResultShape(catalog, capability, executable, responseSet); | |
| 1396 | + break; | |
| 1397 | + } | |
| 1398 | + | |
| 1399 | + const diagnosticIds = unique([ | |
| 1400 | + ...(capability.diagnosticIds ?? []), | |
| 1401 | + ...(executable.diagnosticIds ?? []), | |
| 1402 | + ...(responseSet.diagnosticIds ?? []), | |
| 1403 | + ...(callShapeId ? catalog.symbols[callShapeId]?.diagnosticIds ?? [] : []), | |
| 1404 | + ...(resultShapeId ? catalog.symbols[resultShapeId]?.diagnosticIds ?? [] : []), | |
| 1405 | + ]); | |
| 1406 | + const diagnostics = diagnosticIds | |
| 1407 | + .map((diagnosticId) => catalog.diagnostics[diagnosticId]) | |
| 1408 | + .filter((diagnostic): diagnostic is ImportDiagnostic => diagnostic !== undefined); | |
| 1409 | + | |
| 1410 | + return { | |
| 1411 | + toolPath: [...capability.surface.toolPath], | |
| 1412 | + capabilityId: capability.id, | |
| 1413 | + ...(capability.surface.title ? { title: capability.surface.title } : {}), | |
| 1414 | + ...(capability.surface.summary ? { summary: capability.surface.summary } : {}), | |
| 1415 | + effect: capability.semantics.effect, | |
| 1416 | + interaction: { | |
| 1417 | + mayRequireApproval: capability.interaction.approval.mayRequire, | |
| 1418 | + mayElicit: capability.interaction.elicitation.mayRequest, | |
| 1419 | + }, | |
| 1420 | + callShapeId, | |
| 1421 | + ...(resultShapeId ? { resultShapeId } : {}), | |
| 1422 | + responseSetId: executable.responseSetId, | |
| 1423 | + diagnosticCounts: { | |
| 1424 | + warning: diagnostics.filter((diagnostic) => diagnostic.level === "warning").length, | |
| 1425 | + error: diagnostics.filter((diagnostic) => diagnostic.level === "error").length, | |
| 1426 | + }, | |
| 1427 | + }; | |
| 1428 | +}; | |
| 1429 | + | |
| 1430 | +const summaryView = ( | |
| 1431 | + catalog: CatalogV1, | |
| 1432 | + capability: Capability, | |
| 1433 | + descriptor: ToolDescriptor, | |
| 1434 | +): CapabilitySummaryView => ({ | |
| 1435 | + capabilityId: capability.id, | |
| 1436 | + toolPath: [...capability.surface.toolPath], | |
| 1437 | + ...(capability.surface.summary ? { summary: capability.surface.summary } : {}), | |
| 1438 | + executableIds: [...capability.executableIds], | |
| 1439 | + auth: capability.auth, | |
| 1440 | + interaction: capability.interaction, | |
| 1441 | + callShapeId: descriptor.callShapeId, | |
| 1442 | + ...(descriptor.resultShapeId ? { resultShapeId: descriptor.resultShapeId } : {}), | |
| 1443 | + responseSetId: descriptor.responseSetId, | |
| 1444 | + ...(capability.diagnosticIds ? { diagnosticIds: [...capability.diagnosticIds] } : {}), | |
| 1445 | +}); | |
| 1446 | + | |
| 1447 | +const searchDoc = ( | |
| 1448 | + catalog: CatalogV1, | |
| 1449 | + capability: Capability, | |
| 1450 | +): ToolSearchDoc => ({ | |
| 1451 | + capabilityId: capability.id, | |
| 1452 | + toolPath: [...capability.surface.toolPath], | |
| 1453 | + ...(capability.surface.title ? { title: capability.surface.title } : {}), | |
| 1454 | + ...(capability.surface.summary ? { summary: capability.surface.summary } : {}), | |
| 1455 | + ...(capability.surface.tags ? { tags: [...capability.surface.tags] } : {}), | |
| 1456 | + protocolHints: unique( | |
| 1457 | + capability.executableIds | |
| 1458 | + .map((executableId: ExecutableId) => catalog.executables[executableId]?.protocol) | |
| 1459 | + .filter((protocol): protocol is Executable["protocol"] => protocol !== undefined), | |
| 1460 | + ), | |
| 1461 | + authHints: authHintStrings(catalog, capability.auth), | |
| 1462 | + effect: capability.semantics.effect, | |
| 1463 | +}); | |
| 1464 | + | |
| 1465 | +const symbolTitle = (symbol: IrSymbol): string | undefined => { | |
| 1466 | + switch (symbol.kind) { | |
| 1467 | + case "shape": | |
| 1468 | + return symbol.title ?? symbol.docs?.summary; | |
| 1469 | + case "parameter": | |
| 1470 | + case "header": | |
| 1471 | + return symbol.name; | |
| 1472 | + case "example": | |
| 1473 | + return symbol.name ?? symbol.docs?.summary; | |
| 1474 | + case "requestBody": | |
| 1475 | + case "response": | |
| 1476 | + case "securityScheme": | |
| 1477 | + return symbol.docs?.summary; | |
| 1478 | + } | |
| 1479 | +}; | |
| 1480 | + | |
| 1481 | +const symbolSummary = (symbol: IrSymbol): string | undefined => | |
| 1482 | + symbol.docs?.summary ?? docsSummary(symbol.docs); | |
| 1483 | + | |
| 1484 | +const symbolEdges = (symbol: IrSymbol): Array<{ label: string; targetId: SymbolId }> => { | |
| 1485 | + switch (symbol.kind) { | |
| 1486 | + case "shape": { | |
| 1487 | + const edges: Array<{ label: string; targetId: SymbolId }> = []; | |
| 1488 | + switch (symbol.node.type) { | |
| 1489 | + case "object": | |
| 1490 | + for (const [name, field] of Object.entries(symbol.node.fields)) { | |
| 1491 | + edges.push({ label: `field:${name}`, targetId: field.shapeId }); | |
| 1492 | + } | |
| 1493 | + if (typeof symbol.node.additionalProperties !== "boolean" && symbol.node.additionalProperties) { | |
| 1494 | + edges.push({ | |
| 1495 | + label: "additionalProperties", | |
| 1496 | + targetId: symbol.node.additionalProperties, | |
| 1497 | + }); | |
| 1498 | + } | |
| 1499 | + for (const [pattern, targetId] of Object.entries(symbol.node.patternProperties ?? {})) { | |
| 1500 | + edges.push({ label: `pattern:${pattern}`, targetId }); | |
| 1501 | + } | |
| 1502 | + break; | |
| 1503 | + case "array": | |
| 1504 | + edges.push({ label: "item", targetId: symbol.node.itemShapeId }); | |
| 1505 | + break; | |
| 1506 | + case "tuple": | |
| 1507 | + symbol.node.itemShapeIds.forEach((targetId, index) => { | |
| 1508 | + edges.push({ label: `item:${String(index)}`, targetId }); | |
| 1509 | + }); | |
| 1510 | + if (typeof symbol.node.additionalItems !== "boolean" && symbol.node.additionalItems) { | |
| 1511 | + edges.push({ label: "additionalItems", targetId: symbol.node.additionalItems }); | |
| 1512 | + } | |
| 1513 | + break; | |
| 1514 | + case "map": | |
| 1515 | + edges.push({ label: "value", targetId: symbol.node.valueShapeId }); | |
| 1516 | + break; | |
| 1517 | + case "allOf": | |
| 1518 | + case "anyOf": | |
| 1519 | + case "oneOf": | |
| 1520 | + symbol.node.items.forEach((targetId, index) => { | |
| 1521 | + edges.push({ label: `${symbol.node.type}:${String(index)}`, targetId }); | |
| 1522 | + }); | |
| 1523 | + if (symbol.node.type === "oneOf") { | |
| 1524 | + for (const [key, targetId] of Object.entries(symbol.node.discriminator?.mapping ?? {})) { | |
| 1525 | + edges.push({ label: `discriminator:${key}`, targetId }); | |
| 1526 | + } | |
| 1527 | + } | |
| 1528 | + break; | |
| 1529 | + case "nullable": | |
| 1530 | + edges.push({ label: "item", targetId: symbol.node.itemShapeId }); | |
| 1531 | + break; | |
| 1532 | + case "ref": | |
| 1533 | + edges.push({ label: "target", targetId: symbol.node.target }); | |
| 1534 | + break; | |
| 1535 | + case "not": | |
| 1536 | + edges.push({ label: "not", targetId: symbol.node.itemShapeId }); | |
| 1537 | + break; | |
| 1538 | + case "conditional": | |
| 1539 | + edges.push({ label: "if", targetId: symbol.node.ifShapeId }); | |
| 1540 | + if (symbol.node.thenShapeId) { | |
| 1541 | + edges.push({ label: "then", targetId: symbol.node.thenShapeId }); | |
| 1542 | + } | |
| 1543 | + if (symbol.node.elseShapeId) { | |
| 1544 | + edges.push({ label: "else", targetId: symbol.node.elseShapeId }); | |
| 1545 | + } | |
| 1546 | + break; | |
| 1547 | + case "graphqlInterface": | |
| 1548 | + for (const [name, field] of Object.entries(symbol.node.fields)) { | |
| 1549 | + edges.push({ label: `field:${name}`, targetId: field.shapeId }); | |
| 1550 | + } | |
| 1551 | + symbol.node.possibleTypeIds.forEach((targetId) => { | |
| 1552 | + edges.push({ label: "possibleType", targetId }); | |
| 1553 | + }); | |
| 1554 | + break; | |
| 1555 | + case "graphqlUnion": | |
| 1556 | + symbol.node.memberTypeIds.forEach((targetId) => { | |
| 1557 | + edges.push({ label: "member", targetId }); | |
| 1558 | + }); | |
| 1559 | + break; | |
| 1560 | + default: | |
| 1561 | + break; | |
| 1562 | + } | |
| 1563 | + return edges; | |
| 1564 | + } | |
| 1565 | + case "parameter": | |
| 1566 | + case "header": | |
| 1567 | + return unique([ | |
| 1568 | + ...(symbol.schemaShapeId ? [{ label: "schema", targetId: symbol.schemaShapeId as SymbolId }] : []), | |
| 1569 | + ...((symbol.content ?? []).flatMap((content, index) => [ | |
| 1570 | + ...(content.shapeId ? [{ label: `content:${String(index)}`, targetId: content.shapeId as SymbolId }] : []), | |
| 1571 | + ...((content.exampleIds ?? []).map((targetId) => ({ | |
| 1572 | + label: `example:${String(index)}`, | |
| 1573 | + targetId: targetId as SymbolId, | |
| 1574 | + }))), | |
| 1575 | + ])), | |
| 1576 | + ...((symbol.exampleIds ?? []).map((targetId) => ({ | |
| 1577 | + label: "example", | |
| 1578 | + targetId: targetId as SymbolId, | |
| 1579 | + }))), | |
| 1580 | + ]); | |
| 1581 | + case "requestBody": | |
| 1582 | + return (symbol.contents ?? []).flatMap((content, index) => [ | |
| 1583 | + ...(content.shapeId ? [{ label: `content:${String(index)}`, targetId: content.shapeId as SymbolId }] : []), | |
| 1584 | + ...((content.exampleIds ?? []).map((targetId) => ({ | |
| 1585 | + label: `example:${String(index)}`, | |
| 1586 | + targetId: targetId as SymbolId, | |
| 1587 | + }))), | |
| 1588 | + ]); | |
| 1589 | + case "response": | |
| 1590 | + return [ | |
| 1591 | + ...((symbol.headerIds ?? []).map((targetId) => ({ | |
| 1592 | + label: "header", | |
| 1593 | + targetId: targetId as SymbolId, | |
| 1594 | + }))), | |
| 1595 | + ...((symbol.contents ?? []).flatMap((content, index) => [ | |
| 1596 | + ...(content.shapeId ? [{ label: `content:${String(index)}`, targetId: content.shapeId as SymbolId }] : []), | |
| 1597 | + ...((content.exampleIds ?? []).map((targetId) => ({ | |
| 1598 | + label: `example:${String(index)}`, | |
| 1599 | + targetId: targetId as SymbolId, | |
| 1600 | + }))), | |
| 1601 | + ])), | |
| 1602 | + ]; | |
| 1603 | + case "example": | |
| 1604 | + case "securityScheme": | |
| 1605 | + return []; | |
| 1606 | + } | |
| 1607 | +}; | |
| 1608 | + | |
| 1609 | +export const decodeCatalogV1 = (input: unknown): CatalogV1 => { | |
| 1610 | + try { | |
| 1611 | + return decodeCatalogSync(input); | |
| 1612 | + } catch (cause) { | |
| 1613 | + throw new Error(ParseResult.TreeFormatter.formatErrorSync(cause as never)); | |
| 1614 | + } | |
| 1615 | +}; | |
| 1616 | + | |
| 1617 | +export const decodeCatalogFragmentV1 = (input: unknown): CatalogFragmentV1 => { | |
| 1618 | + try { | |
| 1619 | + return decodeCatalogFragmentSync(input); | |
| 1620 | + } catch (cause) { | |
| 1621 | + throw new Error(ParseResult.TreeFormatter.formatErrorSync(cause as never)); | |
| 1622 | + } | |
| 1623 | +}; | |
| 1624 | + | |
| 1625 | +export const decodeCatalogSnapshotV1 = (input: unknown) => { | |
| 1626 | + try { | |
| 1627 | + const snapshot = decodeCatalogSnapshotSync(input); | |
| 1628 | + assertCatalogInvariants(snapshot.catalog); | |
| 1629 | + return snapshot; | |
| 1630 | + } catch (cause) { | |
| 1631 | + if (cause instanceof Error) { | |
| 1632 | + throw cause; | |
| 1633 | + } | |
| 1634 | + throw new Error(ParseResult.TreeFormatter.formatErrorSync(cause as never)); | |
| 1635 | + } | |
| 1636 | +}; | |
| 1637 | + | |
| 1638 | +export const createEmptyCatalogV1 = (): CatalogV1 => emptyCatalog(); | |
| 1639 | + | |
| 1640 | +export const createCatalogSnapshotV1 = (input: { | |
| 1641 | + import: ImportMetadata; | |
| 1642 | + catalog: CatalogV1; | |
| 1643 | +}) => { | |
| 1644 | + const catalog = assertCatalogInvariants(decodeCatalogV1(input.catalog)); | |
| 1645 | + | |
| 1646 | + return { | |
| 1647 | + version: "ir.v1.snapshot" as const, | |
| 1648 | + import: input.import, | |
| 1649 | + catalog, | |
| 1650 | + }; | |
| 1651 | +}; | |
| 1652 | + | |
| 1653 | +export const createCatalogSnapshotV1FromFragments = (input: { | |
| 1654 | + import: ImportMetadata; | |
| 1655 | + fragments: readonly CatalogFragmentV1[]; | |
| 1656 | +}) => | |
| 1657 | + createCatalogSnapshotV1({ | |
| 1658 | + import: input.import, | |
| 1659 | + catalog: mergeCatalogFragments(input.fragments), | |
| 1660 | + }); | |
| 1661 | + | |
| 1662 | +export const mergeCatalogFragments = (fragments: readonly CatalogFragmentV1[]): CatalogV1 => { | |
| 1663 | + const catalog = emptyCatalog(); | |
| 1664 | + const seenByCollection = new Map<string, string>(); | |
| 1665 | + | |
| 1666 | + const mergeRecord = <T extends { id: string }>( | |
| 1667 | + collectionName: keyof CatalogV1, | |
| 1668 | + entries: Record<string, T> | undefined, | |
| 1669 | + ) => { | |
| 1670 | + if (!entries) { | |
| 1671 | + return; | |
| 1672 | + } | |
| 1673 | + | |
| 1674 | + const target = catalog[collectionName] as Record<string, T>; | |
| 1675 | + for (const [id, entry] of Object.entries(entries)) { | |
| 1676 | + const existing = target[id]; | |
| 1677 | + if (!existing) { | |
| 1678 | + target[id] = entry; | |
| 1679 | + seenByCollection.set(`${String(collectionName)}:${id}`, stableStringify(entry)); | |
| 1680 | + continue; | |
| 1681 | + } | |
| 1682 | + | |
| 1683 | + const existingHash = seenByCollection.get(`${String(collectionName)}:${id}`) ?? stableStringify(existing); | |
| 1684 | + const nextHash = stableStringify(entry); | |
| 1685 | + if (existingHash === nextHash) { | |
| 1686 | + continue; | |
| 1687 | + } | |
| 1688 | + | |
| 1689 | + if (collectionName !== "diagnostics") { | |
| 1690 | + createDiagnostic(catalog, { | |
| 1691 | + idSeed: { | |
| 1692 | + collectionName, | |
| 1693 | + id, | |
| 1694 | + existingHash, | |
| 1695 | + nextHash, | |
| 1696 | + }, | |
| 1697 | + level: "error", | |
| 1698 | + code: "merge_conflict_preserved_first", | |
| 1699 | + message: `Conflicting ${String(collectionName)} entry for ${id}; preserved first value`, | |
| 1700 | + provenance: "provenance" in existing | |
| 1701 | + ? (existing as { provenance: ImportDiagnostic["provenance"] }).provenance | |
| 1702 | + : [], | |
| 1703 | + }); | |
| 1704 | + } | |
| 1705 | + } | |
| 1706 | + }; | |
| 1707 | + | |
| 1708 | + for (const fragment of fragments) { | |
| 1709 | + mergeRecord("documents", fragment.documents); | |
| 1710 | + mergeRecord("resources", fragment.resources); | |
| 1711 | + mergeRecord("scopes", fragment.scopes); | |
| 1712 | + mergeRecord("symbols", fragment.symbols); | |
| 1713 | + mergeRecord("capabilities", fragment.capabilities); | |
| 1714 | + mergeRecord("executables", fragment.executables); | |
| 1715 | + mergeRecord("responseSets", fragment.responseSets); | |
| 1716 | + mergeRecord("diagnostics", fragment.diagnostics); | |
| 1717 | + } | |
| 1718 | + | |
| 1719 | + return assertCatalogInvariants(decodeCatalogV1(catalog)); | |
| 1720 | +}; | |
| 1721 | + | |
| 1722 | +export const validateCatalogInvariants = ( | |
| 1723 | + catalog: CatalogV1, | |
| 1724 | +): CatalogInvariantViolation[] => { | |
| 1725 | + const violations: CatalogInvariantViolation[] = []; | |
| 1726 | + | |
| 1727 | + const checkProvenanceDocuments = (input: { | |
| 1728 | + entityId: string; | |
| 1729 | + provenance: readonly { documentId: DocumentId }[]; | |
| 1730 | + }) => { | |
| 1731 | + for (const provenance of input.provenance) { | |
| 1732 | + if (!catalog.documents[provenance.documentId]) { | |
| 1733 | + violations.push({ | |
| 1734 | + code: "missing_provenance_document", | |
| 1735 | + entityId: input.entityId, | |
| 1736 | + message: `Entity ${input.entityId} references missing provenance document ${provenance.documentId}`, | |
| 1737 | + }); | |
| 1738 | + } | |
| 1739 | + } | |
| 1740 | + }; | |
| 1741 | + | |
| 1742 | + for (const symbol of Object.values(catalog.symbols)) { | |
| 1743 | + if (symbol.provenance.length === 0) { | |
| 1744 | + violations.push({ | |
| 1745 | + code: "missing_symbol_provenance", | |
| 1746 | + entityId: symbol.id, | |
| 1747 | + message: `Symbol ${symbol.id} is missing provenance`, | |
| 1748 | + }); | |
| 1749 | + } | |
| 1750 | + | |
| 1751 | + if (symbol.kind === "shape") { | |
| 1752 | + if (!symbol.resourceId && !symbol.synthetic) { | |
| 1753 | + violations.push({ | |
| 1754 | + code: "missing_resource_context", | |
| 1755 | + entityId: symbol.id, | |
| 1756 | + message: `Shape ${symbol.id} must belong to a resource or be synthetic`, | |
| 1757 | + }); | |
| 1758 | + } | |
| 1759 | + | |
| 1760 | + if (symbol.node.type === "ref" && !catalog.symbols[symbol.node.target]) { | |
| 1761 | + violations.push({ | |
| 1762 | + code: "missing_reference_target", | |
| 1763 | + entityId: symbol.id, | |
| 1764 | + message: `Shape ${symbol.id} references missing shape ${symbol.node.target}`, | |
| 1765 | + }); | |
| 1766 | + } | |
| 1767 | + | |
| 1768 | + if (symbol.node.type === "unknown" && symbol.node.reason?.includes("unresolved")) { | |
| 1769 | + const hasDiagnostic = (symbol.diagnosticIds ?? []) | |
| 1770 | + .map((diagnosticId) => catalog.diagnostics[diagnosticId]) | |
| 1771 | + .some((diagnostic) => diagnostic?.code === "unresolved_ref"); | |
| 1772 | + | |
| 1773 | + if (!hasDiagnostic) { | |
| 1774 | + violations.push({ | |
| 1775 | + code: "missing_unresolved_ref_diagnostic", | |
| 1776 | + entityId: symbol.id, | |
| 1777 | + message: `Shape ${symbol.id} is unresolved but has no unresolved_ref diagnostic`, | |
| 1778 | + }); | |
| 1779 | + } | |
| 1780 | + } | |
| 1781 | + | |
| 1782 | + if (symbol.synthetic && symbol.resourceId && !catalog.resources[symbol.resourceId]) { | |
| 1783 | + violations.push({ | |
| 1784 | + code: "missing_resource_context", | |
| 1785 | + entityId: symbol.id, | |
| 1786 | + message: `Synthetic shape ${symbol.id} references missing resource ${symbol.resourceId}`, | |
| 1787 | + }); | |
| 1788 | + } | |
| 1789 | + } | |
| 1790 | + | |
| 1791 | + checkProvenanceDocuments({ | |
| 1792 | + entityId: symbol.id, | |
| 1793 | + provenance: symbol.provenance, | |
| 1794 | + }); | |
| 1795 | + } | |
| 1796 | + | |
| 1797 | + for (const collection of [ | |
| 1798 | + ...Object.values(catalog.resources), | |
| 1799 | + ...Object.values(catalog.scopes), | |
| 1800 | + ...Object.values(catalog.capabilities), | |
| 1801 | + ...Object.values(catalog.executables), | |
| 1802 | + ...Object.values(catalog.responseSets), | |
| 1803 | + ]) { | |
| 1804 | + if ("provenance" in collection && collection.provenance.length === 0) { | |
| 1805 | + violations.push({ | |
| 1806 | + code: "missing_entity_provenance", | |
| 1807 | + entityId: "id" in collection ? collection.id : undefined, | |
| 1808 | + message: `Entity ${"id" in collection ? collection.id : "unknown"} is missing provenance`, | |
| 1809 | + }); | |
| 1810 | + } | |
| 1811 | + | |
| 1812 | + if ("id" in collection) { | |
| 1813 | + checkProvenanceDocuments({ | |
| 1814 | + entityId: collection.id, | |
| 1815 | + provenance: collection.provenance, | |
| 1816 | + }); | |
| 1817 | + } | |
| 1818 | + } | |
| 1819 | + | |
| 1820 | + for (const resource of Object.values(catalog.resources)) { | |
| 1821 | + if (!catalog.documents[resource.documentId]) { | |
| 1822 | + violations.push({ | |
| 1823 | + code: "missing_document", | |
| 1824 | + entityId: resource.id, | |
| 1825 | + message: `Resource ${resource.id} references missing document ${resource.documentId}`, | |
| 1826 | + }); | |
| 1827 | + } | |
| 1828 | + } | |
| 1829 | + | |
| 1830 | + for (const capability of Object.values(catalog.capabilities)) { | |
| 1831 | + if (!catalog.scopes[capability.serviceScopeId]) { | |
| 1832 | + violations.push({ | |
| 1833 | + code: "missing_service_scope", | |
| 1834 | + entityId: capability.id, | |
| 1835 | + message: `Capability ${capability.id} references missing scope ${capability.serviceScopeId}`, | |
| 1836 | + }); | |
| 1837 | + } | |
| 1838 | + | |
| 1839 | + if ( | |
| 1840 | + capability.preferredExecutableId | |
| 1841 | + && !capability.executableIds.includes(capability.preferredExecutableId) | |
| 1842 | + ) { | |
| 1843 | + violations.push({ | |
| 1844 | + code: "invalid_preferred_executable", | |
| 1845 | + entityId: capability.id, | |
| 1846 | + message: `Capability ${capability.id} preferred executable is not in executableIds`, | |
| 1847 | + }); | |
| 1848 | + } | |
| 1849 | + | |
| 1850 | + for (const executableId of capability.executableIds) { | |
| 1851 | + if (!catalog.executables[executableId]) { | |
| 1852 | + violations.push({ | |
| 1853 | + code: "missing_executable", | |
| 1854 | + entityId: capability.id, | |
| 1855 | + message: `Capability ${capability.id} references missing executable ${executableId}`, | |
| 1856 | + }); | |
| 1857 | + } | |
| 1858 | + } | |
| 1859 | + } | |
| 1860 | + | |
| 1861 | + for (const executable of Object.values(catalog.executables)) { | |
| 1862 | + if (!catalog.capabilities[executable.capabilityId]) { | |
| 1863 | + violations.push({ | |
| 1864 | + code: "missing_executable", | |
| 1865 | + entityId: executable.id, | |
| 1866 | + message: `Executable ${executable.id} references missing capability ${executable.capabilityId}`, | |
| 1867 | + }); | |
| 1868 | + } | |
| 1869 | + | |
| 1870 | + if (!catalog.scopes[executable.scopeId]) { | |
| 1871 | + violations.push({ | |
| 1872 | + code: "missing_scope", | |
| 1873 | + entityId: executable.id, | |
| 1874 | + message: `Executable ${executable.id} references missing scope ${executable.scopeId}`, | |
| 1875 | + }); | |
| 1876 | + } | |
| 1877 | + | |
| 1878 | + if (!catalog.responseSets[executable.responseSetId]) { | |
| 1879 | + violations.push({ | |
| 1880 | + code: "missing_response_set", | |
| 1881 | + entityId: executable.id, | |
| 1882 | + message: `Executable ${executable.id} references missing response set ${executable.responseSetId}`, | |
| 1883 | + }); | |
| 1884 | + } | |
| 1885 | + } | |
| 1886 | + | |
| 1887 | + return violations; | |
| 1888 | +}; | |
| 1889 | + | |
| 1890 | +const assertCatalogInvariants = (catalog: CatalogV1): CatalogV1 => { | |
| 1891 | + const violations = validateCatalogInvariants(catalog); | |
| 1892 | + if (violations.length === 0) { | |
| 1893 | + return catalog; | |
| 1894 | + } | |
| 1895 | + | |
| 1896 | + const preview = violations | |
| 1897 | + .slice(0, 5) | |
| 1898 | + .map((violation) => `${violation.code}: ${violation.message}`) | |
| 1899 | + .join("\n"); | |
| 1900 | + | |
| 1901 | + throw new Error( | |
| 1902 | + [ | |
| 1903 | + `Invalid IR catalog (${violations.length} invariant violation${violations.length === 1 ? "" : "s"}).`, | |
| 1904 | + preview, | |
| 1905 | + ...(violations.length > 5 ? [`...and ${String(violations.length - 5)} more`] : []), | |
| 1906 | + ].join("\n"), | |
| 1907 | + ); | |
| 1908 | +}; | |
| 1909 | + | |
| 1910 | +export const projectCatalogForAgentSdk = (input: { | |
| 1911 | + catalog: CatalogV1; | |
| 1912 | +}): ProjectedCatalog => { | |
| 1913 | + const workingCatalog = cloneCatalog(input.catalog); | |
| 1914 | + const toolDescriptors = {} as Record<CapabilityId, ToolDescriptor>; | |
| 1915 | + const searchDocs = {} as Record<CapabilityId, ToolSearchDoc>; | |
| 1916 | + const capabilityViews = {} as Record<CapabilityId, CapabilitySummaryView>; | |
| 1917 | + | |
| 1918 | + const capabilities = Object.values(workingCatalog.capabilities) | |
| 1919 | + .sort((left, right) => left.id.localeCompare(right.id)); | |
| 1920 | + | |
| 1921 | + for (const capability of capabilities) { | |
| 1922 | + const descriptor = projectCapability(workingCatalog, capability); | |
| 1923 | + toolDescriptors[capability.id] = descriptor; | |
| 1924 | + searchDocs[capability.id] = searchDoc(workingCatalog, capability); | |
| 1925 | + capabilityViews[capability.id] = summaryView(workingCatalog, capability, descriptor); | |
| 1926 | + } | |
| 1927 | + | |
| 1928 | + return { | |
| 1929 | + catalog: workingCatalog, | |
| 1930 | + toolDescriptors, | |
| 1931 | + searchDocs, | |
| 1932 | + capabilityViews, | |
| 1933 | + }; | |
| 1934 | +}; | |
| 1935 | + | |
| 1936 | +export const projectSymbolShallowView = ( | |
| 1937 | + catalog: CatalogV1, | |
| 1938 | + symbolId: SymbolId, | |
| 1939 | +): SymbolShallowView => { | |
| 1940 | + const symbol = catalog.symbols[symbolId]; | |
| 1941 | + if (!symbol) { | |
| 1942 | + throw new Error(`Unknown symbol ${symbolId}`); | |
| 1943 | + } | |
| 1944 | + | |
| 1945 | + return { | |
| 1946 | + symbolId, | |
| 1947 | + kind: symbol.kind, | |
| 1948 | + ...(symbolTitle(symbol) ? { title: symbolTitle(symbol) } : {}), | |
| 1949 | + ...(symbolSummary(symbol) ? { summary: symbolSummary(symbol) } : {}), | |
| 1950 | + edges: symbolEdges(symbol), | |
| 1951 | + }; | |
| 1952 | +}; | |
| 1953 | + | |
| 1954 | +export const projectSymbolExpandedView = ( | |
| 1955 | + catalog: CatalogV1, | |
| 1956 | + symbolId: SymbolId, | |
| 1957 | +): SymbolExpandedView => { | |
| 1958 | + const symbol = catalog.symbols[symbolId]; | |
| 1959 | + if (!symbol) { | |
| 1960 | + throw new Error(`Unknown symbol ${symbolId}`); | |
| 1961 | + } | |
| 1962 | + | |
| 1963 | + return { | |
| 1964 | + symbolId, | |
| 1965 | + symbol, | |
| 1966 | + ...(symbol.diagnosticIds | |
| 1967 | + ? { | |
| 1968 | + diagnostics: symbol.diagnosticIds | |
| 1969 | + .map((diagnosticId) => catalog.diagnostics[diagnosticId]) | |
| 1970 | + .filter((diagnostic): diagnostic is ImportDiagnostic => diagnostic !== undefined), | |
| 1971 | + } | |
| 1972 | + : {}), | |
| 1973 | + }; | |
| 1974 | +}; | |
| 1975 | + | |
| 1976 | +export const buildInvocationPlan = (input: { | |
| 1977 | + catalog: CatalogV1; | |
| 1978 | + capabilityId: CapabilityId; | |
| 1979 | + executableId?: ExecutableId; | |
| 1980 | + connectionBindingId: string; | |
| 1981 | + resolvedEndpoint?: string; | |
| 1982 | + resolvedAuth?: unknown; | |
| 1983 | + resolvedInteraction?: Partial<InvocationPlan["resolvedInteraction"]>; | |
| 1984 | + requestPlan: unknown; | |
| 1985 | +}): InvocationPlan => { | |
| 1986 | + const capability = input.catalog.capabilities[input.capabilityId]; | |
| 1987 | + if (!capability) { | |
| 1988 | + throw new Error(`Unknown capability ${input.capabilityId}`); | |
| 1989 | + } | |
| 1990 | + | |
| 1991 | + const executable = input.executableId | |
| 1992 | + ? input.catalog.executables[input.executableId] | |
| 1993 | + : chooseExecutable(input.catalog, capability); | |
| 1994 | + if (!executable) { | |
| 1995 | + throw new Error(`Unknown executable for capability ${input.capabilityId}`); | |
| 1996 | + } | |
| 1997 | + | |
| 1998 | + const scopes = scopeChain(input.catalog, executable.scopeId).map((scope) => scope.id); | |
| 1999 | + | |
| 2000 | + return { | |
| 2001 | + capabilityId: capability.id, | |
| 2002 | + executableId: executable.id, | |
| 2003 | + connectionBindingId: input.connectionBindingId, | |
| 2004 | + resolvedScopeChain: scopes, | |
| 2005 | + ...(input.resolvedEndpoint ? { resolvedEndpoint: input.resolvedEndpoint } : {}), | |
| 2006 | + ...(input.resolvedAuth !== undefined ? { resolvedAuth: input.resolvedAuth } : {}), | |
| 2007 | + resolvedInteraction: { | |
| 2008 | + requiresApproval: input.resolvedInteraction?.requiresApproval | |
| 2009 | + ?? capability.interaction.approval.mayRequire, | |
| 2010 | + mayPauseForElicitation: input.resolvedInteraction?.mayPauseForElicitation | |
| 2011 | + ?? capability.interaction.elicitation.mayRequest, | |
| 2012 | + resumable: input.resolvedInteraction?.resumable | |
| 2013 | + ?? capability.interaction.resume.supported, | |
| 2014 | + }, | |
| 2015 | + requestPlan: input.requestPlan, | |
| 2016 | + provenance: capability.provenance, | |
| 2017 | + }; | |
| 2018 | +}; | |
| @@ -0,0 +1,58 @@ | ||
| 1 | +import { Schema } from "effect"; | |
| 2 | + | |
| 3 | +export const DocumentIdSchema = Schema.String.pipe(Schema.brand("DocumentId")); | |
| 4 | +export const ResourceIdSchema = Schema.String.pipe(Schema.brand("ResourceId")); | |
| 5 | +export const ScopeIdSchema = Schema.String.pipe(Schema.brand("ScopeId")); | |
| 6 | +export const CapabilityIdSchema = Schema.String.pipe(Schema.brand("CapabilityId")); | |
| 7 | +export const ExecutableIdSchema = Schema.String.pipe(Schema.brand("ExecutableId")); | |
| 8 | +export const ResponseSetIdSchema = Schema.String.pipe(Schema.brand("ResponseSetId")); | |
| 9 | +export const DiagnosticIdSchema = Schema.String.pipe(Schema.brand("DiagnosticId")); | |
| 10 | + | |
| 11 | +export const ShapeSymbolIdSchema = Schema.String.pipe(Schema.brand("ShapeSymbolId")); | |
| 12 | +export const ParameterSymbolIdSchema = Schema.String.pipe(Schema.brand("ParameterSymbolId")); | |
| 13 | +export const RequestBodySymbolIdSchema = Schema.String.pipe( | |
| 14 | + Schema.brand("RequestBodySymbolId"), | |
| 15 | +); | |
| 16 | +export const ResponseSymbolIdSchema = Schema.String.pipe( | |
| 17 | + Schema.brand("ResponseSymbolId"), | |
| 18 | +); | |
| 19 | +export const HeaderSymbolIdSchema = Schema.String.pipe(Schema.brand("HeaderSymbolId")); | |
| 20 | +export const ExampleSymbolIdSchema = Schema.String.pipe(Schema.brand("ExampleSymbolId")); | |
| 21 | +export const SecuritySchemeSymbolIdSchema = Schema.String.pipe( | |
| 22 | + Schema.brand("SecuritySchemeSymbolId"), | |
| 23 | +); | |
| 24 | + | |
| 25 | +export type DocumentId = typeof DocumentIdSchema.Type; | |
| 26 | +export type ResourceId = typeof ResourceIdSchema.Type; | |
| 27 | +export type ScopeId = typeof ScopeIdSchema.Type; | |
| 28 | +export type CapabilityId = typeof CapabilityIdSchema.Type; | |
| 29 | +export type ExecutableId = typeof ExecutableIdSchema.Type; | |
| 30 | +export type ResponseSetId = typeof ResponseSetIdSchema.Type; | |
| 31 | +export type DiagnosticId = typeof DiagnosticIdSchema.Type; | |
| 32 | + | |
| 33 | +export type ShapeSymbolId = typeof ShapeSymbolIdSchema.Type; | |
| 34 | +export type ParameterSymbolId = typeof ParameterSymbolIdSchema.Type; | |
| 35 | +export type RequestBodySymbolId = typeof RequestBodySymbolIdSchema.Type; | |
| 36 | +export type ResponseSymbolId = typeof ResponseSymbolIdSchema.Type; | |
| 37 | +export type HeaderSymbolId = typeof HeaderSymbolIdSchema.Type; | |
| 38 | +export type ExampleSymbolId = typeof ExampleSymbolIdSchema.Type; | |
| 39 | +export type SecuritySchemeSymbolId = typeof SecuritySchemeSymbolIdSchema.Type; | |
| 40 | + | |
| 41 | +export type SymbolId = | |
| 42 | + | ShapeSymbolId | |
| 43 | + | ParameterSymbolId | |
| 44 | + | RequestBodySymbolId | |
| 45 | + | ResponseSymbolId | |
| 46 | + | HeaderSymbolId | |
| 47 | + | ExampleSymbolId | |
| 48 | + | SecuritySchemeSymbolId; | |
| 49 | + | |
| 50 | +export const SymbolIdSchema = Schema.Union( | |
| 51 | + ShapeSymbolIdSchema, | |
| 52 | + ParameterSymbolIdSchema, | |
| 53 | + RequestBodySymbolIdSchema, | |
| 54 | + ResponseSymbolIdSchema, | |
| 55 | + HeaderSymbolIdSchema, | |
| 56 | + ExampleSymbolIdSchema, | |
| 57 | + SecuritySchemeSymbolIdSchema, | |
| 58 | +); | |
| @@ -0,0 +1,3 @@ | ||
| 1 | +export * from "./ids"; | |
| 2 | +export * from "./model"; | |
| 3 | +export * from "./catalog"; | |
| @@ -0,0 +1,971 @@ | ||
| 1 | +import { Schema } from "effect"; | |
| 2 | + | |
| 3 | +import { | |
| 4 | + CapabilityIdSchema, | |
| 5 | + type CapabilityId, | |
| 6 | + DiagnosticIdSchema, | |
| 7 | + type DiagnosticId, | |
| 8 | + DocumentIdSchema, | |
| 9 | + type DocumentId, | |
| 10 | + ExecutableIdSchema, | |
| 11 | + type ExecutableId, | |
| 12 | + ExampleSymbolIdSchema, | |
| 13 | + type ExampleSymbolId, | |
| 14 | + HeaderSymbolIdSchema, | |
| 15 | + type HeaderSymbolId, | |
| 16 | + ParameterSymbolIdSchema, | |
| 17 | + type ParameterSymbolId, | |
| 18 | + RequestBodySymbolIdSchema, | |
| 19 | + type RequestBodySymbolId, | |
| 20 | + ResourceIdSchema, | |
| 21 | + type ResourceId, | |
| 22 | + ResponseSetIdSchema, | |
| 23 | + type ResponseSetId, | |
| 24 | + ResponseSymbolIdSchema, | |
| 25 | + type ResponseSymbolId, | |
| 26 | + ScopeIdSchema, | |
| 27 | + type ScopeId, | |
| 28 | + SecuritySchemeSymbolIdSchema, | |
| 29 | + type SecuritySchemeSymbolId, | |
| 30 | + ShapeSymbolIdSchema, | |
| 31 | + type ShapeSymbolId, | |
| 32 | + SymbolIdSchema, | |
| 33 | + type SymbolId, | |
| 34 | +} from "./ids"; | |
| 35 | + | |
| 36 | +export const SourceKindSchema = Schema.Literal( | |
| 37 | + "openapi", | |
| 38 | + "graphql-schema", | |
| 39 | + "google-discovery", | |
| 40 | + "mcp", | |
| 41 | + "custom", | |
| 42 | +); | |
| 43 | + | |
| 44 | +export const ScopeKindSchema = Schema.Literal( | |
| 45 | + "service", | |
| 46 | + "document", | |
| 47 | + "resource", | |
| 48 | + "pathItem", | |
| 49 | + "operation", | |
| 50 | + "folder", | |
| 51 | +); | |
| 52 | + | |
| 53 | +export const EffectKindSchema = Schema.Literal( | |
| 54 | + "read", | |
| 55 | + "write", | |
| 56 | + "delete", | |
| 57 | + "action", | |
| 58 | + "subscribe", | |
| 59 | +); | |
| 60 | + | |
| 61 | +export const ParameterLocationSchema = Schema.Literal( | |
| 62 | + "path", | |
| 63 | + "query", | |
| 64 | + "header", | |
| 65 | + "cookie", | |
| 66 | +); | |
| 67 | + | |
| 68 | +export const ResponseTraitSchema = Schema.Literal( | |
| 69 | + "success", | |
| 70 | + "redirect", | |
| 71 | + "stream", | |
| 72 | + "download", | |
| 73 | + "upload", | |
| 74 | + "longRunning", | |
| 75 | +); | |
| 76 | + | |
| 77 | +export const SecuritySchemeTypeSchema = Schema.Literal( | |
| 78 | + "oauth2", | |
| 79 | + "http", | |
| 80 | + "apiKey", | |
| 81 | + "basic", | |
| 82 | + "bearer", | |
| 83 | + "custom", | |
| 84 | +); | |
| 85 | + | |
| 86 | +export const SelectionModeSchema = Schema.Literal( | |
| 87 | + "caller", | |
| 88 | + "fixed", | |
| 89 | + "persisted", | |
| 90 | +); | |
| 91 | + | |
| 92 | +export const GraphQLToolKindSchema = Schema.Literal( | |
| 93 | + "request", | |
| 94 | + "field", | |
| 95 | +); | |
| 96 | + | |
| 97 | +export const StatusRangeSchema = Schema.Literal( | |
| 98 | + "1XX", | |
| 99 | + "2XX", | |
| 100 | + "3XX", | |
| 101 | + "4XX", | |
| 102 | + "5XX", | |
| 103 | +); | |
| 104 | + | |
| 105 | +export const PaginationHintKindSchema = Schema.Literal( | |
| 106 | + "cursor", | |
| 107 | + "offset", | |
| 108 | + "token", | |
| 109 | + "unknown", | |
| 110 | +); | |
| 111 | + | |
| 112 | +export const DiagnosticLevelSchema = Schema.Literal( | |
| 113 | + "info", | |
| 114 | + "warning", | |
| 115 | + "error", | |
| 116 | +); | |
| 117 | + | |
| 118 | +export const DiagnosticCodeSchema = Schema.Literal( | |
| 119 | + "external_ref_bundled", | |
| 120 | + "relative_ref_rebased", | |
| 121 | + "schema_hoisted", | |
| 122 | + "selection_shape_synthesized", | |
| 123 | + "opaque_hook_imported", | |
| 124 | + "discriminator_lost", | |
| 125 | + "multi_response_union_synthesized", | |
| 126 | + "unsupported_link_preserved_native", | |
| 127 | + "unsupported_callback_preserved_native", | |
| 128 | + "unresolved_ref", | |
| 129 | + "merge_conflict_preserved_first", | |
| 130 | + "projection_call_shape_synthesized", | |
| 131 | + "projection_result_shape_synthesized", | |
| 132 | + "projection_collision_grouped_fields", | |
| 133 | + "synthetic_resource_context_created", | |
| 134 | + "selection_shape_missing", | |
| 135 | +); | |
| 136 | + | |
| 137 | +export const NativeEncodingSchema = Schema.Literal( | |
| 138 | + "json", | |
| 139 | + "yaml", | |
| 140 | + "graphql", | |
| 141 | + "text", | |
| 142 | + "unknown", | |
| 143 | +); | |
| 144 | + | |
| 145 | +export const ProvenanceRelationSchema = Schema.Literal( | |
| 146 | + "declared", | |
| 147 | + "hoisted", | |
| 148 | + "derived", | |
| 149 | + "merged", | |
| 150 | + "projected", | |
| 151 | +); | |
| 152 | + | |
| 153 | +export const DocumentationBlockSchema = Schema.Struct({ | |
| 154 | + summary: Schema.optional(Schema.String), | |
| 155 | + description: Schema.optional(Schema.String), | |
| 156 | + externalDocsUrl: Schema.optional(Schema.String), | |
| 157 | +}); | |
| 158 | + | |
| 159 | +export const ProvenanceRefSchema = Schema.Struct({ | |
| 160 | + relation: ProvenanceRelationSchema, | |
| 161 | + documentId: DocumentIdSchema, | |
| 162 | + resourceId: Schema.optional(ResourceIdSchema), | |
| 163 | + pointer: Schema.optional(Schema.String), | |
| 164 | + line: Schema.optional(Schema.Number), | |
| 165 | + column: Schema.optional(Schema.Number), | |
| 166 | +}); | |
| 167 | + | |
| 168 | +export const NativeBlobSchema = Schema.Struct({ | |
| 169 | + sourceKind: SourceKindSchema, | |
| 170 | + kind: Schema.String, | |
| 171 | + pointer: Schema.optional(Schema.String), | |
| 172 | + encoding: Schema.optional(NativeEncodingSchema), | |
| 173 | + summary: Schema.optional(Schema.String), | |
| 174 | + value: Schema.optional(Schema.Unknown), | |
| 175 | +}); | |
| 176 | + | |
| 177 | +export const EntityBaseSchema = Schema.Struct({ | |
| 178 | + synthetic: Schema.Boolean, | |
| 179 | + provenance: Schema.Array(ProvenanceRefSchema), | |
| 180 | + diagnosticIds: Schema.optional(Schema.Array(DiagnosticIdSchema)), | |
| 181 | + native: Schema.optional(Schema.Array(NativeBlobSchema)), | |
| 182 | +}); | |
| 183 | + | |
| 184 | +export const ImportMetadataSchema = Schema.Struct({ | |
| 185 | + sourceKind: SourceKindSchema, | |
| 186 | + adapterKey: Schema.String, | |
| 187 | + importerVersion: Schema.String, | |
| 188 | + importedAt: Schema.String, | |
| 189 | + sourceConfigHash: Schema.String, | |
| 190 | +}); | |
| 191 | + | |
| 192 | +export const SourceDocumentSchema = Schema.Struct({ | |
| 193 | + id: DocumentIdSchema, | |
| 194 | + kind: SourceKindSchema, | |
| 195 | + title: Schema.optional(Schema.String), | |
| 196 | + versionHint: Schema.optional(Schema.String), | |
| 197 | + fetchedAt: Schema.String, | |
| 198 | + rawRef: Schema.String, | |
| 199 | + entryUri: Schema.optional(Schema.String), | |
| 200 | + native: Schema.optional(Schema.Array(NativeBlobSchema)), | |
| 201 | +}); | |
| 202 | + | |
| 203 | +export const FieldSpecSchema = Schema.Struct({ | |
| 204 | + shapeId: ShapeSymbolIdSchema, | |
| 205 | + docs: Schema.optional(DocumentationBlockSchema), | |
| 206 | + deprecated: Schema.optional(Schema.Boolean), | |
| 207 | + exampleIds: Schema.optional(Schema.Array(ExampleSymbolIdSchema)), | |
| 208 | +}); | |
| 209 | + | |
| 210 | +export const DiscriminatorSpecSchema = Schema.Struct({ | |
| 211 | + propertyName: Schema.String, | |
| 212 | + mapping: Schema.optional( | |
| 213 | + Schema.Record({ | |
| 214 | + key: Schema.String, | |
| 215 | + value: ShapeSymbolIdSchema, | |
| 216 | + }), | |
| 217 | + ), | |
| 218 | +}); | |
| 219 | + | |
| 220 | +export const ScalarShapeSchema = Schema.Struct({ | |
| 221 | + type: Schema.Literal("scalar"), | |
| 222 | + scalar: Schema.Literal("string", "number", "integer", "boolean", "null", "bytes"), | |
| 223 | + format: Schema.optional(Schema.String), | |
| 224 | + constraints: Schema.optional( | |
| 225 | + Schema.Record({ | |
| 226 | + key: Schema.String, | |
| 227 | + value: Schema.Unknown, | |
| 228 | + }), | |
| 229 | + ), | |
| 230 | +}); | |
| 231 | + | |
| 232 | +export const UnknownShapeSchema = Schema.Struct({ | |
| 233 | + type: Schema.Literal("unknown"), | |
| 234 | + reason: Schema.optional(Schema.String), | |
| 235 | +}); | |
| 236 | + | |
| 237 | +export const ConstShapeSchema = Schema.Struct({ | |
| 238 | + type: Schema.Literal("const"), | |
| 239 | + value: Schema.Unknown, | |
| 240 | +}); | |
| 241 | + | |
| 242 | +export const EnumShapeSchema = Schema.Struct({ | |
| 243 | + type: Schema.Literal("enum"), | |
| 244 | + values: Schema.Array(Schema.Unknown), | |
| 245 | +}); | |
| 246 | + | |
| 247 | +export const ObjectShapeSchema = Schema.Struct({ | |
| 248 | + type: Schema.Literal("object"), | |
| 249 | + fields: Schema.Record({ | |
| 250 | + key: Schema.String, | |
| 251 | + value: FieldSpecSchema, | |
| 252 | + }), | |
| 253 | + required: Schema.optional(Schema.Array(Schema.String)), | |
| 254 | + additionalProperties: Schema.optional( | |
| 255 | + Schema.Union(Schema.Boolean, ShapeSymbolIdSchema), | |
| 256 | + ), | |
| 257 | + patternProperties: Schema.optional( | |
| 258 | + Schema.Record({ | |
| 259 | + key: Schema.String, | |
| 260 | + value: ShapeSymbolIdSchema, | |
| 261 | + }), | |
| 262 | + ), | |
| 263 | +}); | |
| 264 | + | |
| 265 | +export const ArrayShapeSchema = Schema.Struct({ | |
| 266 | + type: Schema.Literal("array"), | |
| 267 | + itemShapeId: ShapeSymbolIdSchema, | |
| 268 | + minItems: Schema.optional(Schema.Number), | |
| 269 | + maxItems: Schema.optional(Schema.Number), | |
| 270 | +}); | |
| 271 | + | |
| 272 | +export const TupleShapeSchema = Schema.Struct({ | |
| 273 | + type: Schema.Literal("tuple"), | |
| 274 | + itemShapeIds: Schema.Array(ShapeSymbolIdSchema), | |
| 275 | + additionalItems: Schema.optional( | |
| 276 | + Schema.Union(Schema.Boolean, ShapeSymbolIdSchema), | |
| 277 | + ), | |
| 278 | +}); | |
| 279 | + | |
| 280 | +export const MapShapeSchema = Schema.Struct({ | |
| 281 | + type: Schema.Literal("map"), | |
| 282 | + valueShapeId: ShapeSymbolIdSchema, | |
| 283 | +}); | |
| 284 | + | |
| 285 | +export const AllOfShapeSchema = Schema.Struct({ | |
| 286 | + type: Schema.Literal("allOf"), | |
| 287 | + items: Schema.Array(ShapeSymbolIdSchema), | |
| 288 | +}); | |
| 289 | + | |
| 290 | +export const AnyOfShapeSchema = Schema.Struct({ | |
| 291 | + type: Schema.Literal("anyOf"), | |
| 292 | + items: Schema.Array(ShapeSymbolIdSchema), | |
| 293 | +}); | |
| 294 | + | |
| 295 | +export const OneOfShapeSchema = Schema.Struct({ | |
| 296 | + type: Schema.Literal("oneOf"), | |
| 297 | + items: Schema.Array(ShapeSymbolIdSchema), | |
| 298 | + discriminator: Schema.optional(DiscriminatorSpecSchema), | |
| 299 | +}); | |
| 300 | + | |
| 301 | +export const NullableShapeSchema = Schema.Struct({ | |
| 302 | + type: Schema.Literal("nullable"), | |
| 303 | + itemShapeId: ShapeSymbolIdSchema, | |
| 304 | +}); | |
| 305 | + | |
| 306 | +export const RefShapeSchema = Schema.Struct({ | |
| 307 | + type: Schema.Literal("ref"), | |
| 308 | + target: ShapeSymbolIdSchema, | |
| 309 | +}); | |
| 310 | + | |
| 311 | +export const NotShapeSchema = Schema.Struct({ | |
| 312 | + type: Schema.Literal("not"), | |
| 313 | + itemShapeId: ShapeSymbolIdSchema, | |
| 314 | +}); | |
| 315 | + | |
| 316 | +export const ConditionalShapeSchema = Schema.Struct({ | |
| 317 | + type: Schema.Literal("conditional"), | |
| 318 | + ifShapeId: ShapeSymbolIdSchema, | |
| 319 | + thenShapeId: Schema.optional(ShapeSymbolIdSchema), | |
| 320 | + elseShapeId: Schema.optional(ShapeSymbolIdSchema), | |
| 321 | +}); | |
| 322 | + | |
| 323 | +export const GraphQLInterfaceShapeSchema = Schema.Struct({ | |
| 324 | + type: Schema.Literal("graphqlInterface"), | |
| 325 | + fields: Schema.Record({ | |
| 326 | + key: Schema.String, | |
| 327 | + value: FieldSpecSchema, | |
| 328 | + }), | |
| 329 | + possibleTypeIds: Schema.Array(ShapeSymbolIdSchema), | |
| 330 | +}); | |
| 331 | + | |
| 332 | +export const GraphQLUnionShapeSchema = Schema.Struct({ | |
| 333 | + type: Schema.Literal("graphqlUnion"), | |
| 334 | + memberTypeIds: Schema.Array(ShapeSymbolIdSchema), | |
| 335 | +}); | |
| 336 | + | |
| 337 | +export const ShapeNodeSchema = Schema.Union( | |
| 338 | + UnknownShapeSchema, | |
| 339 | + ScalarShapeSchema, | |
| 340 | + ConstShapeSchema, | |
| 341 | + EnumShapeSchema, | |
| 342 | + ObjectShapeSchema, | |
| 343 | + ArrayShapeSchema, | |
| 344 | + TupleShapeSchema, | |
| 345 | + MapShapeSchema, | |
| 346 | + AllOfShapeSchema, | |
| 347 | + AnyOfShapeSchema, | |
| 348 | + OneOfShapeSchema, | |
| 349 | + NullableShapeSchema, | |
| 350 | + RefShapeSchema, | |
| 351 | + NotShapeSchema, | |
| 352 | + ConditionalShapeSchema, | |
| 353 | + GraphQLInterfaceShapeSchema, | |
| 354 | + GraphQLUnionShapeSchema, | |
| 355 | +); | |
| 356 | + | |
| 357 | +export const AnchorTargetSchema = Schema.Struct({ | |
| 358 | + shapeId: ShapeSymbolIdSchema, | |
| 359 | + pointer: Schema.optional(Schema.String), | |
| 360 | +}); | |
| 361 | + | |
| 362 | +export const SchemaResourceSchema = Schema.extend( | |
| 363 | + Schema.Struct({ | |
| 364 | + id: ResourceIdSchema, | |
| 365 | + documentId: DocumentIdSchema, | |
| 366 | + canonicalUri: Schema.String, | |
| 367 | + baseUri: Schema.String, | |
| 368 | + dialectUri: Schema.optional(Schema.String), | |
| 369 | + rootShapeId: Schema.optional(ShapeSymbolIdSchema), | |
| 370 | + anchors: Schema.Record({ | |
| 371 | + key: Schema.String, | |
| 372 | + value: AnchorTargetSchema, | |
| 373 | + }), | |
| 374 | + dynamicAnchors: Schema.Record({ | |
| 375 | + key: Schema.String, | |
| 376 | + value: AnchorTargetSchema, | |
| 377 | + }), | |
| 378 | + }), | |
| 379 | + EntityBaseSchema, | |
| 380 | +); | |
| 381 | + | |
| 382 | +export const RequestPlacementHintSchema = Schema.Union( | |
| 383 | + Schema.Struct({ | |
| 384 | + location: Schema.Literal("header", "query", "cookie"), | |
| 385 | + name: Schema.String, | |
| 386 | + }), | |
| 387 | + Schema.Struct({ | |
| 388 | + location: Schema.Literal("body"), | |
| 389 | + path: Schema.String, | |
| 390 | + }), | |
| 391 | +); | |
| 392 | + | |
| 393 | +export const OAuthFlowSchema = Schema.Struct({ | |
| 394 | + authorizationUrl: Schema.optional(Schema.String), | |
| 395 | + tokenUrl: Schema.optional(Schema.String), | |
| 396 | + refreshUrl: Schema.optional(Schema.String), | |
| 397 | + scopes: Schema.optional( | |
| 398 | + Schema.Record({ | |
| 399 | + key: Schema.String, | |
| 400 | + value: Schema.String, | |
| 401 | + }), | |
| 402 | + ), | |
| 403 | +}); | |
| 404 | + | |
| 405 | +export const SecurityPlacementSchema = Schema.Struct({ | |
| 406 | + in: Schema.optional(Schema.Literal("header", "query", "cookie")), | |
| 407 | + name: Schema.optional(Schema.String), | |
| 408 | +}); | |
| 409 | + | |
| 410 | +export const SecuritySchemeSymbolSchema = Schema.extend( | |
| 411 | + Schema.Struct({ | |
| 412 | + id: SecuritySchemeSymbolIdSchema, | |
| 413 | + kind: Schema.Literal("securityScheme"), | |
| 414 | + schemeType: SecuritySchemeTypeSchema, | |
| 415 | + docs: Schema.optional(DocumentationBlockSchema), | |
| 416 | + placement: Schema.optional(SecurityPlacementSchema), | |
| 417 | + http: Schema.optional( | |
| 418 | + Schema.Struct({ | |
| 419 | + scheme: Schema.String, | |
| 420 | + bearerFormat: Schema.optional(Schema.String), | |
| 421 | + }), | |
| 422 | + ), | |
| 423 | + apiKey: Schema.optional( | |
| 424 | + Schema.Struct({ | |
| 425 | + in: Schema.Literal("header", "query", "cookie"), | |
| 426 | + name: Schema.String, | |
| 427 | + }), | |
| 428 | + ), | |
| 429 | + oauth: Schema.optional( | |
| 430 | + Schema.Struct({ | |
| 431 | + flows: Schema.optional( | |
| 432 | + Schema.Record({ | |
| 433 | + key: Schema.String, | |
| 434 | + value: OAuthFlowSchema, | |
| 435 | + }), | |
| 436 | + ), | |
| 437 | + scopes: Schema.optional( | |
| 438 | + Schema.Record({ | |
| 439 | + key: Schema.String, | |
| 440 | + value: Schema.String, | |
| 441 | + }), | |
| 442 | + ), | |
| 443 | + }), | |
| 444 | + ), | |
| 445 | + custom: Schema.optional( | |
| 446 | + Schema.Struct({ | |
| 447 | + placementHints: Schema.optional(Schema.Array(RequestPlacementHintSchema)), | |
| 448 | + }), | |
| 449 | + ), | |
| 450 | + }), | |
| 451 | + EntityBaseSchema, | |
| 452 | +); | |
| 453 | + | |
| 454 | +export const EncodingSpecSchema = Schema.Struct({ | |
| 455 | + contentType: Schema.optional(Schema.String), | |
| 456 | + style: Schema.optional(Schema.String), | |
| 457 | + explode: Schema.optional(Schema.Boolean), | |
| 458 | + allowReserved: Schema.optional(Schema.Boolean), | |
| 459 | + headers: Schema.optional(Schema.Array(HeaderSymbolIdSchema)), | |
| 460 | +}); | |
| 461 | + | |
| 462 | +export const ContentSpecSchema = Schema.Struct({ | |
| 463 | + mediaType: Schema.String, | |
| 464 | + shapeId: Schema.optional(ShapeSymbolIdSchema), | |
| 465 | + exampleIds: Schema.optional(Schema.Array(ExampleSymbolIdSchema)), | |
| 466 | + encoding: Schema.optional( | |
| 467 | + Schema.Record({ | |
| 468 | + key: Schema.String, | |
| 469 | + value: EncodingSpecSchema, | |
| 470 | + }), | |
| 471 | + ), | |
| 472 | +}); | |
| 473 | + | |
| 474 | +export const ShapeSymbolSchema = Schema.extend( | |
| 475 | + Schema.Struct({ | |
| 476 | + id: ShapeSymbolIdSchema, | |
| 477 | + kind: Schema.Literal("shape"), | |
| 478 | + resourceId: Schema.optional(ResourceIdSchema), | |
| 479 | + title: Schema.optional(Schema.String), | |
| 480 | + docs: Schema.optional(DocumentationBlockSchema), | |
| 481 | + deprecated: Schema.optional(Schema.Boolean), | |
| 482 | + node: ShapeNodeSchema, | |
| 483 | + }), | |
| 484 | + EntityBaseSchema, | |
| 485 | +); | |
| 486 | + | |
| 487 | +export const ParameterSymbolSchema = Schema.extend( | |
| 488 | + Schema.Struct({ | |
| 489 | + id: ParameterSymbolIdSchema, | |
| 490 | + kind: Schema.Literal("parameter"), | |
| 491 | + name: Schema.String, | |
| 492 | + location: ParameterLocationSchema, | |
| 493 | + required: Schema.optional(Schema.Boolean), | |
| 494 | + docs: Schema.optional(DocumentationBlockSchema), | |
| 495 | + deprecated: Schema.optional(Schema.Boolean), | |
| 496 | + exampleIds: Schema.optional(Schema.Array(ExampleSymbolIdSchema)), | |
| 497 | + schemaShapeId: Schema.optional(ShapeSymbolIdSchema), | |
| 498 | + content: Schema.optional(Schema.Array(ContentSpecSchema)), | |
| 499 | + style: Schema.optional(Schema.String), | |
| 500 | + explode: Schema.optional(Schema.Boolean), | |
| 501 | + allowReserved: Schema.optional(Schema.Boolean), | |
| 502 | + }), | |
| 503 | + EntityBaseSchema, | |
| 504 | +); | |
| 505 | + | |
| 506 | +export const HeaderSymbolSchema = Schema.extend( | |
| 507 | + Schema.Struct({ | |
| 508 | + id: HeaderSymbolIdSchema, | |
| 509 | + kind: Schema.Literal("header"), | |
| 510 | + name: Schema.String, | |
| 511 | + docs: Schema.optional(DocumentationBlockSchema), | |
| 512 | + deprecated: Schema.optional(Schema.Boolean), | |
| 513 | + exampleIds: Schema.optional(Schema.Array(ExampleSymbolIdSchema)), | |
| 514 | + schemaShapeId: Schema.optional(ShapeSymbolIdSchema), | |
| 515 | + content: Schema.optional(Schema.Array(ContentSpecSchema)), | |
| 516 | + style: Schema.optional(Schema.String), | |
| 517 | + explode: Schema.optional(Schema.Boolean), | |
| 518 | + }), | |
| 519 | + EntityBaseSchema, | |
| 520 | +); | |
| 521 | + | |
| 522 | +export const RequestBodySymbolSchema = Schema.extend( | |
| 523 | + Schema.Struct({ | |
| 524 | + id: RequestBodySymbolIdSchema, | |
| 525 | + kind: Schema.Literal("requestBody"), | |
| 526 | + docs: Schema.optional(DocumentationBlockSchema), | |
| 527 | + required: Schema.optional(Schema.Boolean), | |
| 528 | + contents: Schema.Array(ContentSpecSchema), | |
| 529 | + }), | |
| 530 | + EntityBaseSchema, | |
| 531 | +); | |
| 532 | + | |
| 533 | +export const ResponseSymbolSchema = Schema.extend( | |
| 534 | + Schema.Struct({ | |
| 535 | + id: ResponseSymbolIdSchema, | |
| 536 | + kind: Schema.Literal("response"), | |
| 537 | + docs: Schema.optional(DocumentationBlockSchema), | |
| 538 | + headerIds: Schema.optional(Schema.Array(HeaderSymbolIdSchema)), | |
| 539 | + contents: Schema.optional(Schema.Array(ContentSpecSchema)), | |
| 540 | + }), | |
| 541 | + EntityBaseSchema, | |
| 542 | +); | |
| 543 | + | |
| 544 | +export const ExampleSymbolSchema = Schema.extend( | |
| 545 | + Schema.Struct({ | |
| 546 | + id: ExampleSymbolIdSchema, | |
| 547 | + kind: Schema.Literal("example"), | |
| 548 | + name: Schema.optional(Schema.String), | |
| 549 | + docs: Schema.optional(DocumentationBlockSchema), | |
| 550 | + exampleKind: Schema.Literal("value", "call"), | |
| 551 | + value: Schema.optional(Schema.Unknown), | |
| 552 | + externalValue: Schema.optional(Schema.String), | |
| 553 | + call: Schema.optional( | |
| 554 | + Schema.Struct({ | |
| 555 | + args: Schema.Record({ | |
| 556 | + key: Schema.String, | |
| 557 | + value: Schema.Unknown, | |
| 558 | + }), | |
| 559 | + result: Schema.optional(Schema.Unknown), | |
| 560 | + }), | |
| 561 | + ), | |
| 562 | + }), | |
| 563 | + EntityBaseSchema, | |
| 564 | +); | |
| 565 | + | |
| 566 | +export const SymbolSchema = Schema.Union( | |
| 567 | + ShapeSymbolSchema, | |
| 568 | + ParameterSymbolSchema, | |
| 569 | + RequestBodySymbolSchema, | |
| 570 | + ResponseSymbolSchema, | |
| 571 | + HeaderSymbolSchema, | |
| 572 | + ExampleSymbolSchema, | |
| 573 | + SecuritySchemeSymbolSchema, | |
| 574 | +); | |
| 575 | + | |
| 576 | +export type AuthRequirement = | |
| 577 | + | { | |
| 578 | + readonly kind: "none"; | |
| 579 | + } | |
| 580 | + | { | |
| 581 | + readonly kind: "scheme"; | |
| 582 | + readonly schemeId: SecuritySchemeSymbolId; | |
| 583 | + readonly scopes?: readonly string[]; | |
| 584 | + } | |
| 585 | + | { | |
| 586 | + readonly kind: "allOf"; | |
| 587 | + readonly items: readonly AuthRequirement[]; | |
| 588 | + } | |
| 589 | + | { | |
| 590 | + readonly kind: "anyOf"; | |
| 591 | + readonly items: readonly AuthRequirement[]; | |
| 592 | + }; | |
| 593 | + | |
| 594 | +export const AuthRequirementSchema = Schema.suspend(() => | |
| 595 | + Schema.Union( | |
| 596 | + Schema.Struct({ | |
| 597 | + kind: Schema.Literal("none"), | |
| 598 | + }), | |
| 599 | + Schema.Struct({ | |
| 600 | + kind: Schema.Literal("scheme"), | |
| 601 | + schemeId: SecuritySchemeSymbolIdSchema, | |
| 602 | + scopes: Schema.optional(Schema.Array(Schema.String)), | |
| 603 | + }), | |
| 604 | + Schema.Struct({ | |
| 605 | + kind: Schema.Literal("allOf"), | |
| 606 | + items: Schema.Array(AuthRequirementSchema), | |
| 607 | + }), | |
| 608 | + Schema.Struct({ | |
| 609 | + kind: Schema.Literal("anyOf"), | |
| 610 | + items: Schema.Array(AuthRequirementSchema), | |
| 611 | + }), | |
| 612 | + ) | |
| 613 | +) as Schema.Schema<AuthRequirement, AuthRequirement, never>; | |
| 614 | + | |
| 615 | +export const ServerSpecSchema = Schema.Struct({ | |
| 616 | + url: Schema.String, | |
| 617 | + description: Schema.optional(Schema.String), | |
| 618 | + variables: Schema.optional( | |
| 619 | + Schema.Record({ | |
| 620 | + key: Schema.String, | |
| 621 | + value: Schema.String, | |
| 622 | + }), | |
| 623 | + ), | |
| 624 | +}); | |
| 625 | + | |
| 626 | +export const ScopeDefaultsSchema = Schema.Struct({ | |
| 627 | + servers: Schema.optional(Schema.Array(ServerSpecSchema)), | |
| 628 | + auth: Schema.optional(AuthRequirementSchema), | |
| 629 | + parameterIds: Schema.optional(Schema.Array(ParameterSymbolIdSchema)), | |
| 630 | + headerIds: Schema.optional(Schema.Array(HeaderSymbolIdSchema)), | |
| 631 | + variables: Schema.optional( | |
| 632 | + Schema.Record({ | |
| 633 | + key: Schema.String, | |
| 634 | + value: Schema.String, | |
| 635 | + }), | |
| 636 | + ), | |
| 637 | +}); | |
| 638 | + | |
| 639 | +export const ScopeSchema = Schema.extend( | |
| 640 | + Schema.Struct({ | |
| 641 | + id: ScopeIdSchema, | |
| 642 | + kind: ScopeKindSchema, | |
| 643 | + parentId: Schema.optional(ScopeIdSchema), | |
| 644 | + name: Schema.optional(Schema.String), | |
| 645 | + namespace: Schema.optional(Schema.String), | |
| 646 | + docs: Schema.optional(DocumentationBlockSchema), | |
| 647 | + defaults: Schema.optional(ScopeDefaultsSchema), | |
| 648 | + }), | |
| 649 | + EntityBaseSchema, | |
| 650 | +); | |
| 651 | + | |
| 652 | +export const InteractionSpecSchema = Schema.Struct({ | |
| 653 | + approval: Schema.Struct({ | |
| 654 | + mayRequire: Schema.Boolean, | |
| 655 | + reasons: Schema.optional( | |
| 656 | + Schema.Array( | |
| 657 | + Schema.Literal("write", "delete", "sensitive", "externalSideEffect"), | |
| 658 | + ), | |
| 659 | + ), | |
| 660 | + }), | |
| 661 | + elicitation: Schema.Struct({ | |
| 662 | + mayRequest: Schema.Boolean, | |
| 663 | + shapeId: Schema.optional(ShapeSymbolIdSchema), | |
| 664 | + }), | |
| 665 | + resume: Schema.Struct({ | |
| 666 | + supported: Schema.Boolean, | |
| 667 | + }), | |
| 668 | +}); | |
| 669 | + | |
| 670 | +export const CapabilitySchema = Schema.extend( | |
| 671 | + Schema.Struct({ | |
| 672 | + id: CapabilityIdSchema, | |
| 673 | + serviceScopeId: ScopeIdSchema, | |
| 674 | + surface: Schema.Struct({ | |
| 675 | + toolPath: Schema.Array(Schema.String), | |
| 676 | + title: Schema.optional(Schema.String), | |
| 677 | + summary: Schema.optional(Schema.String), | |
| 678 | + description: Schema.optional(Schema.String), | |
| 679 | + aliases: Schema.optional(Schema.Array(Schema.String)), | |
| 680 | + tags: Schema.optional(Schema.Array(Schema.String)), | |
| 681 | + }), | |
| 682 | + semantics: Schema.Struct({ | |
| 683 | + effect: EffectKindSchema, | |
| 684 | + safe: Schema.optional(Schema.Boolean), | |
| 685 | + idempotent: Schema.optional(Schema.Boolean), | |
| 686 | + destructive: Schema.optional(Schema.Boolean), | |
| 687 | + }), | |
| 688 | + docs: Schema.optional(DocumentationBlockSchema), | |
| 689 | + auth: AuthRequirementSchema, | |
| 690 | + interaction: InteractionSpecSchema, | |
| 691 | + executableIds: Schema.Array(ExecutableIdSchema), | |
| 692 | + preferredExecutableId: Schema.optional(ExecutableIdSchema), | |
| 693 | + exampleIds: Schema.optional(Schema.Array(ExampleSymbolIdSchema)), | |
| 694 | + }), | |
| 695 | + EntityBaseSchema, | |
| 696 | +); | |
| 697 | + | |
| 698 | +export const HttpExecutableSchema = Schema.extend( | |
| 699 | + Schema.Struct({ | |
| 700 | + id: ExecutableIdSchema, | |
| 701 | + protocol: Schema.Literal("http"), | |
| 702 | + capabilityId: CapabilityIdSchema, | |
| 703 | + scopeId: ScopeIdSchema, | |
| 704 | + method: Schema.String, | |
| 705 | + pathTemplate: Schema.String, | |
| 706 | + pathParameterIds: Schema.optional(Schema.Array(ParameterSymbolIdSchema)), | |
| 707 | + queryParameterIds: Schema.optional(Schema.Array(ParameterSymbolIdSchema)), | |
| 708 | + headerParameterIds: Schema.optional(Schema.Array(ParameterSymbolIdSchema)), | |
| 709 | + cookieParameterIds: Schema.optional(Schema.Array(ParameterSymbolIdSchema)), | |
| 710 | + requestBodyId: Schema.optional(RequestBodySymbolIdSchema), | |
| 711 | + responseSetId: ResponseSetIdSchema, | |
| 712 | + }), | |
| 713 | + EntityBaseSchema, | |
| 714 | +); | |
| 715 | + | |
| 716 | +export const GraphQLExecutableSchema = Schema.extend( | |
| 717 | + Schema.Struct({ | |
| 718 | + id: ExecutableIdSchema, | |
| 719 | + protocol: Schema.Literal("graphql"), | |
| 720 | + capabilityId: CapabilityIdSchema, | |
| 721 | + scopeId: ScopeIdSchema, | |
| 722 | + toolKind: GraphQLToolKindSchema, | |
| 723 | + operationType: Schema.Literal("query", "mutation", "subscription"), | |
| 724 | + rootField: Schema.String, | |
| 725 | + operationName: Schema.optional(Schema.String), | |
| 726 | + operationDocument: Schema.optional(Schema.String), | |
| 727 | + argumentShapeId: ShapeSymbolIdSchema, | |
| 728 | + resultShapeId: ShapeSymbolIdSchema, | |
| 729 | + selectionShapeId: Schema.optional(ShapeSymbolIdSchema), | |
| 730 | + selectionMode: SelectionModeSchema, | |
| 731 | + responseSetId: ResponseSetIdSchema, | |
| 732 | + }), | |
| 733 | + EntityBaseSchema, | |
| 734 | +); | |
| 735 | + | |
| 736 | +export const McpExecutableSchema = Schema.extend( | |
| 737 | + Schema.Struct({ | |
| 738 | + id: ExecutableIdSchema, | |
| 739 | + protocol: Schema.Literal("mcp"), | |
| 740 | + capabilityId: CapabilityIdSchema, | |
| 741 | + scopeId: ScopeIdSchema, | |
| 742 | + serverRef: Schema.String, | |
| 743 | + toolName: Schema.String, | |
| 744 | + inputShapeId: Schema.optional(ShapeSymbolIdSchema), | |
| 745 | + outputShapeId: Schema.optional(ShapeSymbolIdSchema), | |
| 746 | + responseSetId: ResponseSetIdSchema, | |
| 747 | + }), | |
| 748 | + EntityBaseSchema, | |
| 749 | +); | |
| 750 | + | |
| 751 | +export const ExecutableSchema = Schema.Union( | |
| 752 | + HttpExecutableSchema, | |
| 753 | + GraphQLExecutableSchema, | |
| 754 | + McpExecutableSchema, | |
| 755 | +); | |
| 756 | + | |
| 757 | +export const PaginationHintSchema = Schema.Struct({ | |
| 758 | + kind: PaginationHintKindSchema, | |
| 759 | + tokenParamName: Schema.optional(Schema.String), | |
| 760 | + nextFieldPath: Schema.optional(Schema.String), | |
| 761 | +}); | |
| 762 | + | |
| 763 | +export const StatusMatchSchema = Schema.Union( | |
| 764 | + Schema.Struct({ | |
| 765 | + kind: Schema.Literal("exact"), | |
| 766 | + status: Schema.Number, | |
| 767 | + }), | |
| 768 | + Schema.Struct({ | |
| 769 | + kind: Schema.Literal("range"), | |
| 770 | + value: StatusRangeSchema, | |
| 771 | + }), | |
| 772 | + Schema.Struct({ | |
| 773 | + kind: Schema.Literal("default"), | |
| 774 | + }), | |
| 775 | +); | |
| 776 | + | |
| 777 | +export const ResponseVariantSchema = Schema.Struct({ | |
| 778 | + match: StatusMatchSchema, | |
| 779 | + responseId: ResponseSymbolIdSchema, | |
| 780 | + traits: Schema.optional(Schema.Array(ResponseTraitSchema)), | |
| 781 | + pagination: Schema.optional(PaginationHintSchema), | |
| 782 | +}); | |
| 783 | + | |
| 784 | +export const ResponseSetSchema = Schema.extend( | |
| 785 | + Schema.Struct({ | |
| 786 | + id: ResponseSetIdSchema, | |
| 787 | + variants: Schema.Array(ResponseVariantSchema), | |
| 788 | + }), | |
| 789 | + EntityBaseSchema, | |
| 790 | +); | |
| 791 | + | |
| 792 | +export const ImportDiagnosticSchema = Schema.Struct({ | |
| 793 | + id: DiagnosticIdSchema, | |
| 794 | + level: DiagnosticLevelSchema, | |
| 795 | + code: DiagnosticCodeSchema, | |
| 796 | + message: Schema.String, | |
| 797 | + relatedSymbolIds: Schema.optional(Schema.Array(SymbolIdSchema)), | |
| 798 | + provenance: Schema.Array(ProvenanceRefSchema), | |
| 799 | +}); | |
| 800 | + | |
| 801 | +export const CatalogV1Schema = Schema.Struct({ | |
| 802 | + version: Schema.Literal("ir.v1"), | |
| 803 | + documents: Schema.Record({ | |
| 804 | + key: DocumentIdSchema, | |
| 805 | + value: SourceDocumentSchema, | |
| 806 | + }), | |
| 807 | + resources: Schema.Record({ | |
| 808 | + key: ResourceIdSchema, | |
| 809 | + value: SchemaResourceSchema, | |
| 810 | + }), | |
| 811 | + scopes: Schema.Record({ | |
| 812 | + key: ScopeIdSchema, | |
| 813 | + value: ScopeSchema, | |
| 814 | + }), | |
| 815 | + symbols: Schema.Record({ | |
| 816 | + key: Schema.String, | |
| 817 | + value: SymbolSchema, | |
| 818 | + }), | |
| 819 | + capabilities: Schema.Record({ | |
| 820 | + key: CapabilityIdSchema, | |
| 821 | + value: CapabilitySchema, | |
| 822 | + }), | |
| 823 | + executables: Schema.Record({ | |
| 824 | + key: ExecutableIdSchema, | |
| 825 | + value: ExecutableSchema, | |
| 826 | + }), | |
| 827 | + responseSets: Schema.Record({ | |
| 828 | + key: ResponseSetIdSchema, | |
| 829 | + value: ResponseSetSchema, | |
| 830 | + }), | |
| 831 | + diagnostics: Schema.Record({ | |
| 832 | + key: DiagnosticIdSchema, | |
| 833 | + value: ImportDiagnosticSchema, | |
| 834 | + }), | |
| 835 | +}); | |
| 836 | + | |
| 837 | +export const CatalogFragmentV1Schema = Schema.Struct({ | |
| 838 | + version: Schema.Literal("ir.v1.fragment"), | |
| 839 | + documents: Schema.optional( | |
| 840 | + Schema.Record({ | |
| 841 | + key: DocumentIdSchema, | |
| 842 | + value: SourceDocumentSchema, | |
| 843 | + }), | |
| 844 | + ), | |
| 845 | + resources: Schema.optional( | |
| 846 | + Schema.Record({ | |
| 847 | + key: ResourceIdSchema, | |
| 848 | + value: SchemaResourceSchema, | |
| 849 | + }), | |
| 850 | + ), | |
| 851 | + scopes: Schema.optional( | |
| 852 | + Schema.Record({ | |
| 853 | + key: ScopeIdSchema, | |
| 854 | + value: ScopeSchema, | |
| 855 | + }), | |
| 856 | + ), | |
| 857 | + symbols: Schema.optional( | |
| 858 | + Schema.Record({ | |
| 859 | + key: Schema.String, | |
| 860 | + value: SymbolSchema, | |
| 861 | + }), | |
| 862 | + ), | |
| 863 | + capabilities: Schema.optional( | |
| 864 | + Schema.Record({ | |
| 865 | + key: CapabilityIdSchema, | |
| 866 | + value: CapabilitySchema, | |
| 867 | + }), | |
| 868 | + ), | |
| 869 | + executables: Schema.optional( | |
| 870 | + Schema.Record({ | |
| 871 | + key: ExecutableIdSchema, | |
| 872 | + value: ExecutableSchema, | |
| 873 | + }), | |
| 874 | + ), | |
| 875 | + responseSets: Schema.optional( | |
| 876 | + Schema.Record({ | |
| 877 | + key: ResponseSetIdSchema, | |
| 878 | + value: ResponseSetSchema, | |
| 879 | + }), | |
| 880 | + ), | |
| 881 | + diagnostics: Schema.optional( | |
| 882 | + Schema.Record({ | |
| 883 | + key: DiagnosticIdSchema, | |
| 884 | + value: ImportDiagnosticSchema, | |
| 885 | + }), | |
| 886 | + ), | |
| 887 | +}); | |
| 888 | + | |
| 889 | +export const CatalogSnapshotV1Schema = Schema.Struct({ | |
| 890 | + version: Schema.Literal("ir.v1.snapshot"), | |
| 891 | + import: ImportMetadataSchema, | |
| 892 | + catalog: CatalogV1Schema, | |
| 893 | +}); | |
| 894 | + | |
| 895 | +export type SourceKind = typeof SourceKindSchema.Type; | |
| 896 | +export type ScopeKind = typeof ScopeKindSchema.Type; | |
| 897 | +export type EffectKind = typeof EffectKindSchema.Type; | |
| 898 | +export type ParameterLocation = typeof ParameterLocationSchema.Type; | |
| 899 | +export type ResponseTrait = typeof ResponseTraitSchema.Type; | |
| 900 | +export type SecuritySchemeType = typeof SecuritySchemeTypeSchema.Type; | |
| 901 | +export type SelectionMode = typeof SelectionModeSchema.Type; | |
| 902 | +export type GraphQLToolKind = typeof GraphQLToolKindSchema.Type; | |
| 903 | +export type PaginationHintKind = typeof PaginationHintKindSchema.Type; | |
| 904 | +export type DiagnosticCode = typeof DiagnosticCodeSchema.Type; | |
| 905 | +export type DiagnosticLevel = typeof DiagnosticLevelSchema.Type; | |
| 906 | + | |
| 907 | +export type DocumentationBlock = typeof DocumentationBlockSchema.Type; | |
| 908 | +export type ProvenanceRef = typeof ProvenanceRefSchema.Type; | |
| 909 | +export type NativeBlob = typeof NativeBlobSchema.Type; | |
| 910 | +export type EntityBase = typeof EntityBaseSchema.Type; | |
| 911 | +export type ImportMetadata = typeof ImportMetadataSchema.Type; | |
| 912 | +export type SourceDocument = typeof SourceDocumentSchema.Type; | |
| 913 | +export type FieldSpec = typeof FieldSpecSchema.Type; | |
| 914 | +export type DiscriminatorSpec = typeof DiscriminatorSpecSchema.Type; | |
| 915 | +export type ScalarShape = typeof ScalarShapeSchema.Type; | |
| 916 | +export type UnknownShape = typeof UnknownShapeSchema.Type; | |
| 917 | +export type ConstShape = typeof ConstShapeSchema.Type; | |
| 918 | +export type EnumShape = typeof EnumShapeSchema.Type; | |
| 919 | +export type ObjectShape = typeof ObjectShapeSchema.Type; | |
| 920 | +export type ArrayShape = typeof ArrayShapeSchema.Type; | |
| 921 | +export type TupleShape = typeof TupleShapeSchema.Type; | |
| 922 | +export type MapShape = typeof MapShapeSchema.Type; | |
| 923 | +export type AllOfShape = typeof AllOfShapeSchema.Type; | |
| 924 | +export type AnyOfShape = typeof AnyOfShapeSchema.Type; | |
| 925 | +export type OneOfShape = typeof OneOfShapeSchema.Type; | |
| 926 | +export type NullableShape = typeof NullableShapeSchema.Type; | |
| 927 | +export type RefShape = typeof RefShapeSchema.Type; | |
| 928 | +export type NotShape = typeof NotShapeSchema.Type; | |
| 929 | +export type ConditionalShape = typeof ConditionalShapeSchema.Type; | |
| 930 | +export type GraphQLInterfaceShape = typeof GraphQLInterfaceShapeSchema.Type; | |
| 931 | +export type GraphQLUnionShape = typeof GraphQLUnionShapeSchema.Type; | |
| 932 | +export type ShapeNode = typeof ShapeNodeSchema.Type; | |
| 933 | +export type AnchorTarget = typeof AnchorTargetSchema.Type; | |
| 934 | +export type SchemaResource = typeof SchemaResourceSchema.Type; | |
| 935 | +export type RequestPlacementHint = typeof RequestPlacementHintSchema.Type; | |
| 936 | +export type OAuthFlow = typeof OAuthFlowSchema.Type; | |
| 937 | +export type SecurityPlacement = typeof SecurityPlacementSchema.Type; | |
| 938 | +export type SecuritySchemeSymbol = typeof SecuritySchemeSymbolSchema.Type; | |
| 939 | +export type EncodingSpec = typeof EncodingSpecSchema.Type; | |
| 940 | +export type ContentSpec = typeof ContentSpecSchema.Type; | |
| 941 | +export type ShapeSymbol = typeof ShapeSymbolSchema.Type; | |
| 942 | +export type ParameterSymbol = typeof ParameterSymbolSchema.Type; | |
| 943 | +export type HeaderSymbol = typeof HeaderSymbolSchema.Type; | |
| 944 | +export type RequestBodySymbol = typeof RequestBodySymbolSchema.Type; | |
| 945 | +export type ResponseSymbol = typeof ResponseSymbolSchema.Type; | |
| 946 | +export type ExampleSymbol = typeof ExampleSymbolSchema.Type; | |
| 947 | +export type Symbol = | |
| 948 | + | ShapeSymbol | |
| 949 | + | ParameterSymbol | |
| 950 | + | RequestBodySymbol | |
| 951 | + | ResponseSymbol | |
| 952 | + | HeaderSymbol | |
| 953 | + | ExampleSymbol | |
| 954 | + | SecuritySchemeSymbol; | |
| 955 | +export type ServerSpec = typeof ServerSpecSchema.Type; | |
| 956 | +export type ScopeDefaults = typeof ScopeDefaultsSchema.Type; | |
| 957 | +export type Scope = typeof ScopeSchema.Type; | |
| 958 | +export type InteractionSpec = typeof InteractionSpecSchema.Type; | |
| 959 | +export type Capability = typeof CapabilitySchema.Type; | |
| 960 | +export type HttpExecutable = typeof HttpExecutableSchema.Type; | |
| 961 | +export type GraphQLExecutable = typeof GraphQLExecutableSchema.Type; | |
| 962 | +export type McpExecutable = typeof McpExecutableSchema.Type; | |
| 963 | +export type Executable = typeof ExecutableSchema.Type; | |
| 964 | +export type PaginationHint = typeof PaginationHintSchema.Type; | |
| 965 | +export type StatusMatch = typeof StatusMatchSchema.Type; | |
| 966 | +export type ResponseVariant = typeof ResponseVariantSchema.Type; | |
| 967 | +export type ResponseSet = typeof ResponseSetSchema.Type; | |
| 968 | +export type ImportDiagnostic = typeof ImportDiagnosticSchema.Type; | |
| 969 | +export type CatalogV1 = typeof CatalogV1Schema.Type; | |
| 970 | +export type CatalogFragmentV1 = typeof CatalogFragmentV1Schema.Type; | |
| 971 | +export type CatalogSnapshotV1 = typeof CatalogSnapshotV1Schema.Type; | |
| @@ -0,0 +1,15 @@ | ||
| 1 | +{ | |
| 2 | + "compilerOptions": { | |
| 3 | + "target": "ES2022", | |
| 4 | + "module": "ESNext", | |
| 5 | + "moduleResolution": "Bundler", | |
| 6 | + "strict": true, | |
| 7 | + "skipLibCheck": true, | |
| 8 | + "noEmit": true, | |
| 9 | + "lib": ["ES2022"], | |
| 10 | + "types": ["bun-types", "node"] | |
| 11 | + }, | |
| 12 | + "include": [ | |
| 13 | + "src/**/*.ts" | |
| 14 | + ] | |
| 15 | +} | |