History

wip

Rhys Sullivan committed 8 months ago

Commit fa98f631 parent 55e58d47 files · +534 −50Browse files

Unified · Split

7 files
  1. packages/control-plane/src/ir/catalog.test.ts +38 −2
  2. packages/control-plane/src/ir/catalog.ts +326 −17
  3. packages/control-plane/src/runtime/ir-execution.ts +47 −29
  4. packages/control-plane/src/runtime/local-source-artifacts.test.ts +4 −0
  5. packages/control-plane/src/runtime/local-source-artifacts.ts +110 −1
  6. packages/control-plane/src/runtime/source-catalog-snapshot.test.ts +3 −1
  7. packages/control-plane/src/runtime/source-catalog-snapshot.ts +6 −0
packages/control-plane/src/ir/catalog.test.ts+38−2
@@ -33,6 +33,19 @@ const put = <K extends string, V>(record: Record<K, V>, key: K, value: V) => { · 32 unmodified lines
3333 record[key] = value;
3434 };
3535
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+
3649 const docId = DocumentIdSchema.make("doc_primary");
3750 const baseProvenance = (pointer: string): ProvenanceRef[] => [{
3851 relation: "declared",
@@ -630,7 +643,6 @@ describe("IR catalog", () => { · 591 unmodified lines
630643
631644 const descriptor = projected.toolDescriptors[CapabilityIdSchema.make("cap_events_update")];
632645 expect(descriptor.toolPath).toEqual(["google", "calendar", "events", "update"]);
633− expect(descriptor.resultShapeId).toBe(ShapeSymbolIdSchema.make("shape_event_result"));
634646
635647 const callShape = projected.catalog.symbols[descriptor.callShapeId];
636648 expect(callShape?.kind).toBe("shape");
@@ -646,6 +658,19 @@ describe("IR catalog", () => { · 9 unmodified lines
646658 ]);
647659 expect(callShape.node.required).toEqual(["calendarId", "eventId", "body"]);
648660
661+ const resultShape = descriptor.resultShapeId
662+ ? projected.catalog.symbols[descriptor.resultShapeId]
663+ : undefined;
664+ if (resultShape?.kind !== "shape" || resultShape.node.type !== "object") {
665+ throw new Error("Expected projected HTTP result envelope");
666+ }
667+ expect(Object.keys(resultShape.node.fields)).toEqual(["data", "error", "headers", "status"]);
668+ expectNullableShapeContains(
669+ projected.catalog,
670+ resultShape.node.fields.data.shapeId,
671+ ShapeSymbolIdSchema.make("shape_event_result"),
672+ );
673+
649674 const searchDoc = projected.searchDocs[CapabilityIdSchema.make("cap_events_update")];
650675 expect(searchDoc.authHints).toContain("oauth2");
651676 expect(searchDoc.tags).toEqual(["calendar", "events"]);
@@ -680,7 +705,18 @@ describe("IR catalog", () => { · 28 unmodified lines
680705 }
681706
682707 expect(Object.keys(callShape.node.fields)).toEqual(["id", "input", "select"]);
683− expect(descriptor.resultShapeId).toBe(ShapeSymbolIdSchema.make("shape_graphql_result"));
708+ const resultShape = descriptor.resultShapeId
709+ ? projected.catalog.symbols[descriptor.resultShapeId]
710+ : undefined;
711+ if (resultShape?.kind !== "shape" || resultShape.node.type !== "object") {
712+ throw new Error("Expected projected GraphQL result envelope");
713+ }
714+ expect(Object.keys(resultShape.node.fields)).toEqual(["data", "error", "headers", "status"]);
715+ expectNullableShapeContains(
716+ projected.catalog,
717+ resultShape.node.fields.data.shapeId,
718+ ShapeSymbolIdSchema.make("shape_graphql_result"),
719+ );
684720 });
685721
686722 it("builds inspect views and invocation plans from the projected graph", () => {
packages/control-plane/src/ir/catalog.ts+326−17
@@ -622,12 +622,17 @@ const responseContentCandidates = ( · 621 unmodified lines
622622 content.shapeId ? [{ mediaType: content.mediaType, shapeId: content.shapeId }] : [],
623623 );
624624
625−const projectResultShapeFromResponses = (
625+type RankedResponseVariantEntry = {
626+ variant: ResponseSet["variants"][number];
627+ response: ResponseSymbol;
628+ score: number;
629+};
630+
631+const responseVariantEntries = (
626632 catalog: CatalogV1,
627− capability: Capability,
628633 responseSet: ResponseSet,
629−): ShapeSymbolId | undefined => {
630− const ranked = [...responseSet.variants]
634+): RankedResponseVariantEntry[] =>
635+ [...responseSet.variants]
631636 .map((variant) => {
632637 const response = getResponseSymbol(catalog, variant.responseId);
633638 return response
@@ -638,14 +643,27 @@ const projectResultShapeFromResponses = ( · 4 unmodified lines
638643 }
639644 : null;
640645 })
641− .filter((entry): entry is { variant: ResponseSet["variants"][number]; response: ResponseSymbol; score: number } => entry !== null)
642− .sort((left, right) => right.score - left.score);
646+ .filter((entry): entry is RankedResponseVariantEntry => entry !== null);
643647
644− const preferred = ranked.some(({ score }) => score >= 60)
645− ? ranked.filter(({ score }) => score >= 60)
646− : ranked.filter(({ score }) => score === (ranked[0]?.score ?? 0));
648+const isSuccessStatusMatch = (match: StatusMatch): boolean => {
649+ switch (match.kind) {
650+ case "exact":
651+ return match.status >= 200 && match.status < 300;
652+ case "range":
653+ return match.value === "2XX";
654+ case "default":
655+ return false;
656+ }
657+};
647658
648− const jsonCandidates = preferred.flatMap(({ response }) =>
659+const contentShapeIdFromResponseEntries = (
660+ catalog: CatalogV1,
661+ capability: Capability,
662+ label: string,
663+ title: string,
664+ entries: readonly RankedResponseVariantEntry[],
665+): ShapeSymbolId | undefined => {
666+ const jsonCandidates = entries.flatMap(({ response }) =>
649667 responseContentCandidates(response).filter((candidate) => isJsonMediaType(candidate.mediaType)),
650668 );
651669
@@ -657,12 +675,12 @@ const projectResultShapeFromResponses = ( · 5 unmodified lines
657675 mediaType: candidate.mediaType,
658676 shapeId: candidate.shapeId,
659677 })),
660− `responseSet:${responseSet.id}:json`,
678+ `${label}:json`,
661679 );
662680 }
663681
664682 const fallbackShapeIds = unique(
665− preferred.flatMap(({ response }) =>
683+ entries.flatMap(({ response }) =>
666684 responseContentCandidates(response).map((candidate) => candidate.shapeId),
667685 ),
668686 );
@@ -677,8 +695,8 @@ const projectResultShapeFromResponses = ( · 8 unmodified lines
677695
678696 return createSyntheticShape(catalog, {
679697 capability,
680− label: `responseSet:${responseSet.id}:union`,
681− title: `${capability.surface.title ?? capability.id} result`,
698+ label: `${label}:union`,
699+ title,
682700 node: {
683701 type: "anyOf",
684702 items: fallbackShapeIds,
@@ -692,6 +710,297 @@ const projectResultShapeFromResponses = ( · 7 unmodified lines
692710 });
693711 };
694712
713+const projectResultShapeFromResponses = (
714+ catalog: CatalogV1,
715+ capability: Capability,
716+ responseSet: ResponseSet,
717+): ShapeSymbolId | undefined =>
718+ contentShapeIdFromResponseEntries(
719+ catalog,
720+ capability,
721+ `responseSet:${responseSet.id}`,
722+ `${capability.surface.title ?? capability.id} result`,
723+ responseVariantEntries(catalog, responseSet).filter(({ variant }) => isSuccessStatusMatch(variant.match)),
724+ );
725+
726+const projectErrorShapeFromResponses = (
727+ catalog: CatalogV1,
728+ capability: Capability,
729+ responseSet: ResponseSet,
730+): ShapeSymbolId | undefined =>
731+ contentShapeIdFromResponseEntries(
732+ catalog,
733+ capability,
734+ `responseSet:${responseSet.id}:error`,
735+ `${capability.surface.title ?? capability.id} error`,
736+ responseVariantEntries(catalog, responseSet).filter(({ variant }) => !isSuccessStatusMatch(variant.match)),
737+ );
738+
739+const scalarShape = (
740+ catalog: CatalogV1,
741+ capability: Capability,
742+ input: {
743+ label: string;
744+ title: string;
745+ scalar: "string" | "number" | "integer" | "boolean" | "null" | "bytes";
746+ },
747+): ShapeSymbolId =>
748+ createSyntheticShape(catalog, {
749+ capability,
750+ label: input.label,
751+ title: input.title,
752+ node: {
753+ type: "scalar",
754+ scalar: input.scalar,
755+ },
756+ });
757+
758+const constNullShape = (
759+ catalog: CatalogV1,
760+ capability: Capability,
761+ label: string,
762+): ShapeSymbolId =>
763+ createSyntheticShape(catalog, {
764+ capability,
765+ label,
766+ title: "null",
767+ node: {
768+ type: "const",
769+ value: null,
770+ },
771+ });
772+
773+const unknownShape = (
774+ catalog: CatalogV1,
775+ capability: Capability,
776+ input: {
777+ label: string;
778+ title: string;
779+ reason: string;
780+ },
781+): ShapeSymbolId =>
782+ createSyntheticShape(catalog, {
783+ capability,
784+ label: input.label,
785+ title: input.title,
786+ node: {
787+ type: "unknown",
788+ reason: input.reason,
789+ },
790+ });
791+
792+const nullableShape = (
793+ catalog: CatalogV1,
794+ capability: Capability,
795+ input: {
796+ label: string;
797+ title: string;
798+ baseShapeId: ShapeSymbolId;
799+ },
800+): ShapeSymbolId => {
801+ const nullShapeId = constNullShape(catalog, capability, `${input.label}:null`);
802+ if (input.baseShapeId === nullShapeId) {
803+ return nullShapeId;
804+ }
805+
806+ return createSyntheticShape(catalog, {
807+ capability,
808+ label: `${input.label}:nullable`,
809+ title: input.title,
810+ node: {
811+ type: "anyOf",
812+ items: unique([input.baseShapeId, nullShapeId]),
813+ },
814+ });
815+};
816+
817+const headersShape = (
818+ catalog: CatalogV1,
819+ capability: Capability,
820+ label: string,
821+): ShapeSymbolId => {
822+ const valueShapeId = scalarShape(catalog, capability, {
823+ label: `${label}:value`,
824+ title: "Header value",
825+ scalar: "string",
826+ });
827+
828+ return createSyntheticShape(catalog, {
829+ capability,
830+ label,
831+ title: "Response headers",
832+ node: {
833+ type: "object",
834+ fields: {},
835+ additionalProperties: valueShapeId,
836+ },
837+ });
838+};
839+
840+const shapeFieldShapeId = (
841+ catalog: CatalogV1,
842+ shapeId: ShapeSymbolId | undefined,
843+ fieldName: string,
844+ seen = new Set<ShapeSymbolId>(),
845+): ShapeSymbolId | undefined => {
846+ if (!shapeId || seen.has(shapeId)) {
847+ return undefined;
848+ }
849+
850+ const shape = getShape(catalog, shapeId);
851+ if (!shape) {
852+ return undefined;
853+ }
854+
855+ const nextSeen = new Set(seen);
856+ nextSeen.add(shapeId);
857+
858+ switch (shape.node.type) {
859+ case "ref":
860+ return shapeFieldShapeId(catalog, shape.node.target, fieldName, nextSeen);
861+ case "object":
862+ return shape.node.fields[fieldName]?.shapeId;
863+ case "allOf":
864+ for (const item of shape.node.items) {
865+ const found = shapeFieldShapeId(catalog, item, fieldName, nextSeen);
866+ if (found) {
867+ return found;
868+ }
869+ }
870+ return undefined;
871+ default:
872+ return undefined;
873+ }
874+};
875+
876+const projectExecutionResultShape = (
877+ catalog: CatalogV1,
878+ capability: Capability,
879+ executable: Executable,
880+ responseSet: ResponseSet,
881+): ShapeSymbolId => {
882+ const genericDataShape = unknownShape(catalog, capability, {
883+ label: `executionResult:${capability.id}:data:unknown`,
884+ title: "Response data",
885+ reason: `Execution result data for ${capability.id} is not statically known`,
886+ });
887+ const genericErrorShape = unknownShape(catalog, capability, {
888+ label: `executionResult:${capability.id}:error:unknown`,
889+ title: "Response error",
890+ reason: `Execution result error for ${capability.id} is not statically known`,
891+ });
892+ const genericHeadersShape = headersShape(
893+ catalog,
894+ capability,
895+ `executionResult:${capability.id}:headers`,
896+ );
897+ const genericStatusShape = scalarShape(catalog, capability, {
898+ label: `executionResult:${capability.id}:status`,
899+ title: "Response status",
900+ scalar: "integer",
901+ });
902+
903+ let dataShapeId: ShapeSymbolId | undefined;
904+ let errorShapeId: ShapeSymbolId | undefined;
905+ let headersShapeId: ShapeSymbolId = genericHeadersShape;
906+ let statusShapeId: ShapeSymbolId = genericStatusShape;
907+
908+ switch (executable.protocol) {
909+ case "http":
910+ dataShapeId = projectResultShapeFromResponses(catalog, capability, responseSet);
911+ errorShapeId = projectErrorShapeFromResponses(catalog, capability, responseSet);
912+ break;
913+ case "graphql":
914+ dataShapeId =
915+ shapeFieldShapeId(catalog, executable.resultShapeId, "data")
916+ ?? shapeFieldShapeId(catalog, executable.resultShapeId, "body")
917+ ?? executable.resultShapeId;
918+ errorShapeId =
919+ shapeFieldShapeId(catalog, executable.resultShapeId, "error")
920+ ?? shapeFieldShapeId(catalog, executable.resultShapeId, "errors");
921+ headersShapeId =
922+ shapeFieldShapeId(catalog, executable.resultShapeId, "headers")
923+ ?? genericHeadersShape;
924+ statusShapeId =
925+ shapeFieldShapeId(catalog, executable.resultShapeId, "status")
926+ ?? genericStatusShape;
927+ break;
928+ case "mcp":
929+ dataShapeId = executable.outputShapeId;
930+ errorShapeId = undefined;
931+ statusShapeId = constNullShape(
932+ catalog,
933+ capability,
934+ `executionResult:${capability.id}:status:null`,
935+ );
936+ break;
937+ }
938+
939+ const dataFieldShapeId = nullableShape(catalog, capability, {
940+ label: `executionResult:${capability.id}:data`,
941+ title: "Result data",
942+ baseShapeId: dataShapeId ?? genericDataShape,
943+ });
944+ const errorFieldShapeId = nullableShape(catalog, capability, {
945+ label: `executionResult:${capability.id}:error`,
946+ title: "Result error",
947+ baseShapeId: errorShapeId ?? genericErrorShape,
948+ });
949+ const statusFieldShapeId = nullableShape(catalog, capability, {
950+ label: `executionResult:${capability.id}:status`,
951+ title: "Result status",
952+ baseShapeId: statusShapeId,
953+ });
954+
955+ return createSyntheticShape(catalog, {
956+ capability,
957+ label: `executionResult:${capability.id}`,
958+ title: `${capability.surface.title ?? capability.id} result`,
959+ node: {
960+ type: "object",
961+ fields: {
962+ data: {
963+ shapeId: dataFieldShapeId,
964+ docs: {
965+ description: "Successful result payload when available.",
966+ },
967+ },
968+ error: {
969+ shapeId: errorFieldShapeId,
970+ docs: {
971+ description: "Error payload when the remote execution completed but failed.",
972+ },
973+ },
974+ headers: {
975+ shapeId: headersShapeId,
976+ docs: {
977+ description: "Response headers when available.",
978+ },
979+ },
980+ status: {
981+ shapeId: statusFieldShapeId,
982+ docs: {
983+ description: "Transport status code when available.",
984+ },
985+ },
986+ },
987+ required: ["data", "error", "headers", "status"],
988+ additionalProperties: false,
989+ },
990+ diagnostic: {
991+ level: "info",
992+ code: "projection_result_shape_synthesized",
993+ message: `Synthesized execution result envelope for ${capability.id}`,
994+ relatedSymbolIds: unique([
995+ dataFieldShapeId,
996+ errorFieldShapeId,
997+ headersShapeId,
998+ statusFieldShapeId,
999+ ]),
1000+ },
1001+ });
1002+};
1003+
6951004 const groupFieldShape = (
6961005 catalog: CatalogV1,
6971006 capability: Capability,
@@ -1073,15 +1382,15 @@ const projectCapability = ( · 375 unmodified lines
10731382 switch (executable.protocol) {
10741383 case "http":
10751384 callShapeId = projectHttpCallShape(catalog, capability, executable);
1076− resultShapeId = projectResultShapeFromResponses(catalog, capability, responseSet);
1385+ resultShapeId = projectExecutionResultShape(catalog, capability, executable, responseSet);
10771386 break;
10781387 case "graphql":
10791388 callShapeId = projectGraphqlCallShape(catalog, capability, executable);
1080− resultShapeId = executable.resultShapeId;
1389+ resultShapeId = projectExecutionResultShape(catalog, capability, executable, responseSet);
10811390 break;
10821391 case "mcp":
10831392 callShapeId = projectMcpCallShape(catalog, capability, executable);
1084− resultShapeId = executable.outputShapeId;
1393+ resultShapeId = projectExecutionResultShape(catalog, capability, executable, responseSet);
10851394 break;
10861395 }
10871396
packages/control-plane/src/runtime/ir-execution.ts+47−29
@@ -65,15 +65,11 @@ const readSourceTransport = (source: Source): "auto" | "streamable-http" | "sse" · 64 unmodified lines
6565 return undefined;
6666 };
6767
68−const summarizeHttpBody = (body: unknown): string => {
69− if (typeof body === "string") {
70− return body.slice(0, 400);
71− }
72− try {
73− return JSON.stringify(body).slice(0, 400);
74− } catch {
75− return String(body);
76− }
68+type ExecutionEnvelope = {
69+ data: unknown;
70+ error: unknown;
71+ headers: Record<string, string>;
72+ status: number | null;
7773 };
7874
7975 const decodeFetchBody = async (response: Response): Promise<unknown> => {
@@ -87,6 +83,16 @@ const decodeFetchBody = async (response: Response): Promise<unknown> => { · 7 unmodified lines
8783 return response.text();
8884 };
8985
86+const responseHeadersRecord = (response: Response): Record<string, string> => {
87+ const headers: Record<string, string> = {};
88+ response.headers.forEach((value, key) => {
89+ headers[key] = value;
90+ });
91+ return headers;
92+};
93+
94+const executionEnvelope = (input: ExecutionEnvelope): ExecutionEnvelope => input;
95+
9096 const parameterById = (catalog: CatalogV1, parameterId: string): ParameterSymbol | undefined => {
9197 const symbol = catalog.symbols[parameterId];
9298 return symbol?.kind === "parameter" ? symbol : undefined;
@@ -340,11 +346,12 @@ const executeHttp = (input: { · 247 unmodified lines
340346 ...(body !== undefined ? { body } : {}),
341347 });
342348 const responseBody = await decodeFetchBody(response);
343− if (!response.ok) {
344− throw new Error(`HTTP ${response.status}: ${summarizeHttpBody(responseBody)}`);
345− }
346−
347− return responseBody;
349+ return executionEnvelope({
350+ data: response.ok ? responseBody : null,
351+ error: response.ok ? null : responseBody,
352+ headers: responseHeadersRecord(response),
353+ status: response.status,
354+ });
348355 },
349356 catch: (cause) => cause instanceof Error ? cause : new Error(String(cause)),
350357 });
@@ -394,7 +401,9 @@ const executeGraphql = (input: { · 43 unmodified lines
394401 Effect.tryPromise({
395402 try: async () => {
396403 const argsRecord = asObject(input.args);
397− const native = input.executable.native?.[0]?.value as Record<string, unknown> | undefined;
404+ const native = input.executable.native
405+ ?.find((blob) => blob.kind === "graphql_provider_data")
406+ ?.value as Record<string, unknown> | undefined;
398407 const requestHeaders = {
399408 ...readSourceHeaders(input.source),
400409 ...input.auth.headers,
@@ -430,14 +439,19 @@ const executeGraphql = (input: { · 29 unmodified lines
430439 }),
431440 });
432441 const body = await decodeFetchBody(response);
433− if (!response.ok) {
434− throw new Error(`GraphQL HTTP ${response.status}: ${summarizeHttpBody(body)}`);
435− }
436− return body;
442+ const bodyRecord = asObject(body);
443+ const errors = Array.isArray(bodyRecord.errors) ? bodyRecord.errors : [];
444+ return executionEnvelope({
445+ data: body,
446+ error: errors.length > 0 ? errors : (response.status >= 400 ? body : null),
447+ headers: responseHeadersRecord(response),
448+ status: response.status,
449+ });
437450 }
438451
439452 const operationDocument = asString(native.operationDocument)!;
440453 const operationName = asString(native.operationName) ?? undefined;
454+ const fieldName = asString(native.fieldName) ?? input.executable.rootField;
441455 const variables = graphqlArgsPayload({
442456 catalog: input.catalog,
443457 executable: input.executable,
@@ -468,18 +482,14 @@ const executeGraphql = (input: { · 24 unmodified lines
468482 }),
469483 });
470484 const body = asObject(await decodeFetchBody(response));
471− if (!response.ok) {
472− throw new Error(`GraphQL HTTP ${response.status}: ${summarizeHttpBody(body)}`);
473− }
474−
475485 const errors = Array.isArray(body.errors) ? body.errors : [];
476486 const data = asObject(body.data);
477− const fieldName = asString(native.fieldName) ?? input.executable.rootField;
478− return {
487+ return executionEnvelope({
479488 data: data[fieldName] ?? null,
480− errors,
481− isError: errors.length > 0,
482− };
489+ error: errors.length > 0 ? errors : (response.status >= 400 ? body : null),
490+ headers: responseHeadersRecord(response),
491+ status: response.status,
492+ });
483493 },
484494 catch: (cause) => cause instanceof Error ? cause : new Error(String(cause)),
485495 });
@@ -572,7 +582,15 @@ const executeMcp = (input: { · 86 unmodified lines
572582 }
573583 : undefined;
574584
575− return await definition.execute(asObject(payload), executionContext);
585+ const result = await definition.execute(asObject(payload), executionContext);
586+ const resultRecord = asObject(result);
587+ const isError = resultRecord.isError === true;
588+ return executionEnvelope({
589+ data: isError ? null : (result ?? null),
590+ error: isError ? result : null,
591+ headers: {},
592+ status: null,
593+ });
576594 },
577595 catch: (cause) => cause instanceof Error ? cause : new Error(String(cause)),
578596 });
packages/control-plane/src/runtime/local-source-artifacts.test.ts+4−0
@@ -109,8 +109,11 @@ describe("local-source-artifacts", () => { · 108 unmodified lines
109109 const persistedArtifact = JSON.parse(readFileSync(path, "utf8"));
110110 const persistedDocument = Object.values(persistedArtifact.snapshot.catalog.documents)[0] as {
111111 native?: ReadonlyArray<{ kind?: string }>;
112+ provenance?: unknown;
112113 };
113114 expect(persistedDocument.native?.some((blob) => blob.kind === "source_document") ?? false).toBe(false);
115+ expect(persistedDocument.provenance).toBeUndefined();
116+ expect(Object.keys(persistedArtifact.snapshot.catalog.diagnostics)).toHaveLength(0);
114117
115118 const decoded = yield* readLocalSourceArtifact({
116119 context,
@@ -120,6 +123,7 @@ describe("local-source-artifacts", () => { · 3 unmodified lines
120123 expect(decoded?.snapshot.import.adapterKey).toBe("openapi");
121124 expect(decoded?.sourceId).toBe("src_test");
122125 expect((Object.values(decoded?.snapshot.catalog.documents ?? {})[0] as { native?: Array<{ value?: unknown }> })?.native?.[0]?.value).toBe('{"openapi":"3.1.0"}');
126+ expect((Object.values(decoded?.snapshot.catalog.documents ?? {})[0] as { provenance?: unknown }).provenance).toBeUndefined();
123127 }).pipe(Effect.provide(NodeFileSystem.layer)),
124128 );
125129
packages/control-plane/src/runtime/local-source-artifacts.ts+110−1
@@ -16,6 +16,7 @@ import * as Schema from "effect/Schema"; · 15 unmodified lines
1616 import {
1717 CatalogSnapshotV1Schema,
1818 type NativeBlob,
19+ type ShapeNode,
1920 type SourceDocument,
2021 } from "../ir/model";
2122 import type { SourceCatalogSyncResult } from "./source-catalog-support";
@@ -57,6 +58,10 @@ const mapFileSystemError = (path: string, action: string) => (cause: unknown) => · 35 unmodified lines
5758 const mutableRecord = <K extends string, V>(value: Readonly<Record<K, V>>): Record<K, V> =>
5859 value as Record<K, V>;
5960
61+const clearProvenance = (value: unknown): void => {
62+ (value as { provenance: Array<unknown> }).provenance = [];
63+};
64+
6065 const localSourceArtifactPath = (input: {
6166 context: ResolvedLocalWorkspaceContext;
6267 sourceId: string;
@@ -154,6 +159,109 @@ const hydrateArtifactSourceDocuments = (input: { · 91 unmodified lines
154159 return nextArtifact;
155160 };
156161
162+const stripShapeFieldDocs = (node: ShapeNode): void => {
163+ switch (node.type) {
164+ case "object":
165+ case "graphqlInterface":
166+ for (const field of Object.values(node.fields)) {
167+ delete (field as { docs?: unknown }).docs;
168+ }
169+ return;
170+ default:
171+ return;
172+ }
173+};
174+
175+const stripRuntimeMetadata = (artifact: LocalSourceArtifact): LocalSourceArtifact => {
176+ const nextArtifact = structuredClone(artifact);
177+
178+ for (const document of Object.values(nextArtifact.snapshot.catalog.documents)) {
179+ delete (document as { provenance?: unknown }).provenance;
180+ delete (document as { docs?: unknown }).docs;
181+ delete (document as { native?: unknown }).native;
182+ }
183+
184+ for (const resource of Object.values(nextArtifact.snapshot.catalog.resources)) {
185+ clearProvenance(resource as unknown);
186+ delete (resource as { docs?: unknown }).docs;
187+ delete (resource as { native?: unknown }).native;
188+ }
189+
190+ for (const scope of Object.values(nextArtifact.snapshot.catalog.scopes)) {
191+ clearProvenance(scope as unknown);
192+ delete (scope as { docs?: unknown }).docs;
193+ delete (scope as { native?: unknown }).native;
194+ }
195+
196+ for (const capability of Object.values(nextArtifact.snapshot.catalog.capabilities)) {
197+ clearProvenance(capability as unknown);
198+ delete (capability as { native?: unknown }).native;
199+ delete (capability as { exampleIds?: unknown }).exampleIds;
200+ }
201+
202+ for (const executable of Object.values(nextArtifact.snapshot.catalog.executables)) {
203+ clearProvenance(executable as unknown);
204+ if (Array.isArray((executable as { native?: NativeBlob[] }).native)) {
205+ const keptNative = ((executable as { native?: NativeBlob[] }).native ?? []).filter((blob) =>
206+ blob.kind === "google_discovery_provider_data"
207+ );
208+ if (keptNative.length > 0) {
209+ (executable as { native?: NativeBlob[] }).native = keptNative;
210+ } else {
211+ delete (executable as { native?: unknown }).native;
212+ }
213+ }
214+ }
215+
216+ for (const responseSet of Object.values(nextArtifact.snapshot.catalog.responseSets)) {
217+ clearProvenance(responseSet as unknown);
218+ delete (responseSet as { native?: unknown }).native;
219+ }
220+
221+ for (const diagnostic of Object.values(nextArtifact.snapshot.catalog.diagnostics)) {
222+ delete (diagnostic as { provenance?: unknown }).provenance;
223+ }
224+
225+ const mutableSymbols = mutableRecord(nextArtifact.snapshot.catalog.symbols);
226+ for (const [symbolId, symbol] of Object.entries(mutableSymbols)) {
227+ if (symbol.kind === "example") {
228+ delete mutableSymbols[symbolId as keyof typeof mutableSymbols];
229+ continue;
230+ }
231+
232+ clearProvenance(symbol as unknown);
233+ delete (symbol as { docs?: unknown }).docs;
234+ delete (symbol as { diagnosticIds?: unknown }).diagnosticIds;
235+ delete (symbol as { native?: unknown }).native;
236+ delete (symbol as { deprecated?: unknown }).deprecated;
237+
238+ switch (symbol.kind) {
239+ case "shape":
240+ stripShapeFieldDocs(symbol.node);
241+ break;
242+ case "parameter":
243+ case "header":
244+ case "response":
245+ delete (symbol as { exampleIds?: unknown }).exampleIds;
246+ if ("content" in symbol && Array.isArray(symbol.content)) {
247+ for (const content of symbol.content) {
248+ delete (content as { exampleIds?: unknown }).exampleIds;
249+ }
250+ }
251+ break;
252+ case "requestBody":
253+ for (const content of symbol.contents) {
254+ delete (content as { exampleIds?: unknown }).exampleIds;
255+ }
256+ break;
257+ }
258+ }
259+
260+ mutableRecord(nextArtifact.snapshot.catalog).diagnostics = {};
261+
262+ return nextArtifact;
263+};
264+
157265 const snapshotHash = (snapshot: SourceCatalogSyncResult["snapshot"]): string =>
158266 contentHash(JSON.stringify(snapshot));
159267
@@ -281,6 +389,7 @@ export const writeLocalSourceArtifact = (input: { · 121 unmodified lines
281389 const legacyPath = legacyLocalSourceArtifactPath(input);
282390 const sourceDocumentDirectory = localSourceDocumentDirectory(input);
283391 const split = splitArtifactSourceDocuments(input.artifact);
392+ const runtimeArtifact = stripRuntimeMetadata(split.artifact);
284393 yield* fs.makeDirectory(directory, { recursive: true }).pipe(
285394 Effect.mapError(mapFileSystemError(directory, "create source artifact directory")),
286395 );
@@ -302,7 +411,7 @@ export const writeLocalSourceArtifact = (input: { · 15 unmodified lines
302411 );
303412 }
304413 }
305− yield* fs.writeFileString(path, `${JSON.stringify(split.artifact)}\n`).pipe(
414+ yield* fs.writeFileString(path, `${JSON.stringify(runtimeArtifact)}\n`).pipe(
306415 Effect.mapError(mapFileSystemError(path, "write source artifact")),
307416 );
308417 const legacyExists = yield* fs.exists(legacyPath).pipe(
packages/control-plane/src/runtime/source-catalog-snapshot.test.ts+3−1
@@ -708,7 +708,9 @@ describe("source-catalog-snapshot", () => { · 707 unmodified lines
708708 expect(executable.operationType).toBe("query");
709709 expect(executable.rootField).toBe("viewer");
710710 expect(executable.selectionMode).toBe("fixed");
711− expect(executable.native).toBeUndefined();
711+ expect(executable.native).toMatchObject([{
712+ kind: "graphql_provider_data",
713+ }]);
712714 });
713715
714716 it("projects MCP metadata into capability semantics", () => {
packages/control-plane/src/runtime/source-catalog-snapshot.ts+6−0
@@ -2009,6 +2009,12 @@ const createGraphqlCapability = (input: { · 2008 unmodified lines
20092009 responseSetId,
20102010 synthetic: false,
20112011 provenance: provenanceFor(input.documentId, `#/graphql/${input.operation.providerData.toolId}/executable`),
2012+ native: [nativeBlob({
2013+ source: input.source,
2014+ kind: "graphql_provider_data",
2015+ pointer: `#/graphql/${input.operation.providerData.toolId}/providerData`,
2016+ value: input.operation.providerData,
2017+ })],
20122018 } satisfies GraphQLExecutable;
20132019
20142020 const effect = input.operation.effect;

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