History

Improve source type projection and inspection performance

Rhys Sullivan committed 7 months ago

Commit 747f7a31 parent 20557f610 files · +1075 −144Browse files

Unified · Split

10 files
  1. .executor/executor.jsonc +11 −1
  2. apps/executor/src/server/server.real-ingestion.test.ts +32 −0
  3. packages/platform/control-plane/src/runtime/catalog/catalog-typescript.ts +233 −126
  4. packages/platform/control-plane/src/runtime/catalog/source/runtime.test.ts +526 −0
  5. packages/platform/control-plane/src/runtime/catalog/source/runtime.ts +1 −1
  6. packages/platform/control-plane/src/runtime/catalog/source/snapshot.test.ts +156 −0
  7. packages/platform/control-plane/src/runtime/catalog/source/type-declarations.test.ts +19 −5
  8. packages/platform/control-plane/src/runtime/sources/source-inspection.ts +4 −3
  9. packages/sources/core/src/catalog-json-schema.ts +78 −2
  10. packages/sources/graphql/src/graphql-tools.ts +15 −6
.executor/executor.jsonc+11−1
@@ -1,15 +1,25 @@
11 {
22 "sources": {
33 "vercel": {
4− "kind": "openapi",
54 "name": "Vercel API",
65 "connection": {
76 "endpoint": "https://api.vercel.com/"
87 },
8+ "kind": "openapi",
99 "binding": {
1010 "specUrl": "https://openapi.vercel.sh/",
1111 "defaultHeaders": null
1212 }
13+ },
14+ "linear": {
15+ "kind": "graphql",
16+ "name": "Linear GraphQL",
17+ "connection": {
18+ "endpoint": "https://api.linear.app/graphql"
19+ },
20+ "binding": {
21+ "defaultHeaders": null
22+ }
1323 }
1424 }
1525 }
apps/executor/src/server/server.real-ingestion.test.ts+32−0
@@ -157,6 +157,38 @@ describe("local-executor-server real ingestion", () => { · 156 unmodified lines
157157 expect(connected.source.status).toBe("connected");
158158 expect(connected.source.namespace).toBe("linear");
159159
160+ const inspection = yield* client.sources.inspection({
161+ path: {
162+ workspaceId: installation.workspaceId,
163+ sourceId: connected.source.id,
164+ },
165+ });
166+
167+ expect(inspection.source.kind).toBe("graphql");
168+ expect(inspection.toolCount).toBeGreaterThan(100);
169+ expect(
170+ inspection.tools.every((tool) => tool.inputTypePreview === undefined),
171+ ).toBe(true);
172+ expect(
173+ inspection.tools.every((tool) => tool.outputTypePreview === undefined),
174+ ).toBe(true);
175+
176+ const toolDetail = yield* client.sources.inspectionTool({
177+ path: {
178+ workspaceId: installation.workspaceId,
179+ sourceId: connected.source.id,
180+ toolPath: "linear.administrableTeams",
181+ },
182+ });
183+
184+ expect(toolDetail.summary.path).toBe("linear.administrableTeams");
185+ expect(toolDetail.summary.inputTypePreview).toBeUndefined();
186+ expect(toolDetail.summary.outputTypePreview).toBeUndefined();
187+ expect(toolDetail.contract.input.typeDeclaration).toContain(
188+ "type LinearAdministrableTeamsCall = {",
189+ );
190+ expect(toolDetail.contract.input.schemaJson).toContain("\"$defs\"");
191+
160192 const storedSource = yield* client.sources.get({
161193 path: {
162194 workspaceId: installation.workspaceId,
packages/platform/control-plane/src/runtime/catalog/catalog-typescript.ts+233−126
@@ -5,6 +5,7 @@ import type { ShapeSymbolId } from "@executor/ir/ids"; · 4 unmodified lines
55 import type {
66 CatalogV1,
77 DocumentationBlock,
8+ ProvenanceRef,
89 ShapeNode,
910 ShapeSymbol,
1011 } from "@executor/ir/model";
@@ -52,7 +53,6 @@ export type CatalogTypeProjector = { · 41 unmodified lines
5253 };
5354
5455 const VALID_IDENTIFIER_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
55−const TYPE_ALIAS_REFERENCE_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
5656
5757 const hashSignature = (value: string): string =>
5858 createHash("sha256").update(value).digest("hex").slice(0, 24);
@@ -160,6 +160,89 @@ const isSyntheticShapeLabel = (value: string): boolean => · 101 unmodified lines
160160
161161 const looksLikeHumanPhrase = (value: string): boolean => /\s/.test(value.trim());
162162
163+const nominalTypeName = (shape: ShapeSymbol): string | undefined => {
164+ const title = shape.title?.trim();
165+ return title && !isSyntheticShapeLabel(title)
166+ ? formatTypeNameSegment(title)
167+ : undefined;
168+};
169+
170+const decodePointerSegment = (value: string): string =>
171+ value.replace(/~1/g, "/").replace(/~0/g, "~");
172+
173+const meaningfulProvenanceName = (value: string): string | undefined => {
174+ const stripped = value.trim();
175+ if (
176+ stripped.length === 0
177+ || /^\d+$/.test(stripped)
178+ || [
179+ "$defs",
180+ "definitions",
181+ "components",
182+ "schemas",
183+ "schema",
184+ "properties",
185+ "items",
186+ "additionalProperties",
187+ "patternProperties",
188+ "allOf",
189+ "anyOf",
190+ "oneOf",
191+ "not",
192+ "if",
193+ "then",
194+ "else",
195+ "input",
196+ "output",
197+ "graphql",
198+ "scalars",
199+ "responses",
200+ "headers",
201+ "parameters",
202+ "requestBody",
203+ ].includes(stripped)
204+ ) {
205+ return undefined;
206+ }
207+
208+ return formatTypeNameSegment(stripped);
209+};
210+
211+const typeNameFromProvenance = (provenance: readonly ProvenanceRef[]): string | undefined => {
212+ for (const entry of provenance) {
213+ const pointer = entry.pointer;
214+ if (!pointer?.startsWith("#/")) {
215+ continue;
216+ }
217+
218+ const segments = pointer
219+ .slice(2)
220+ .split("/")
221+ .map(decodePointerSegment);
222+ const markerIndexes = segments.flatMap((segment, index) =>
223+ ["$defs", "definitions", "schemas", "input", "output"].includes(segment)
224+ ? [index]
225+ : []
226+ );
227+
228+ for (const markerIndex of markerIndexes) {
229+ const candidate = meaningfulProvenanceName(segments[markerIndex + 1] ?? "");
230+ if (candidate) {
231+ return candidate;
232+ }
233+ }
234+
235+ for (let index = segments.length - 1; index >= 0; index -= 1) {
236+ const candidate = meaningfulProvenanceName(segments[index] ?? "");
237+ if (candidate) {
238+ return candidate;
239+ }
240+ }
241+ }
242+
243+ return undefined;
244+};
245+
163246 const cleanDocText = (value: string | undefined): string | null => {
164247 const trimmed = value?.trim();
165248 return trimmed && trimmed.length > 0 ? trimmed : null;
@@ -475,14 +558,17 @@ export const createCatalogTypeProjector = (input: { · 309 unmodified lines
475558 }): CatalogTypeProjector => {
476559 const { catalog, roots } = input;
477560 const signatureCache = new Map<ShapeSymbolId, SignatureInfo>();
478− const recursiveSignatures = new Set<string>();
479− const shapeIdsBySignature = new Map<string, ShapeSymbolId[]>();
480− const usageCountBySignature = new Map<string, number>();
481− const aliasNameBySignature = new Map<string, string>();
482− const canonicalTypeBySignature = new Map<string, string>();
483− const rootAliasHintBySignature = new Map<string, string>();
561+ const rootShapeIds = new Set(roots.map((root) => root.shapeId));
562+ const reachableShapeIds = new Set<ShapeSymbolId>();
563+ const recursiveShapeIds = new Set<ShapeSymbolId>();
564+ const explicitRefTargetShapeIds = new Set<ShapeSymbolId>();
565+ const aliasNameByShapeId = new Map<ShapeSymbolId, string>();
566+ const rootAliasHintByShapeId = new Map<ShapeSymbolId, string>();
567+ const declarationBodyByShapeId = new Map<ShapeSymbolId, string>();
568+ const nominalTypeNameByShapeId = new Map<ShapeSymbolId, string>();
569+ const nominalTypeNameCounts = new Map<string, number>();
484570 const usedAliasNames = new Set<string>();
485− const usedAliasSignatures = new Set<string>();
571+ const usedAliasShapeIds = new Set<ShapeSymbolId>();
486572
487573 const shapeSignatureInfo = (shapeId: ShapeSymbolId, stack: readonly ShapeSymbolId[] = []): SignatureInfo => {
488574 const cached = signatureCache.get(shapeId);
@@ -590,25 +676,13 @@ export const createCatalogTypeProjector = (input: { · 101 unmodified lines
590676 recursive,
591677 };
592678
593− if (signature.recursive) {
594− recursiveSignatures.add(signature.key);
595− }
596−
597679 signatureCache.set(shapeId, signature);
598− const group = shapeIdsBySignature.get(signature.key);
599− if (group) {
600− group.push(shapeId);
601− } else {
602− shapeIdsBySignature.set(signature.key, [shapeId]);
603− }
604680 return signature;
605681 };
606682
607683 const shapeSignature = (shapeId: ShapeSymbolId, stack: readonly ShapeSymbolId[] = []): string =>
608684 shapeSignatureInfo(shapeId, stack).key;
609685
610− const reachableShapeIds = new Set<ShapeSymbolId>();
611−
612686 const collectReachableShapes = (shapeId: ShapeSymbolId): void => {
613687 if (reachableShapeIds.has(shapeId)) {
614688 return;
@@ -624,33 +698,81 @@ export const createCatalogTypeProjector = (input: { · 9 unmodified lines
624698 collectReachableShapes(root.shapeId);
625699 }
626700
627− for (const rootShapeId of roots.map((root) => root.shapeId)) {
628− const rootShape = getShapeSymbol(catalog, rootShapeId);
629− if (isInlineShapeNode(rootShape.node)) {
701+ for (const shapeId of reachableShapeIds) {
702+ const shape = getShapeSymbol(catalog, shapeId);
703+ if (shape.synthetic) {
630704 continue;
631705 }
632706
633− const signature = shapeSignature(rootShapeId);
634− usageCountBySignature.set(signature, (usageCountBySignature.get(signature) ?? 0) + 1);
707+ const name = nominalTypeName(shape);
708+ if (!name) {
709+ continue;
710+ }
711+
712+ nominalTypeNameByShapeId.set(shapeId, name);
713+ nominalTypeNameCounts.set(name, (nominalTypeNameCounts.get(name) ?? 0) + 1);
635714 }
636715
637− for (const shapeId of reachableShapeIds) {
716+ const uniqueNominalTypeNameForShapeId = (
717+ shapeId: ShapeSymbolId,
718+ ): string | undefined => {
719+ const name = nominalTypeNameByShapeId.get(shapeId);
720+ return name && nominalTypeNameCounts.get(name) === 1
721+ ? name
722+ : undefined;
723+ };
724+
725+ const visitingShapeIds: Array<ShapeSymbolId> = [];
726+ const processedShapeIds = new Set<ShapeSymbolId>();
727+
728+ const markRecursiveShapes = (cycleStartShapeId: ShapeSymbolId): void => {
729+ const cycleStartIndex = visitingShapeIds.indexOf(cycleStartShapeId);
730+ if (cycleStartIndex === -1) {
731+ recursiveShapeIds.add(cycleStartShapeId);
732+ return;
733+ }
734+
735+ for (const recursiveShapeId of visitingShapeIds.slice(cycleStartIndex)) {
736+ recursiveShapeIds.add(recursiveShapeId);
737+ }
738+ recursiveShapeIds.add(cycleStartShapeId);
739+ };
740+
741+ const detectRecursiveShapes = (shapeId: ShapeSymbolId): void => {
742+ if (processedShapeIds.has(shapeId)) {
743+ return;
744+ }
745+
746+ if (visitingShapeIds.includes(shapeId)) {
747+ markRecursiveShapes(shapeId);
748+ return;
749+ }
750+
751+ visitingShapeIds.push(shapeId);
638752 for (const childShapeId of childShapeIds(getShapeSymbol(catalog, shapeId).node)) {
639− const childShape = getShapeSymbol(catalog, childShapeId);
640− if (isInlineShapeNode(childShape.node)) {
753+ if (visitingShapeIds.includes(childShapeId)) {
754+ markRecursiveShapes(childShapeId);
641755 continue;
642756 }
643757
644− const signature = shapeSignature(childShapeId);
645− usageCountBySignature.set(signature, (usageCountBySignature.get(signature) ?? 0) + 1);
758+ detectRecursiveShapes(childShapeId);
646759 }
647− }
760+ visitingShapeIds.pop();
761+ processedShapeIds.add(shapeId);
762+ };
648763
649764 for (const root of roots) {
650− const signature = shapeSignature(root.shapeId);
651− const existing = rootAliasHintBySignature.get(signature);
765+ detectRecursiveShapes(root.shapeId);
766+ const existing = rootAliasHintByShapeId.get(root.shapeId);
652767 if (!existing || root.aliasHint.length < existing.length) {
653− rootAliasHintBySignature.set(signature, root.aliasHint);
768+ rootAliasHintByShapeId.set(root.shapeId, root.aliasHint);
769+ }
770+ }
771+
772+ for (const shapeId of reachableShapeIds) {
773+ const shape = getShapeSymbol(catalog, shapeId);
774+ if (shape.node.type === "ref") {
775+ explicitRefTargetShapeIds.add(shape.node.target);
654776 }
655777 }
656778
@@ -883,60 +1005,56 @@ export const createCatalogTypeProjector = (input: { · 226 unmodified lines
8831005 return baseText ? `${wrapCompositeType(baseText)} & (${unionText})` : unionText;
8841006 };
8851007
886− const representativeShapeIdForSignature = (signature: string): ShapeSymbolId => {
887− const group = shapeIdsBySignature.get(signature);
888− if (!group || group.length === 0) {
889− throw new Error(`Missing representative shape for signature ${signature}`);
890− }
891−
892− const sorted = [...group].sort((left, right) => {
893− const leftShape = getShapeSymbol(catalog, left);
894− const rightShape = getShapeSymbol(catalog, right);
895− const leftTitle = leftShape.title ?? leftShape.id;
896− const rightTitle = rightShape.title ?? rightShape.id;
897− const leftRefPenalty = leftShape.node.type === "ref" ? 1 : 0;
898− const rightRefPenalty = rightShape.node.type === "ref" ? 1 : 0;
899− const leftSynthetic = isSyntheticShapeLabel(leftTitle) ? 1 : 0;
900− const rightSynthetic = isSyntheticShapeLabel(rightTitle) ? 1 : 0;
901− return leftRefPenalty - rightRefPenalty
902− || leftSynthetic - rightSynthetic
903− || leftTitle.localeCompare(rightTitle);
904− });
905−
906− return sorted[0]!;
907− };
908−
909− const aliasNameForSignature = (signature: string, hint?: string): string => {
910− const existing = aliasNameBySignature.get(signature);
1008+ const aliasNameForShapeId = (shapeId: ShapeSymbolId, hint?: string): string => {
1009+ const existing = aliasNameByShapeId.get(shapeId);
9111010 if (existing) {
9121011 return existing;
9131012 }
9141013
915− const representative = getShapeSymbol(catalog, representativeShapeIdForSignature(signature));
916− const representativeName = representative.title ?? representative.id;
917− const rootHint = rootAliasHintBySignature.get(signature);
918− const contextualHint = hint ? compactAliasHint(joinTypeNameSegments(hint)) : undefined;
919− const representativeTypeName = !isSyntheticShapeLabel(representativeName) && !looksLikeHumanPhrase(representativeName)
920− ? formatTypeNameSegment(representativeName)
921− : undefined;
922− const baseName = rootHint
923− ?? representativeTypeName
924− ?? contextualHint
925− ?? formatTypeNameSegment(representative.id);
926− let candidate = baseName;
1014+ const shape = getShapeSymbol(catalog, shapeId);
1015+ const rootHint = rootAliasHintByShapeId.get(shapeId);
1016+ const contextualHint = hint ? compactAliasHint(hint) : undefined;
1017+ const uniqueNominalName = uniqueNominalTypeNameForShapeId(shapeId);
1018+ const nameCandidates = [
1019+ rootHint,
1020+ uniqueNominalName,
1021+ typeNameFromProvenance(shape.provenance),
1022+ nominalTypeNameByShapeId.get(shapeId),
1023+ contextualHint,
1024+ formatTypeNameSegment(shape.id),
1025+ ].filter((candidate): candidate is string => candidate !== undefined);
1026+ const [baseName = formatTypeNameSegment(shape.id)] = nameCandidates;
1027+ let candidate =
1028+ nameCandidates.find((name) => !usedAliasNames.has(name))
1029+ ?? baseName;
9271030 let suffix = 2;
9281031 while (usedAliasNames.has(candidate)) {
9291032 candidate = `${baseName}_${String(suffix)}`;
9301033 suffix += 1;
9311034 }
9321035
933− aliasNameBySignature.set(signature, candidate);
1036+ aliasNameByShapeId.set(shapeId, candidate);
9341037 usedAliasNames.add(candidate);
9351038 return candidate;
9361039 };
9371040
938− const shouldAliasSignature = (signature: string): boolean =>
939− recursiveSignatures.has(signature) || (usageCountBySignature.get(signature) ?? 0) > 1;
1041+ const shouldEmitDeclarationShape = (shapeId: ShapeSymbolId): boolean => {
1042+ const shape = getShapeSymbol(catalog, shapeId);
1043+ if (isInlineShapeNode(shape.node)) {
1044+ return false;
1045+ }
1046+
1047+ if (rootShapeIds.has(shapeId)) {
1048+ return true;
1049+ }
1050+
1051+ if (shape.node.type === "ref") {
1052+ return false;
1053+ }
1054+
1055+ return uniqueNominalTypeNameForShapeId(shapeId) !== undefined
1056+ || explicitRefTargetShapeIds.has(shapeId);
1057+ };
9401058
9411059 const renderDeclarationShapeBody = (
9421060 shapeId: ShapeSymbolId,
@@ -965,21 +1083,15 @@ export const createCatalogTypeProjector = (input: { · 22 unmodified lines
9651083 }
9661084 };
9671085
968− const canonicalTypeForSignature = (signature: string, hint?: string): string => {
969− const existing = canonicalTypeBySignature.get(signature);
1086+ const renderDeclarationBodyForShapeId = (shapeId: ShapeSymbolId): string => {
1087+ const existing = declarationBodyByShapeId.get(shapeId);
9701088 if (existing) {
9711089 return existing;
9721090 }
9731091
974− const aliasName = aliasNameForSignature(signature, hint);
975− canonicalTypeBySignature.set(signature, aliasName);
976− const representativeShapeId = representativeShapeIdForSignature(signature);
977− const body = renderDeclarationShapeBody(representativeShapeId, [representativeShapeId], aliasName);
978− const canonical = TYPE_ALIAS_REFERENCE_PATTERN.test(body) && body !== aliasName
979− ? body
980− : aliasName;
981− canonicalTypeBySignature.set(signature, canonical);
982− return canonical;
1092+ const body = renderDeclarationShapeBody(shapeId, [shapeId], aliasNameForShapeId(shapeId));
1093+ declarationBodyByShapeId.set(shapeId, body);
1094+ return body;
9831095 };
9841096
9851097 const renderDeclarationShape: RenderShape = (shapeId, options = {}) => {
@@ -988,16 +1100,21 @@ export const createCatalogTypeProjector = (input: { · 2 unmodified lines
9881100 return renderInlineShapeNode(shape.node);
9891101 }
9901102
991− const signature = shapeSignature(shapeId);
992− if (shouldAliasSignature(signature)) {
993− usedAliasSignatures.add(signature);
994− return canonicalTypeForSignature(signature, options.aliasHint);
1103+ const stack = options.stack ?? [];
1104+ if (shape.node.type === "ref" && !rootShapeIds.has(shapeId)) {
1105+ if (stack.includes(shapeId)) {
1106+ return renderDeclarationShape(shape.node.target, {
1107+ stack,
1108+ aliasHint: options.aliasHint,
1109+ });
1110+ }
1111+
1112+ return renderDeclarationShapeBody(shapeId, [...stack, shapeId], options.aliasHint);
9951113 }
9961114
997− const stack = options.stack ?? [];
998− if (stack.includes(shapeId)) {
999− usedAliasSignatures.add(signature);
1000− return canonicalTypeForSignature(signature, options.aliasHint);
1115+ if (stack.includes(shapeId) || shouldEmitDeclarationShape(shapeId)) {
1116+ usedAliasShapeIds.add(shapeId);
1117+ return aliasNameForShapeId(shapeId, options.aliasHint);
10011118 }
10021119
10031120 return renderDeclarationShapeBody(shapeId, [...stack, shapeId], options.aliasHint);
@@ -1063,53 +1180,43 @@ export const createCatalogTypeProjector = (input: { · 59 unmodified lines
10631180 };
10641181
10651182 const supportingDeclarations = (): readonly string[] => {
1066− const pending = [...usedAliasSignatures];
1067− const emitted = new Set<string>();
1183+ const pending = [...usedAliasShapeIds];
1184+ const emitted = new Set<ShapeSymbolId>();
1185+ let pendingIndex = 0;
10681186
1069− while (pending.length > 0) {
1070− const signature = pending.pop()!;
1071− if (emitted.has(signature)) {
1187+ while (pendingIndex < pending.length) {
1188+ const shapeId = pending[pendingIndex++]!;
1189+ if (emitted.has(shapeId)) {
10721190 continue;
10731191 }
10741192
1075− emitted.add(signature);
1076− const representativeShapeId = representativeShapeIdForSignature(signature);
1077− renderDeclarationShapeBody(representativeShapeId, [representativeShapeId], aliasNameForSignature(signature));
1193+ emitted.add(shapeId);
1194+ renderDeclarationBodyForShapeId(shapeId);
10781195
1079− for (const discoveredSignature of usedAliasSignatures) {
1080− if (!emitted.has(discoveredSignature)) {
1081− pending.push(discoveredSignature);
1196+ for (const discoveredShapeId of usedAliasShapeIds) {
1197+ if (!emitted.has(discoveredShapeId)) {
1198+ pending.push(discoveredShapeId);
10821199 }
10831200 }
10841201 }
10851202
1086− return [...emitted]
1087− .sort((left, right) => aliasNameForSignature(left).localeCompare(aliasNameForSignature(right)))
1088− .map((signature) => {
1089− const canonicalType = canonicalTypeForSignature(signature);
1090− const representativeShapeId = representativeShapeIdForSignature(signature);
1091− const aliasName = aliasNameForSignature(signature);
1092− const representativeShape = getShapeSymbol(catalog, representativeShapeId);
1203+ const declarations = [...emitted]
1204+ .sort((left, right) => aliasNameForShapeId(left).localeCompare(aliasNameForShapeId(right)))
1205+ .map((shapeId) => {
1206+ const aliasName = aliasNameForShapeId(shapeId);
1207+ const shape = getShapeSymbol(catalog, shapeId);
10931208 const comment = documentationComment({
1094− title: representativeShape.title,
1095− docs: representativeShape.docs,
1096− deprecated: representativeShape.deprecated,
1209+ title: shape.title,
1210+ docs: shape.docs,
1211+ deprecated: shape.deprecated,
10971212 includeTitle: true,
10981213 });
1099− if (canonicalType !== aliasName) {
1100− return comment
1101− ? `${comment}\ntype ${aliasName} = ${canonicalType};`
1102− : `type ${aliasName} = ${canonicalType};`;
1103− }
1104− const body = renderDeclarationShapeBody(representativeShapeId, [representativeShapeId], aliasName);
1105− if (body === aliasName) {
1106− return "";
1107− }
1108−
1214+ const body = renderDeclarationBodyForShapeId(shapeId);
11091215 const declaration = `type ${aliasName} = ${body};`;
11101216 return comment ? `${comment}\n${declaration}` : declaration;
1111− })
1112− .filter((declaration) => declaration.length > 0);
1217+ });
1218+
1219+ return declarations;
11131220 };
11141221
11151222 return {
packages/platform/control-plane/src/runtime/catalog/source/runtime.test.ts+526−0
@@ -345,6 +345,532 @@ describe("source-catalog-runtime", () => { · 344 unmodified lines
345345 expect(joinTypeNameSegments("linear", "teamsSearch", "call")).toBe("LinearTeamsSearchCall");
346346 });
347347
348+ it("keeps nominally distinct shapes separate in declaration exports", () => {
349+ const loadedCatalog = createLoadedCatalog({
350+ mutateCatalog: (catalog) => {
351+ const stringShapeId = ShapeSymbolIdSchema.make("shape_string");
352+ const projectFilterShapeId = ShapeSymbolIdSchema.make("shape_project_filter");
353+ const callShapeId = ShapeSymbolIdSchema.make("shape_team_query_args");
354+ const stringShape = catalog.symbols[stringShapeId];
355+ const callShape = catalog.symbols[callShapeId];
356+ const resourceId = stringShape?.kind === "shape" ? stringShape.resourceId : undefined;
357+
358+ if (!callShape || callShape.kind !== "shape" || callShape.node.type !== "object") {
359+ throw new Error("Expected object call shape in fixture");
360+ }
361+
362+ put(catalog.symbols as Record<typeof projectFilterShapeId, CatalogV1["symbols"][typeof projectFilterShapeId]>, projectFilterShapeId, {
363+ id: projectFilterShapeId,
364+ kind: "shape",
365+ ...(resourceId ? { resourceId } : {}),
366+ title: "ProjectFilter",
367+ node: {
368+ type: "object",
369+ fields: {
370+ name: {
371+ shapeId: stringShapeId,
372+ },
373+ },
374+ additionalProperties: false,
375+ },
376+ synthetic: false,
377+ provenance: baseProvenance("#/input/ProjectFilter"),
378+ });
379+
380+ put(catalog.symbols as Record<typeof callShapeId, CatalogV1["symbols"][typeof callShapeId]>, callShapeId, {
381+ ...callShape,
382+ node: {
383+ ...callShape.node,
384+ fields: {
385+ ...callShape.node.fields,
386+ projectFilter: {
387+ shapeId: projectFilterShapeId,
388+ docs: {
389+ description: "Filter projects by name.",
390+ },
391+ },
392+ },
393+ },
394+ });
395+ },
396+ });
397+
398+ const descriptor = Object.values(loadedCatalog.projected.toolDescriptors).find((tool) =>
399+ tool.toolPath.join(".") === "linear.teams"
400+ );
401+ if (!descriptor) {
402+ throw new Error("Expected linear.teams descriptor in fixture");
403+ }
404+
405+ const projector = createCatalogTypeProjector({
406+ catalog: loadedCatalog.projected.catalog,
407+ roots: [{
408+ shapeId: descriptor.callShapeId,
409+ aliasHint: joinTypeNameSegments(...descriptor.toolPath, "call"),
410+ }],
411+ });
412+
413+ const rootType = projector.renderDeclarationShape(descriptor.callShapeId, {
414+ aliasHint: joinTypeNameSegments(...descriptor.toolPath, "call"),
415+ });
416+ const declarations = projector.supportingDeclarations();
417+ const declarationText = declarations.join("\n\n");
418+
419+ expect(rootType).toBe("LinearTeamsCall");
420+ expect(declarations).toHaveLength(3);
421+ expect(declarationText).toContain("type LinearTeamsCall = {");
422+ expect(declarationText).toContain("filter?: TeamFilter;");
423+ expect(declarationText).toContain("projectFilter?: ProjectFilter;");
424+ expect(declarationText).toContain("type TeamFilter = {");
425+ expect(declarationText).toContain("type ProjectFilter = {");
426+ expect(declarationText).not.toContain("type LinearTeamsCallProjectFilter");
427+ });
428+
429+ it("uses provenance names for GraphQL depth variants instead of numeric suffixes", () => {
430+ const loadedCatalog = createLoadedCatalog({
431+ mutateCatalog: (catalog) => {
432+ const stringShapeId = ShapeSymbolIdSchema.make("shape_string");
433+ const callShapeId = ShapeSymbolIdSchema.make("shape_team_query_args");
434+ const teamShapeId = ShapeSymbolIdSchema.make("shape_team_output");
435+ const teamDepthShapeId = ShapeSymbolIdSchema.make(
436+ "shape_team_output_depth_1",
437+ );
438+ const teamRefShapeId = ShapeSymbolIdSchema.make("shape_team_output_ref");
439+ const teamDepthRefShapeId = ShapeSymbolIdSchema.make(
440+ "shape_team_output_depth_1_ref",
441+ );
442+ const stringShape = catalog.symbols[stringShapeId];
443+ const callShape = catalog.symbols[callShapeId];
444+ const resourceId =
445+ stringShape?.kind === "shape" ? stringShape.resourceId : undefined;
446+
447+ if (!callShape || callShape.kind !== "shape" || callShape.node.type !== "object") {
448+ throw new Error("Expected object call shape in fixture");
449+ }
450+
451+ put(catalog.symbols as Record<typeof teamShapeId, CatalogV1["symbols"][typeof teamShapeId]>, teamShapeId, {
452+ id: teamShapeId,
453+ kind: "shape",
454+ ...(resourceId ? { resourceId } : {}),
455+ node: {
456+ type: "object",
457+ fields: {
458+ __typename: {
459+ shapeId: stringShapeId,
460+ },
461+ },
462+ required: ["__typename"],
463+ additionalProperties: false,
464+ },
465+ synthetic: false,
466+ provenance: baseProvenance("#/$defs/graphql/output/Team"),
467+ });
468+
469+ put(catalog.symbols as Record<typeof teamDepthShapeId, CatalogV1["symbols"][typeof teamDepthShapeId]>, teamDepthShapeId, {
470+ id: teamDepthShapeId,
471+ kind: "shape",
472+ ...(resourceId ? { resourceId } : {}),
473+ node: {
474+ type: "object",
475+ fields: {
476+ __typename: {
477+ shapeId: stringShapeId,
478+ },
479+ name: {
480+ shapeId: stringShapeId,
481+ },
482+ },
483+ required: ["__typename", "name"],
484+ additionalProperties: false,
485+ },
486+ synthetic: false,
487+ provenance: baseProvenance("#/$defs/graphql/output/Team__depth1"),
488+ });
489+
490+ put(catalog.symbols as Record<typeof teamRefShapeId, CatalogV1["symbols"][typeof teamRefShapeId]>, teamRefShapeId, {
491+ id: teamRefShapeId,
492+ kind: "shape",
493+ ...(resourceId ? { resourceId } : {}),
494+ node: {
495+ type: "ref",
496+ target: teamShapeId,
497+ },
498+ synthetic: false,
499+ provenance: baseProvenance("#/query/teams/args/properties/team"),
500+ });
501+
502+ put(catalog.symbols as Record<typeof teamDepthRefShapeId, CatalogV1["symbols"][typeof teamDepthRefShapeId]>, teamDepthRefShapeId, {
503+ id: teamDepthRefShapeId,
504+ kind: "shape",
505+ ...(resourceId ? { resourceId } : {}),
506+ node: {
507+ type: "ref",
508+ target: teamDepthShapeId,
509+ },
510+ synthetic: false,
511+ provenance: baseProvenance("#/query/teams/args/properties/teamDepth"),
512+ });
513+
514+ put(catalog.symbols as Record<typeof callShapeId, CatalogV1["symbols"][typeof callShapeId]>, callShapeId, {
515+ ...callShape,
516+ node: {
517+ ...callShape.node,
518+ fields: {
519+ ...callShape.node.fields,
520+ team: {
521+ shapeId: teamRefShapeId,
522+ },
523+ teamDepth: {
524+ shapeId: teamDepthRefShapeId,
525+ },
526+ },
527+ },
528+ });
529+ },
530+ });
531+
532+ const descriptor = Object.values(loadedCatalog.projected.toolDescriptors).find((tool) =>
533+ tool.toolPath.join(".") === "linear.teams"
534+ );
535+ if (!descriptor) {
536+ throw new Error("Expected linear.teams descriptor in fixture");
537+ }
538+
539+ const projector = createCatalogTypeProjector({
540+ catalog: loadedCatalog.projected.catalog,
541+ roots: [{
542+ shapeId: descriptor.callShapeId,
543+ aliasHint: joinTypeNameSegments(...descriptor.toolPath, "call"),
544+ }],
545+ });
546+
547+ projector.renderDeclarationShape(descriptor.callShapeId, {
548+ aliasHint: joinTypeNameSegments(...descriptor.toolPath, "call"),
549+ });
550+ const declarationText = projector.supportingDeclarations().join("\n\n");
551+
552+ expect(declarationText).toContain("team?: Team;");
553+ expect(declarationText).toContain("teamDepth?: TeamDepth1;");
554+ expect(declarationText).toContain("type Team = {");
555+ expect(declarationText).toContain("type TeamDepth1 = {");
556+ expect(declarationText).not.toContain("type Team_2 = {");
557+ });
558+
559+ it("keeps recursive JSON-like wrappers inline without numeric helper aliases", () => {
560+ const catalog = createEmptyCatalogV1();
561+ const resourceId = ResourceIdSchema.make("res_json");
562+ const stringShapeId = ShapeSymbolIdSchema.make("shape_json_string");
563+ const numberShapeId = ShapeSymbolIdSchema.make("shape_json_number");
564+ const booleanShapeId = ShapeSymbolIdSchema.make("shape_json_boolean");
565+ const arrayShapeId = ShapeSymbolIdSchema.make("shape_json_array");
566+ const objectShapeId = ShapeSymbolIdSchema.make("shape_json_object");
567+ const rootShapeId = ShapeSymbolIdSchema.make("shape_json_value");
568+
569+ put(catalog.documents as Record<typeof docId, CatalogV1["documents"][typeof docId]>, docId, {
570+ id: docId,
571+ kind: "graphql-schema",
572+ title: "Json Test",
573+ fetchedAt: "2026-03-20T00:00:00.000Z",
574+ rawRef: "memory://json-test",
575+ });
576+ put(catalog.resources as Record<typeof resourceId, CatalogV1["resources"][typeof resourceId]>, resourceId, {
577+ id: resourceId,
578+ documentId: docId,
579+ canonicalUri: "memory://json-test",
580+ baseUri: "memory://json-test",
581+ anchors: {},
582+ dynamicAnchors: {},
583+ synthetic: false,
584+ provenance: baseProvenance("#"),
585+ });
586+
587+ put(catalog.symbols as Record<typeof stringShapeId, CatalogV1["symbols"][typeof stringShapeId]>, stringShapeId, {
588+ id: stringShapeId,
589+ kind: "shape",
590+ resourceId,
591+ node: { type: "scalar", scalar: "string" },
592+ synthetic: false,
593+ provenance: baseProvenance("#/string"),
594+ });
595+ put(catalog.symbols as Record<typeof numberShapeId, CatalogV1["symbols"][typeof numberShapeId]>, numberShapeId, {
596+ id: numberShapeId,
597+ kind: "shape",
598+ resourceId,
599+ node: { type: "scalar", scalar: "number" },
600+ synthetic: false,
601+ provenance: baseProvenance("#/number"),
602+ });
603+ put(catalog.symbols as Record<typeof booleanShapeId, CatalogV1["symbols"][typeof booleanShapeId]>, booleanShapeId, {
604+ id: booleanShapeId,
605+ kind: "shape",
606+ resourceId,
607+ node: { type: "scalar", scalar: "boolean" },
608+ synthetic: false,
609+ provenance: baseProvenance("#/boolean"),
610+ });
611+ put(catalog.symbols as Record<typeof arrayShapeId, CatalogV1["symbols"][typeof arrayShapeId]>, arrayShapeId, {
612+ id: arrayShapeId,
613+ kind: "shape",
614+ resourceId,
615+ node: {
616+ type: "array",
617+ itemShapeId: rootShapeId,
618+ },
619+ synthetic: false,
620+ provenance: baseProvenance("#/array"),
621+ });
622+ put(catalog.symbols as Record<typeof objectShapeId, CatalogV1["symbols"][typeof objectShapeId]>, objectShapeId, {
623+ id: objectShapeId,
624+ kind: "shape",
625+ resourceId,
626+ node: {
627+ type: "object",
628+ fields: {},
629+ required: [],
630+ additionalProperties: rootShapeId,
631+ },
632+ synthetic: false,
633+ provenance: baseProvenance("#/object"),
634+ });
635+ put(catalog.symbols as Record<typeof rootShapeId, CatalogV1["symbols"][typeof rootShapeId]>, rootShapeId, {
636+ id: rootShapeId,
637+ kind: "shape",
638+ resourceId,
639+ title: "JsonValue",
640+ node: {
641+ type: "oneOf",
642+ items: [
643+ stringShapeId,
644+ numberShapeId,
645+ booleanShapeId,
646+ arrayShapeId,
647+ objectShapeId,
648+ ],
649+ },
650+ synthetic: false,
651+ provenance: baseProvenance("#/jsonValue"),
652+ });
653+
654+ const projector = createCatalogTypeProjector({
655+ catalog,
656+ roots: [{
657+ shapeId: rootShapeId,
658+ aliasHint: "JsonValue",
659+ }],
660+ });
661+
662+ const rootType = projector.renderDeclarationShape(rootShapeId, {
663+ aliasHint: "JsonValue",
664+ });
665+ const declarationText = projector.supportingDeclarations().join("\n\n");
666+
667+ expect(rootType).toBe("JsonValue");
668+ expect(declarationText).toContain("type JsonValue =");
669+ expect(declarationText).toContain("Array<JsonValue>");
670+ expect(declarationText).toContain("[key: string]: JsonValue;");
671+ expect(declarationText).not.toContain("type JsonValue_2");
672+ expect(declarationText).not.toContain("type JsonValue_3");
673+ });
674+
675+ it("uses provenance names before numeric suffixes for duplicate titled ref targets", () => {
676+ const loadedCatalog = createLoadedCatalog({
677+ mutateCatalog: (catalog) => {
678+ const stringShapeId = ShapeSymbolIdSchema.make("shape_string");
679+ const callShapeId = ShapeSymbolIdSchema.make("shape_team_query_args");
680+ const accessCentrifyShapeId = ShapeSymbolIdSchema.make("shape_access_centrify");
681+ const accessSchemasCentrifyShapeId = ShapeSymbolIdSchema.make("shape_access_schemas_centrify");
682+ const accessCentrifyRefShapeId = ShapeSymbolIdSchema.make("shape_access_centrify_ref");
683+ const accessSchemasCentrifyRefShapeId = ShapeSymbolIdSchema.make("shape_access_schemas_centrify_ref");
684+ const stringShape = catalog.symbols[stringShapeId];
685+ const callShape = catalog.symbols[callShapeId];
686+ const resourceId =
687+ stringShape?.kind === "shape" ? stringShape.resourceId : undefined;
688+
689+ if (!callShape || callShape.kind !== "shape" || callShape.node.type !== "object") {
690+ throw new Error("Expected object call shape in fixture");
691+ }
692+
693+ for (const [shapeId, pointer] of [
694+ [accessCentrifyShapeId, "#/components/schemas/access_centrify"],
695+ [accessSchemasCentrifyShapeId, "#/components/schemas/access_schemas-centrify"],
696+ ] as const) {
697+ put(catalog.symbols as Record<typeof shapeId, CatalogV1["symbols"][typeof shapeId]>, shapeId, {
698+ id: shapeId,
699+ kind: "shape",
700+ ...(resourceId ? { resourceId } : {}),
701+ title: "Centrify",
702+ node: {
703+ type: "object",
704+ fields: {
705+ name: {
706+ shapeId: stringShapeId,
707+ },
708+ },
709+ required: ["name"],
710+ additionalProperties: false,
711+ },
712+ synthetic: false,
713+ provenance: baseProvenance(pointer),
714+ });
715+ }
716+
717+ put(catalog.symbols as Record<typeof accessCentrifyRefShapeId, CatalogV1["symbols"][typeof accessCentrifyRefShapeId]>, accessCentrifyRefShapeId, {
718+ id: accessCentrifyRefShapeId,
719+ kind: "shape",
720+ ...(resourceId ? { resourceId } : {}),
721+ node: {
722+ type: "ref",
723+ target: accessCentrifyShapeId,
724+ },
725+ synthetic: false,
726+ provenance: baseProvenance("#/query/teams/args/properties/primaryIdp"),
727+ });
728+ put(catalog.symbols as Record<typeof accessSchemasCentrifyRefShapeId, CatalogV1["symbols"][typeof accessSchemasCentrifyRefShapeId]>, accessSchemasCentrifyRefShapeId, {
729+ id: accessSchemasCentrifyRefShapeId,
730+ kind: "shape",
731+ ...(resourceId ? { resourceId } : {}),
732+ node: {
733+ type: "ref",
734+ target: accessSchemasCentrifyShapeId,
735+ },
736+ synthetic: false,
737+ provenance: baseProvenance("#/query/teams/args/properties/secondaryIdp"),
738+ });
739+
740+ put(catalog.symbols as Record<typeof callShapeId, CatalogV1["symbols"][typeof callShapeId]>, callShapeId, {
741+ ...callShape,
742+ node: {
743+ ...callShape.node,
744+ fields: {
745+ ...callShape.node.fields,
746+ primaryIdp: {
747+ shapeId: accessCentrifyRefShapeId,
748+ },
749+ secondaryIdp: {
750+ shapeId: accessSchemasCentrifyRefShapeId,
751+ },
752+ },
753+ },
754+ });
755+ },
756+ });
757+
758+ const descriptor = Object.values(loadedCatalog.projected.toolDescriptors).find((tool) =>
759+ tool.toolPath.join(".") === "linear.teams"
760+ );
761+ if (!descriptor) {
762+ throw new Error("Expected linear.teams descriptor in fixture");
763+ }
764+
765+ const projector = createCatalogTypeProjector({
766+ catalog: loadedCatalog.projected.catalog,
767+ roots: [{
768+ shapeId: descriptor.callShapeId,
769+ aliasHint: joinTypeNameSegments(...descriptor.toolPath, "call"),
770+ }],
771+ });
772+
773+ projector.renderDeclarationShape(descriptor.callShapeId, {
774+ aliasHint: joinTypeNameSegments(...descriptor.toolPath, "call"),
775+ });
776+ const declarationText = projector.supportingDeclarations().join("\n\n");
777+
778+ expect(declarationText).toContain("primaryIdp?: AccessCentrify;");
779+ expect(declarationText).toContain("secondaryIdp?: AccessSchemasCentrify;");
780+ expect(declarationText).toContain("type AccessCentrify = {");
781+ expect(declarationText).toContain("type AccessSchemasCentrify = {");
782+ expect(declarationText).not.toContain("type Centrify_2 = {");
783+ });
784+
785+ it("renders shared GraphQL typename-only refs through normal aliasing", () => {
786+ const loadedCatalog = createLoadedCatalog({
787+ mutateCatalog: (catalog) => {
788+ const stringShapeId = ShapeSymbolIdSchema.make("shape_string");
789+ const callShapeId = ShapeSymbolIdSchema.make("shape_team_query_args");
790+ const teamDepthShapeId = ShapeSymbolIdSchema.make("shape_team_output_depth_2");
791+ const teamDepthRefShapeId = ShapeSymbolIdSchema.make(
792+ "shape_team_output_depth_2_ref",
793+ );
794+ const stringShape = catalog.symbols[stringShapeId];
795+ const callShape = catalog.symbols[callShapeId];
796+ const resourceId =
797+ stringShape?.kind === "shape" ? stringShape.resourceId : undefined;
798+
799+ if (!callShape || callShape.kind !== "shape" || callShape.node.type !== "object") {
800+ throw new Error("Expected object call shape in fixture");
801+ }
802+
803+ put(catalog.symbols as Record<typeof teamDepthShapeId, CatalogV1["symbols"][typeof teamDepthShapeId]>, teamDepthShapeId, {
804+ id: teamDepthShapeId,
805+ kind: "shape",
806+ ...(resourceId ? { resourceId } : {}),
807+ node: {
808+ type: "object",
809+ fields: {
810+ __typename: {
811+ shapeId: stringShapeId,
812+ },
813+ },
814+ required: ["__typename"],
815+ additionalProperties: false,
816+ },
817+ synthetic: false,
818+ provenance: baseProvenance("#/$defs/graphql/output/GraphqlTypenameOnly"),
819+ });
820+
821+ put(catalog.symbols as Record<typeof teamDepthRefShapeId, CatalogV1["symbols"][typeof teamDepthRefShapeId]>, teamDepthRefShapeId, {
822+ id: teamDepthRefShapeId,
823+ kind: "shape",
824+ ...(resourceId ? { resourceId } : {}),
825+ node: {
826+ type: "ref",
827+ target: teamDepthShapeId,
828+ },
829+ synthetic: false,
830+ provenance: baseProvenance("#/query/teams/args/properties/teamDepthTwo"),
831+ });
832+
833+ put(catalog.symbols as Record<typeof callShapeId, CatalogV1["symbols"][typeof callShapeId]>, callShapeId, {
834+ ...callShape,
835+ node: {
836+ ...callShape.node,
837+ fields: {
838+ ...callShape.node.fields,
839+ teamDepthTwo: {
840+ shapeId: teamDepthRefShapeId,
841+ },
842+ },
843+ },
844+ });
845+ },
846+ });
847+
848+ const descriptor = Object.values(loadedCatalog.projected.toolDescriptors).find((tool) =>
849+ tool.toolPath.join(".") === "linear.teams"
850+ );
851+ if (!descriptor) {
852+ throw new Error("Expected linear.teams descriptor in fixture");
853+ }
854+
855+ const projector = createCatalogTypeProjector({
856+ catalog: loadedCatalog.projected.catalog,
857+ roots: [{
858+ shapeId: descriptor.callShapeId,
859+ aliasHint: joinTypeNameSegments(...descriptor.toolPath, "call"),
860+ }],
861+ });
862+
863+ projector.renderDeclarationShape(descriptor.callShapeId, {
864+ aliasHint: joinTypeNameSegments(...descriptor.toolPath, "call"),
865+ });
866+ const declarationText = projector.supportingDeclarations().join("\n\n");
867+
868+ expect(declarationText).toContain("type GraphqlTypenameOnly = {");
869+ expect(declarationText).toContain("__typename: string;");
870+ expect(declarationText).toContain("teamDepthTwo?: GraphqlTypenameOnly;");
871+ expect(declarationText).not.toContain("type TeamDepth2 = {");
872+ });
873+
348874 it.effect("projects friendly schemas for discover and inspection consumers", () =>
349875 Effect.gen(function* () {
350876 const [tool] = yield* expandCatalogTools({
packages/platform/control-plane/src/runtime/catalog/source/runtime.ts+1−1
@@ -920,7 +920,7 @@ const declarationBlockForShape = (input: { · 919 unmodified lines
920920 }),
921921 ].join("\n\n");
922922
923− return formatWithPrettier(declarationText, "typescript-module");
923+ return declarationText;
924924 },
925925 catch: (cause) =>
926926 cause instanceof Error ? cause : new Error(String(cause)),
packages/platform/control-plane/src/runtime/catalog/source/snapshot.test.ts+156−0
@@ -1198,4 +1198,160 @@ describe("source-catalog-snapshot", () => { · 1197 unmodified lines
11981198 ),
11991199 ).toBe(false);
12001200 });
1201+
1202+ it("collapses GraphQL typename-only output stubs to a shared ref", () => {
1203+ const snapshot = createGraphqlCatalogSnapshot({
1204+ source: {
1205+ ...baseSource,
1206+ kind: "graphql",
1207+ namespace: "linear",
1208+ },
1209+ documents: [],
1210+ operations: [
1211+ {
1212+ toolId: "teamDepthRefs",
1213+ title: "Team Depth Refs",
1214+ description: "Return depth-limited Team variants.",
1215+ effect: "read",
1216+ inputSchema: {
1217+ type: "object",
1218+ additionalProperties: false,
1219+ },
1220+ outputSchema: {
1221+ type: "object",
1222+ properties: {
1223+ data: {
1224+ $ref: "#/$defs/graphql/output/Team",
1225+ },
1226+ errors: {
1227+ type: "array",
1228+ items: {
1229+ type: "object",
1230+ properties: {
1231+ message: {
1232+ type: "string",
1233+ },
1234+ },
1235+ required: ["message"],
1236+ additionalProperties: false,
1237+ },
1238+ },
1239+ },
1240+ required: ["data", "errors"],
1241+ additionalProperties: false,
1242+ $defs: {
1243+ graphql: {
1244+ output: {
1245+ Team: {
1246+ type: "object",
1247+ properties: {
1248+ organization: {
1249+ $ref: "#/$defs/graphql/output/GraphqlTypenameOnly",
1250+ },
1251+ projects: {
1252+ $ref: "#/$defs/graphql/output/GraphqlTypenameOnly",
1253+ },
1254+ __typename: {
1255+ type: "string",
1256+ },
1257+ },
1258+ required: ["organization", "projects", "__typename"],
1259+ additionalProperties: false,
1260+ },
1261+ GraphqlTypenameOnly: {
1262+ type: "object",
1263+ properties: {
1264+ __typename: {
1265+ type: "string",
1266+ },
1267+ },
1268+ required: ["__typename"],
1269+ additionalProperties: false,
1270+ },
1271+ },
1272+ },
1273+ },
1274+ },
1275+ providerData: {
1276+ kind: "graphql",
1277+ toolKind: "field",
1278+ toolId: "teamDepthRefs",
1279+ rawToolId: "teamDepthRefs",
1280+ group: "query",
1281+ leaf: "teamDepthRefs",
1282+ fieldName: "teamDepthRefs",
1283+ operationType: "query",
1284+ operationName: "TeamDepthRefsQuery",
1285+ operationDocument:
1286+ "query TeamDepthRefsQuery { teamDepthRefs { organization { __typename } projects { __typename } __typename } }",
1287+ queryTypeName: "Query",
1288+ mutationTypeName: null,
1289+ subscriptionTypeName: null,
1290+ },
1291+ },
1292+ ],
1293+ });
1294+
1295+ const executable = Object.values(snapshot.catalog.executables)[0]!;
1296+ const resultShape =
1297+ snapshot.catalog.symbols[executable.projection.resultDataShapeId!];
1298+ expect(resultShape?.kind).toBe("shape");
1299+ if (!resultShape || resultShape.kind !== "shape") {
1300+ throw new Error("Expected result data shape symbol");
1301+ }
1302+
1303+ expect(resultShape.node.type).toBe("ref");
1304+ if (resultShape.node.type !== "ref") {
1305+ throw new Error("Expected result data ref shape");
1306+ }
1307+
1308+ const teamShape = snapshot.catalog.symbols[resultShape.node.target];
1309+ expect(teamShape?.kind).toBe("shape");
1310+ if (!teamShape || teamShape.kind !== "shape" || teamShape.node.type !== "object") {
1311+ throw new Error("Expected Team object shape");
1312+ }
1313+
1314+ const organizationFieldShape =
1315+ snapshot.catalog.symbols[teamShape.node.fields.organization!.shapeId];
1316+ const projectsFieldShape =
1317+ snapshot.catalog.symbols[teamShape.node.fields.projects!.shapeId];
1318+ expect(organizationFieldShape?.kind).toBe("shape");
1319+ expect(projectsFieldShape?.kind).toBe("shape");
1320+ if (
1321+ !organizationFieldShape
1322+ || organizationFieldShape.kind !== "shape"
1323+ || organizationFieldShape.node.type !== "ref"
1324+ || !projectsFieldShape
1325+ || projectsFieldShape.kind !== "shape"
1326+ || projectsFieldShape.node.type !== "ref"
1327+ ) {
1328+ throw new Error("Expected GraphQL output ref field shapes");
1329+ }
1330+
1331+ expect(organizationFieldShape.node.target).toBe(
1332+ projectsFieldShape.node.target,
1333+ );
1334+
1335+ const organizationTarget =
1336+ snapshot.catalog.symbols[organizationFieldShape.node.target];
1337+ const projectsTarget =
1338+ snapshot.catalog.symbols[projectsFieldShape.node.target];
1339+ expect(organizationTarget?.kind).toBe("shape");
1340+ expect(projectsTarget?.kind).toBe("shape");
1341+ if (
1342+ !organizationTarget
1343+ || organizationTarget.kind !== "shape"
1344+ || !projectsTarget
1345+ || projectsTarget.kind !== "shape"
1346+ ) {
1347+ throw new Error("Expected GraphQL output target shapes");
1348+ }
1349+
1350+ expect(organizationTarget.provenance[0]?.pointer).toBe(
1351+ "#/$defs/graphql/output/GraphqlTypenameOnly",
1352+ );
1353+ expect(projectsTarget.provenance[0]?.pointer).toBe(
1354+ "#/$defs/graphql/output/GraphqlTypenameOnly",
1355+ );
1356+ });
12011357 });
packages/platform/control-plane/src/runtime/catalog/source/type-declarations.test.ts+19−5
@@ -487,6 +487,7 @@ describe("source-type-declarations", () => { · 486 unmodified lines
487487 );
488488 expect(sourceDeclaration).toContain("type LinearTeamsCall = {");
489489 expect(sourceDeclaration).toContain("type LinearTeamsResult = {");
490+ expect(sourceDeclaration).toContain("type LinearTeamsSearchResult = {");
490491 expect(sourceDeclaration.match(/type LinearTeamsCall =/g)?.length).toBe(
491492 1,
492493 );
@@ -497,8 +498,13 @@ describe("source-type-declarations", () => { · 4 unmodified lines
497498 "teams: (args?: LinearTeamsCall) => Promise<LinearTeamsResult>;",
498499 );
499500 expect(sourceDeclaration).toContain(
500− "teamsSearch: (args?: LinearTeamsCall) => Promise<LinearTeamsResult>;",
501+ "teamsSearch: (args?: LinearTeamsCall) => Promise<LinearTeamsSearchResult>;",
501502 );
503+ expect(sourceDeclaration).toContain("filter?: {");
504+ expect(sourceDeclaration).toContain("name?: string;");
505+ expect(sourceDeclaration).not.toContain("type ResultData");
506+ expect(sourceDeclaration).not.toContain("type ResponseHeaders");
507+ expect(sourceDeclaration).not.toContain("Member1");
502508 expect(sourceDeclaration).not.toMatch(/type\s+[0-9]/);
503509 expect(aggregateDeclaration).toContain(
504510 'import type { SourceTools_src_linear } from "../sources/src_linear";',
@@ -687,10 +693,15 @@ describe("source-type-declarations", () => { · 182 unmodified lines
687693 "src_linear.d.ts",
688694 ),
689695 );
690−
691− expect(sourceDeclaration).toContain("unsupportedNot?: unknown;");
692696 expect(sourceDeclaration).toContain(
693− "unsupportedConditional?: unknown;",
697+ "unsupportedNot?: UnsupportedNot;",
698+ );
699+ expect(sourceDeclaration).toContain(
700+ "unsupportedConditional?: UnsupportedConditional;",
701+ );
702+ expect(sourceDeclaration).toContain("type UnsupportedNot = unknown;");
703+ expect(sourceDeclaration).toContain(
704+ "type UnsupportedConditional = unknown;",
694705 );
695706 }),
696707 );
@@ -926,7 +937,10 @@ describe("source-type-declarations", () => { · 229 unmodified lines
926937 );
927938
928939 expect(sourceDeclaration).toContain(
929− "teams: (args?: LinearTeamsCall) => Promise<{",
940+ "teams: (args?: LinearTeamsCall) => Promise<LinearTeamsResult>;",
941+ );
942+ expect(sourceDeclaration).toContain(
943+ "type AbuseBlockHistoryItem = {",
930944 );
931945 expect(sourceDeclaration).toContain("actor?: string;");
932946 expect(sourceDeclaration).toContain('action: "blocked";');
packages/platform/control-plane/src/runtime/sources/source-inspection.ts+4−3
@@ -301,7 +301,7 @@ const resolveSourceInspectionTool = (input: { · 300 unmodified lines
301301 catalogs: [loaded.catalogEntry],
302302 path: input.toolPath,
303303 includeSchemas: true,
304− includeTypePreviews: true,
304+ includeTypePreviews: false,
305305 });
306306
307307 return {
@@ -393,7 +393,8 @@ export const getSourceInspection = (input: { · 85 unmodified lines
393393 const inspection = yield* resolveSourceInspection({
394394 ...input,
395395 includeSchemas: false,
396− includeTypePreviews: true,
396+ // Keep the initial tool list lightweight; full type previews are built on demand per tool.
397+ includeTypePreviews: false,
397398 });
398399
399400 return {
@@ -452,7 +453,7 @@ export const discoverSourceInspectionTools = (input: { · 52 unmodified lines
452453 workspaceId: input.workspaceId,
453454 sourceId: input.sourceId,
454455 includeSchemas: false,
455− includeTypePreviews: true,
456+ includeTypePreviews: false,
456457 });
457458 const queryTokens = tokenize(input.payload.query);
458459 const results = inspection.tools
packages/sources/core/src/catalog-json-schema.ts+78−2
@@ -48,6 +48,80 @@ const asStringArray = (value: unknown): string[] => · 47 unmodified lines
4848 return stringValue === null ? [] : [stringValue];
4949 });
5050
51+const decodePointerSegment = (value: string): string =>
52+ value.replace(/~1/g, "/").replace(/~0/g, "~");
53+
54+const isNamedSchemaPointer = (pointer: string): boolean => {
55+ if (!pointer.startsWith("#/")) {
56+ return false;
57+ }
58+
59+ const segments = pointer
60+ .slice(2)
61+ .split("/")
62+ .map(decodePointerSegment);
63+
64+ for (let index = 0; index < segments.length; index += 1) {
65+ const segment = segments[index];
66+ const next = segments[index + 1];
67+ const afterNext = segments[index + 2];
68+
69+ if (
70+ (segment === "$defs" || segment === "definitions")
71+ && next !== undefined
72+ ) {
73+ if (index + 2 === segments.length) {
74+ return true;
75+ }
76+ continue;
77+ }
78+
79+ if (
80+ segment === "components"
81+ && next !== undefined
82+ && afterNext !== undefined
83+ && [
84+ "schemas",
85+ "responses",
86+ "parameters",
87+ "headers",
88+ "requestBodies",
89+ "examples",
90+ "links",
91+ "callbacks",
92+ ].includes(next)
93+ ) {
94+ if (index + 3 === segments.length) {
95+ return true;
96+ }
97+ continue;
98+ }
99+
100+ if (
101+ segment === "graphql"
102+ && next !== undefined
103+ && afterNext !== undefined
104+ && ["input", "output", "scalars", "enums"].includes(next)
105+ ) {
106+ if (index + 3 === segments.length) {
107+ return true;
108+ }
109+ continue;
110+ }
111+
112+ if (
113+ ["input", "output", "scalars", "enums"].includes(segment)
114+ && next !== undefined
115+ ) {
116+ if (index + 2 === segments.length) {
117+ return true;
118+ }
119+ }
120+ }
121+
122+ return false;
123+};
124+
51125 const addDiagnostic = (
52126 catalog: CatalogFragmentBuilder,
53127 input: Omit<ImportDiagnostic, "id">,
@@ -148,6 +222,8 @@ export const createJsonSchemaImporter = (input: { · 94 unmodified lines
148222 description: asString(objectSchema.description),
149223 });
150224 const deprecated = asBoolean(objectSchema.deprecated) ?? undefined;
225+ const preserveNominalIdentity =
226+ title !== undefined || isNamedSchemaPointer(key);
151227
152228 const register = (
153229 node: ShapeNode,
@@ -158,7 +234,7 @@ export const createJsonSchemaImporter = (input: { · 4 unmodified lines
158234 ): ReturnType<typeof ShapeSymbolIdSchema.make> => {
159235 const signature = stableStringify(node);
160236 const recursive = recursiveShapeIds.has(shapeId);
161− const existingShapeId = recursive
237+ const existingShapeId = recursive || preserveNominalIdentity
162238 ? undefined
163239 : structuralCache.get(signature);
164240
@@ -214,7 +290,7 @@ export const createJsonSchemaImporter = (input: { · 49 unmodified lines
214290 : {}),
215291 } satisfies ShapeSymbol;
216292
217− if (!recursive) {
293+ if (!recursive && !preserveNominalIdentity) {
218294 structuralCache.set(signature, shapeId);
219295 }
220296
packages/sources/graphql/src/graphql-tools.ts+15−6
@@ -469,6 +469,9 @@ const graphqlInputObjectRef = (name: string): string => · 468 unmodified lines
469469 const graphqlOutputTypeRef = (name: string, depth: number): string =>
470470 `${GRAPHQL_SCHEMA_REF_PREFIX}/output/${depth === 0 ? name : `${name}__depth${depth}`}`;
471471
472+const graphqlTypenameOnlyOutputRef = (): string =>
473+ `${GRAPHQL_SCHEMA_REF_PREFIX}/output/GraphqlTypenameOnly`;
474+
472475 const schemaRef = (ref: string): JsonSchema => ({
473476 $ref: ref,
474477 });
@@ -490,6 +493,15 @@ const createGraphqlSchemaRefTableBuilder = () => { · 15 unmodified lines
490493 refTable[ref] = JSON.stringify(schema);
491494 };
492495
496+ const ensureTypenameOnlyOutputRef = (): JsonSchema => {
497+ const ref = graphqlTypenameOnlyOutputRef();
498+ if (!(ref in refTable)) {
499+ defineRefSchema(ref, typenameOnlyObjectSchema());
500+ }
501+
502+ return schemaRef(ref);
503+ };
504+
493505 const ensureScalarRef = (name: string): JsonSchema => {
494506 const ref = graphqlScalarRef(name);
495507 if (!(ref in refTable)) {
@@ -614,28 +626,26 @@ const createGraphqlSchemaRefTableBuilder = () => { · 118 unmodified lines
614626 if (isUnionType(namedType)) {
615627 const result = {
616628 selectionSet: "{ __typename }",
617− schema: schemaRef(ref),
629+ schema: ensureTypenameOnlyOutputRef(),
618630 } satisfies SelectedGraphqlOutput;
619631 outputSelectionsByRef.set(ref, result);
620− defineRefSchema(ref, typenameOnlyObjectSchema());
621632 return result;
622633 }
623634
624635 if (!isObjectType(namedType) && !isInterfaceType(namedType)) {
625636 return {
626637 selectionSet: "{ __typename }",
627− schema: typenameOnlyObjectSchema(),
638+ schema: ensureTypenameOnlyOutputRef(),
628639 };
629640 }
630641
631642 const fallback = {
632643 selectionSet: "{ __typename }",
633− schema: schemaRef(ref),
644+ schema: ensureTypenameOnlyOutputRef(),
634645 } satisfies SelectedGraphqlOutput;
635646 outputSelectionsByRef.set(ref, fallback);
636647
637648 if (depth >= 2) {
638− defineRefSchema(ref, typenameOnlyObjectSchema());
639649 return fallback;
640650 }
641651
@@ -665,7 +675,6 @@ const createGraphqlSchemaRefTableBuilder = () => { · 23 unmodified lines
665675 ]);
666676
667677 if (selectedFields.length === 0) {
668− defineRefSchema(ref, typenameOnlyObjectSchema());
669678 return fallback;
670679 }
671680