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 | } | ||