History

move ir into kernel

Rhys Sullivan committed 8 months ago

Commit e2dcd001 parent b2b2e8023 files · +94 −32Browse files

Showing the changes to packages/kernel (8 of 23 files). Show all

Unified · Split

8 files
  1. packages/kernel/ir/README.md +34 −0
  2. packages/kernel/ir/package.json +27 −0
  3. packages/kernel/ir/src/catalog.test.ts +748 −0
  4. packages/kernel/ir/src/catalog.ts +2018 −0
  5. packages/kernel/ir/src/ids.ts +58 −0
  6. packages/kernel/ir/src/index.ts +3 −0
  7. packages/kernel/ir/src/model.ts +971 −0
  8. packages/kernel/ir/tsconfig.json +15 −0
packages/kernel/ir/README.md+34−0
@@ -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
packages/kernel/ir/package.json+27−0
@@ -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+}
packages/kernel/ir/src/catalog.test.ts+748−0
@@ -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+});
packages/kernel/ir/src/catalog.ts+2018−0
@@ -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+};
packages/kernel/ir/src/ids.ts+58−0
@@ -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+);
packages/kernel/ir/src/index.ts+3−0
@@ -0,0 +1,3 @@
1+export * from "./ids";
2+export * from "./model";
3+export * from "./catalog";
packages/kernel/ir/src/model.ts+971−0
@@ -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;
packages/kernel/ir/tsconfig.json+15−0
@@ -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+}

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t