History

Bump tool source cache version to v24 and enhance JSON Schema type hinting for unions and object variants

Rhys Sullivan committed 8 months ago

Commit ae17ce31 parent 34a8eeb3 files · +559 −11Browse files

Unified · Split

3 files
  1. executor/packages/convex/runtime/tool_source_loading.ts +1 −1
  2. executor/packages/core/src/openapi/schema-hints.test.ts +206 −3
  3. executor/packages/core/src/openapi/schema-hints.ts +352 −7
executor/packages/convex/runtime/tool_source_loading.ts+1−1
@@ -25,7 +25,7 @@ import { asPayload } from "../lib/object"; · 24 unmodified lines
25const OPENAPI_SPEC_CACHE_TTL_MS = 5 * 60 * 60_000;25const OPENAPI_SPEC_CACHE_TTL_MS = 5 * 60 * 60_000;
26 26
27/** Cache version - bump when tool snapshot/registry/type-hint semantics change. */27/** Cache version - bump when tool snapshot/registry/type-hint semantics change. */
28const TOOL_SOURCE_CACHE_VERSION = "v20";28const TOOL_SOURCE_CACHE_VERSION = "v24";
29 29
30export function sourceSignature(workspaceId: string, sources: Array<{ id: string; updatedAt: number; enabled: boolean }>): string {30export function sourceSignature(workspaceId: string, sources: Array<{ id: string; updatedAt: number; enabled: boolean }>): string {
31 const parts = sources31 const parts = sources
executor/packages/core/src/openapi/schema-hints.test.ts+206−3
@@ -52,9 +52,8 @@ test("jsonSchemaTypeHintFallback factors common object fields in oneOf", () => { · 51 unmodified lines
52 const hint = jsonSchemaTypeHintFallback(schema);52 const hint = jsonSchemaTypeHintFallback(schema);
53 expect(hint).toContain("domain: string");53 expect(hint).toContain("domain: string");
54 expect(hint).toContain("value: string");54 expect(hint).toContain("value: string");
55 expect(jsonSchemaTypeHintFallback(schema)).toContain("& (");55 // Variants only differ by the discriminant; they should be merged.
56 expect(jsonSchemaTypeHintFallback(schema)).toContain("type: \"A\"");56 expect(hint).toContain("type: \"A\" | \"AAAA\"");
57 expect(jsonSchemaTypeHintFallback(schema)).toContain("type: \"AAAA\"");
58});57});
59 58
60test("jsonSchemaTypeHintFallback parenthesizes union inside intersection", () => {59test("jsonSchemaTypeHintFallback parenthesizes union inside intersection", () => {
@@ -100,3 +99,207 @@ test("jsonSchemaTypeHintFallback inlines small component schema refs at depth th · 39 unmodified lines
100 expect(hint).toContain("count");99 expect(hint).toContain("count");
101 expect(hint).not.toContain("components[\"schemas\"][\"Pagination\"]");100 expect(hint).not.toContain("components[\"schemas\"][\"Pagination\"]");
102});101});
102
103test("jsonSchemaTypeHintFallback drops plain scalar variants when union has multiple object shapes", () => {
104 const schema = {
105 oneOf: [
106 { type: "string" },
107 {
108 type: "object",
109 properties: {
110 records: { type: "string" },
111 },
112 required: ["records"],
113 },
114 {
115 type: "object",
116 properties: {
117 records: { type: "string" },
118 pagination: { type: "number" },
119 },
120 required: ["records", "pagination"],
121 },
122 ],
123 };
124
125 // string variant is pruned; remaining two objects collapse into optional pagination.
126 expect(jsonSchemaTypeHintFallback(schema)).toBe("{ records: string; pagination?: number }");
127});
128
129test("jsonSchemaTypeHintFallback drops empty object variant when a more specific object is present", () => {
130 const schema = {
131 oneOf: [
132 { type: "object" },
133 {
134 type: "object",
135 properties: {
136 a: { type: "string" },
137 },
138 required: ["a"],
139 },
140 ],
141 };
142
143 expect(jsonSchemaTypeHintFallback(schema)).toBe("{ a: string }");
144});
145
146test("jsonSchemaTypeHintFallback keeps scalar | object when union has only one object variant", () => {
147 const schema = {
148 oneOf: [
149 { type: "string" },
150 {
151 type: "object",
152 properties: {
153 a: { type: "string" },
154 },
155 required: ["a"],
156 },
157 ],
158 };
159
160 expect(jsonSchemaTypeHintFallback(schema)).toBe("string | { a: string }");
161});
162
163test("jsonSchemaTypeHintFallback partially factors repeated required keys from large unions", () => {
164 const schema = {
165 oneOf: [
166 {
167 type: "object",
168 properties: {
169 name: { type: "string" },
170 type: { enum: ["A"] },
171 value: { type: "string" },
172 },
173 required: ["name", "type", "value"],
174 },
175 {
176 type: "object",
177 properties: {
178 name: { type: "string" },
179 type: { enum: ["AAAA"] },
180 value: { type: "string" },
181 },
182 required: ["name", "type", "value"],
183 },
184 {
185 type: "object",
186 properties: {
187 name: { type: "string" },
188 type: { enum: ["CNAME"] },
189 value: { type: "string" },
190 },
191 required: ["name", "type"],
192 },
193 {
194 type: "object",
195 properties: {
196 type: { enum: ["SRV"] },
197 srv: { type: "object", properties: { target: { type: "string" } }, required: ["target"] },
198 },
199 required: ["type", "srv"],
200 },
201 ],
202 };
203
204 const hint = jsonSchemaTypeHintFallback(schema);
205 // `name` should only appear once after factoring.
206 expect(hint.split("name: string").length - 1).toBe(1);
207 // Still must mention SRV variant.
208 expect(hint).toContain("type: \"SRV\"");
209 // The factored form uses an intersection.
210 expect(hint).toContain("& (");
211});
212
213test("jsonSchemaTypeHintFallback factors common fields and then partially factors repeated keys inside residual union", () => {
214 const schema = {
215 anyOf: [
216 {
217 type: "object",
218 properties: {
219 name: { type: "string" },
220 type: { enum: ["A"] },
221 value: { type: "string" },
222 ttl: { type: "number" },
223 comment: { type: "string" },
224 },
225 required: ["name", "type", "value"],
226 },
227 {
228 type: "object",
229 properties: {
230 name: { type: "string" },
231 type: { enum: ["AAAA"] },
232 value: { type: "string" },
233 ttl: { type: "number" },
234 comment: { type: "string" },
235 },
236 required: ["name", "type", "value"],
237 },
238 {
239 type: "object",
240 properties: {
241 type: { enum: ["SRV"] },
242 srv: {
243 type: "object",
244 properties: { target: { type: "string" } },
245 required: ["target"],
246 },
247 ttl: { type: "number" },
248 comment: { type: "string" },
249 },
250 required: ["type", "srv"],
251 },
252 {
253 type: "object",
254 properties: {
255 name: { type: "string" },
256 type: { enum: ["NS"] },
257 value: { type: "string" },
258 ttl: { type: "number" },
259 comment: { type: "string" },
260 },
261 required: ["name", "type"],
262 },
263 ],
264 };
265
266 const hint = jsonSchemaTypeHintFallback(schema);
267 // `ttl`/`comment` appear once as common fields.
268 expect(hint.split("ttl?: number").length - 1).toBe(1);
269 expect(hint.split("comment?: string").length - 1).toBe(1);
270 // `name` should also be factored once inside the residual union.
271 expect(hint.split("name: string").length - 1).toBe(1);
272});
273
274test("jsonSchemaTypeHintFallback merges discriminated object variants that only differ by type enum", () => {
275 const schema = {
276 oneOf: [
277 {
278 type: "object",
279 properties: {
280 name: { type: "string" },
281 type: { type: "string", enum: ["A"] },
282 value: { type: "string" },
283 },
284 required: ["name", "type", "value"],
285 },
286 {
287 type: "object",
288 properties: {
289 name: { type: "string" },
290 type: { type: "string", enum: ["AAAA"] },
291 value: { type: "string" },
292 },
293 required: ["name", "type", "value"],
294 },
295 ],
296 };
297
298 const hint = jsonSchemaTypeHintFallback(schema);
299 expect(hint).toContain("name: string");
300 expect(hint).toContain("value: string");
301 expect(hint).toContain("type: \"A\" | \"AAAA\"");
302 // Should not render as two separate object variants.
303 expect(hint).not.toContain("type: \"A\"; value");
304 expect(hint).not.toContain("type: \"AAAA\"; value");
305});
executor/packages/core/src/openapi/schema-hints.ts+352−7
@@ -242,6 +242,179 @@ function isObjectSchema(schema: JsonSchema): boolean { · 241 unmodified lines
242 return Object.keys(asRecord(schema.properties)).length > 0;242 return Object.keys(asRecord(schema.properties)).length > 0;
243}243}
244 244
245function isEmptyObjectSchema(schema: JsonSchema): boolean {
246 const type = typeof schema.type === "string" ? schema.type : undefined;
247 if (type !== "object") return false;
248 return Object.keys(asRecord(schema.properties)).length === 0;
249}
250
251function isPlainScalarSchema(schema: JsonSchema): boolean {
252 const type = typeof schema.type === "string" ? schema.type : undefined;
253 if (type !== "string" && type !== "number" && type !== "integer" && type !== "boolean") {
254 return false;
255 }
256 if (Array.isArray(schema.enum) && schema.enum.length > 0) return false;
257
258 // If this scalar has any meaningful constraints, keep it.
259 const allowedKeys = new Set([
260 "type",
261 "title",
262 "description",
263 "deprecated",
264 "examples",
265 "example",
266 "nullable",
267 "default",
268 ]);
269 for (const key of Object.keys(schema)) {
270 if (!allowedKeys.has(key)) return false;
271 }
272
273 return true;
274}
275
276function normalizeUnionSchemaVariants(variants: JsonSchema[]): JsonSchema[] {
277 if (variants.length < 2) return variants;
278
279 // Many real-world OpenAPI specs include `oneOf: [{type:"object"}, { ...specific... }]`
280 // or even `oneOf: [{type:"string"}, { ...object... }, { ...object... }]`.
281 // These broad scalar/empty-object variants add noise without helping the UI/LLM.
282 const hasNonEmptyObject = variants.some((v) => isObjectSchema(v) && Object.keys(asRecord(v.properties)).length > 0);
283
284 let filtered = variants;
285 if (hasNonEmptyObject) {
286 filtered = filtered.filter((v) => !isEmptyObjectSchema(v));
287 }
288
289 const objectCount = filtered.filter(isObjectSchema).length;
290 if (objectCount >= 2) {
291 filtered = filtered.filter((v) => !isPlainScalarSchema(v));
292 }
293
294 return filtered.length > 0 ? filtered : variants;
295}
296
297function extractStringLiteralFromSchema(schema: JsonSchema): string | null {
298 // Support the two common encodings we see in OpenAPI:
299 // - { type: "string", enum: ["A"] }
300 // - { const: "A" }
301 const enumValues = Array.isArray(schema.enum) ? schema.enum : undefined;
302 if (enumValues && enumValues.length === 1 && typeof enumValues[0] === "string") {
303 return enumValues[0];
304 }
305 if (typeof (schema as Record<string, unknown>).const === "string") {
306 return String((schema as Record<string, unknown>).const);
307 }
308 return null;
309}
310
311function mergeDiscriminatedObjectUnionVariants(
312 variants: JsonSchema[],
313 discriminantKey: string,
314 depth: number,
315 componentSchemas?: Record<string, unknown>,
316 seenRefs: Set<string> = new Set(),
317): JsonSchema[] {
318 if (variants.length < 2) return variants;
319 if (!variants.every(isObjectSchema)) return variants;
320
321 type VariantInfo = {
322 idx: number;
323 schema: JsonSchema;
324 disc: string;
325 signature: string;
326 };
327
328 const infos: VariantInfo[] = [];
329
330 for (let i = 0; i < variants.length; i++) {
331 const variant = variants[i]!;
332 const props = asRecord(variant.properties);
333 const discSchema = asRecord(props[discriminantKey]);
334 const disc = extractStringLiteralFromSchema(discSchema);
335 if (!disc) {
336 return variants;
337 }
338
339 const required = new Set(
340 (Array.isArray(variant.required) ? variant.required : []).filter(
341 (k): k is string => typeof k === "string",
342 ),
343 );
344
345 const otherKeys = Object.keys(props)
346 .filter((k) => k !== discriminantKey)
347 .sort();
348 const requiredOther = [...required]
349 .filter((k) => k !== discriminantKey)
350 .sort();
351
352 const keyHints = otherKeys.map((k) => {
353 const hint = jsonSchemaTypeHintFallback(props[k], depth + 1, componentSchemas, seenRefs);
354 return `${k}:${hint}`;
355 });
356
357 // Group by: same other keys + same required set + same per-key hints.
358 const signature = JSON.stringify({ otherKeys, requiredOther, keyHints });
359 infos.push({ idx: i, schema: variant, disc, signature });
360 }
361
362 const bySig = new Map<string, VariantInfo[]>();
363 for (const info of infos) {
364 const bucket = bySig.get(info.signature);
365 if (bucket) bucket.push(info);
366 else bySig.set(info.signature, [info]);
367 }
368
369 let changed = false;
370 const used = new Set<number>();
371 const out: JsonSchema[] = [];
372
373 // Preserve stable order: walk original variants and emit merged groups
374 // when we encounter their first member.
375 for (let i = 0; i < variants.length; i++) {
376 if (used.has(i)) continue;
377 const info = infos.find((x) => x.idx === i);
378 if (!info) {
379 out.push(variants[i]!);
380 continue;
381 }
382
383 const group = bySig.get(info.signature) ?? [info];
384 if (group.length < 2) {
385 out.push(variants[i]!);
386 used.add(i);
387 continue;
388 }
389
390 // Merge group: same schema shape, only discriminant differs.
391 changed = true;
392 for (const member of group) used.add(member.idx);
393
394 const base = group[0]!.schema;
395 const baseProps = asRecord(base.properties);
396 const discSchema = asRecord(baseProps[discriminantKey]);
397 const discType = typeof discSchema.type === "string" ? discSchema.type : "string";
398
399 const mergedDiscriminant: JsonSchema = {
400 type: discType,
401 enum: group.map((m) => m.disc),
402 };
403
404 const mergedProps = {
405 ...baseProps,
406 [discriminantKey]: mergedDiscriminant,
407 };
408
409 out.push({
410 ...base,
411 properties: mergedProps,
412 });
413 }
414
415 return changed ? out : variants;
416}
417
245function subsetKeys(left: string[], right: string[]): boolean {418function subsetKeys(left: string[], right: string[]): boolean {
246 const rightSet = new Set(right);419 const rightSet = new Set(right);
247 for (const key of left) {420 for (const key of left) {
@@ -373,12 +546,138 @@ function tryFactorCommonObjectFields( · 125 unmodified lines
373 .join("; ");546 .join("; ");
374 const commonType = `{ ${commonInner} }`;547 const commonType = `{ ${commonInner} }`;
375 548
376 const residualType = joinUnion(residualSchemas.map((s) => jsonSchemaTypeHintFallback(s, depth + 1, componentSchemas, seenRefs)));549 const partiallyFactoredResidual = tryFactorPartialCommonObjectFields(
550 residualSchemas,
551 depth,
552 componentSchemas,
553 seenRefs,
554 );
555
556 const residualType = partiallyFactoredResidual
557 ?? joinUnion(residualSchemas.map((s) => jsonSchemaTypeHintFallback(s, depth + 1, componentSchemas, seenRefs)));
377 if (residualType === "unknown") return commonType;558 if (residualType === "unknown") return commonType;
378 559
379 return `${commonType} & (${residualType})`;560 return `${commonType} & (${residualType})`;
380}561}
381 562
563function tryFactorPartialCommonObjectFields(
564 variants: JsonSchema[],
565 depth: number,
566 componentSchemas?: Record<string, unknown>,
567 seenRefs: Set<string> = new Set(),
568): string | null {
569 // If there's a large union where a field repeats across many (but not all)
570 // object variants (e.g. Vercel DNS createRecord repeats `name` in most
571 // variants), pull it out as:
572 // ({ name: string } & (<union-without-name>)) | <remaining>
573 // This is purely a *hint* readability improvement.
574
575 if (variants.length < 3) return null;
576 if (!variants.every(isObjectSchema)) return null;
577
578 const propsList = variants.map((v) => asRecord(v.properties));
579 const requiredList = variants.map((v) => new Set(
580 (Array.isArray(v.required) ? v.required : []).filter((k): k is string => typeof k === "string"),
581 ));
582
583 type Candidate = {
584 key: string;
585 hint: string;
586 indices: number[];
587 };
588
589 const candidates: Candidate[] = [];
590 const minGroupSize = variants.length >= 4 ? 3 : 2;
591
592 // Collect keys present in at least minGroupSize variants.
593 const keyCounts = new Map<string, number>();
594 for (const props of propsList) {
595 for (const key of Object.keys(props)) {
596 keyCounts.set(key, (keyCounts.get(key) ?? 0) + 1);
597 }
598 }
599
600 for (const [key, count] of keyCounts.entries()) {
601 if (count < minGroupSize) continue;
602
603 // Consider only keys that are required in the variants we factor.
604 const indicesWithKey = propsList
605 .map((props, idx) => (props[key] !== undefined ? idx : -1))
606 .filter((idx) => idx !== -1);
607
608 // Group by identical type hint.
609 const byHint = new Map<string, number[]>();
610 for (const idx of indicesWithKey) {
611 const variant = variants[idx]!;
612 if (!requiredList[idx]!.has(key)) continue;
613 const props = propsList[idx]!;
614 const hint = jsonSchemaTypeHintFallback(props[key], depth + 1, componentSchemas, seenRefs);
615 const bucket = byHint.get(hint);
616 if (bucket) bucket.push(idx);
617 else byHint.set(hint, [idx]);
618 }
619
620 for (const [hint, indices] of byHint.entries()) {
621 if (indices.length < minGroupSize) continue;
622 if (indices.length === variants.length) continue;
623 candidates.push({ key, hint, indices });
624 }
625 }
626
627 if (candidates.length === 0) return null;
628
629 // Pick the best candidate: largest group first, then stable key order.
630 candidates.sort((a, b) => {
631 if (b.indices.length !== a.indices.length) return b.indices.length - a.indices.length;
632 if (a.key !== b.key) return a.key.localeCompare(b.key);
633 return a.hint.localeCompare(b.hint);
634 });
635 const best = candidates[0]!;
636
637 // Build the factored group.
638 const commonType = `{ ${formatTsPropertyKey(best.key)}: ${best.hint} }`;
639 const bestSet = new Set(best.indices);
640
641 const residualSchemas: JsonSchema[] = [];
642 const remainingSchemas: JsonSchema[] = [];
643
644 for (let i = 0; i < variants.length; i++) {
645 const variant = variants[i]!;
646 const props = propsList[i]!;
647 if (!bestSet.has(i)) {
648 remainingSchemas.push(variant);
649 continue;
650 }
651
652 const residualProps = Object.fromEntries(
653 Object.entries(props).filter(([k]) => k !== best.key),
654 );
655 const req = Array.isArray(variant.required) ? variant.required : [];
656 const residualRequired = req.filter(
657 (k) => typeof k === "string" && k !== best.key,
658 );
659
660 residualSchemas.push({
661 type: "object",
662 properties: residualProps,
663 ...(residualRequired.length > 0 ? { required: residualRequired } : {}),
664 } as unknown as JsonSchema);
665 }
666
667 // If factoring doesn't actually remove anything useful, bail.
668 const anyResidualHasProps = residualSchemas.some(
669 (s) => Object.keys(asRecord(s.properties)).length > 0,
670 );
671 if (!anyResidualHasProps) return null;
672
673 const residualType = joinUnion(residualSchemas.map((s) => jsonSchemaTypeHintFallback(s, depth + 1, componentSchemas, seenRefs)));
674 const groupType = `${commonType} & (${residualType})`;
675
676 if (remainingSchemas.length === 0) return groupType;
677 const remainingType = joinUnion(remainingSchemas.map((s) => jsonSchemaTypeHintFallback(s, depth + 1, componentSchemas, seenRefs)));
678 return joinUnion([groupType, remainingType]);
679}
680
382function joinIntersection(parts: string[]): string {681function joinIntersection(parts: string[]): string {
383 const expanded = parts.flatMap((part) => splitTopLevelBy(part, "&"));682 const expanded = parts.flatMap((part) => splitTopLevelBy(part, "&"));
384 const unique = dedupeTypeParts(expanded).filter((part) => part !== "unknown");683 const unique = dedupeTypeParts(expanded).filter((part) => part !== "unknown");
@@ -431,8 +730,23 @@ export function jsonSchemaTypeHintFallback( · 46 unmodified lines
431 730
432 const oneOf = Array.isArray(shape.oneOf) ? shape.oneOf : undefined;731 const oneOf = Array.isArray(shape.oneOf) ? shape.oneOf : undefined;
433 if (oneOf && oneOf.length > 0) {732 if (oneOf && oneOf.length > 0) {
434 const collapsed = tryCollapseSimpleObjectUnion(733 const variants = normalizeUnionSchemaVariants(
435 oneOf.filter((entry): entry is JsonSchema => Boolean(entry && typeof entry === "object")),734 oneOf.filter((entry): entry is JsonSchema => Boolean(entry && typeof entry === "object")),
735 );
736 const objectVariantsMerged = mergeDiscriminatedObjectUnionVariants(
737 variants.filter(isObjectSchema),
738 "type",
739 depth,
740 componentSchemas,
741 seenRefs,
742 );
743 const mergedVariants = variants.some((v) => !isObjectSchema(v))
744 ? variants.filter((v) => !isObjectSchema(v)).concat(objectVariantsMerged)
745 : objectVariantsMerged;
746 const objectVariants = mergedVariants.filter(isObjectSchema);
747
748 const collapsed = tryCollapseSimpleObjectUnion(
749 objectVariants,
436 depth,750 depth,
437 componentSchemas,751 componentSchemas,
438 seenRefs,752 seenRefs,
@@ -440,19 +754,42 @@ export function jsonSchemaTypeHintFallback( · 1 unmodified line
440 if (collapsed) return collapsed;754 if (collapsed) return collapsed;
441 755
442 const factored = tryFactorCommonObjectFields(756 const factored = tryFactorCommonObjectFields(
443 oneOf.filter((entry): entry is JsonSchema => Boolean(entry && typeof entry === "object")),757 objectVariants,
444 depth,758 depth,
445 componentSchemas,759 componentSchemas,
446 seenRefs,760 seenRefs,
447 );761 );
448 if (factored) return factored;762 if (factored) return factored;
449 return joinUnion(oneOf.map((entry) => jsonSchemaTypeHintFallback(entry, depth + 1, componentSchemas, seenRefs)));763
764 const partial = tryFactorPartialCommonObjectFields(
765 objectVariants,
766 depth,
767 componentSchemas,
768 seenRefs,
769 );
770 if (partial) return partial;
771 return joinUnion(mergedVariants.map((entry) => jsonSchemaTypeHintFallback(entry, depth + 1, componentSchemas, seenRefs)));
450 }772 }
451 773
452 const anyOf = Array.isArray(shape.anyOf) ? shape.anyOf : undefined;774 const anyOf = Array.isArray(shape.anyOf) ? shape.anyOf : undefined;
453 if (anyOf && anyOf.length > 0) {775 if (anyOf && anyOf.length > 0) {
454 const collapsed = tryCollapseSimpleObjectUnion(776 const variants = normalizeUnionSchemaVariants(
455 anyOf.filter((entry): entry is JsonSchema => Boolean(entry && typeof entry === "object")),777 anyOf.filter((entry): entry is JsonSchema => Boolean(entry && typeof entry === "object")),
778 );
779 const objectVariantsMerged = mergeDiscriminatedObjectUnionVariants(
780 variants.filter(isObjectSchema),
781 "type",
782 depth,
783 componentSchemas,
784 seenRefs,
785 );
786 const mergedVariants = variants.some((v) => !isObjectSchema(v))
787 ? variants.filter((v) => !isObjectSchema(v)).concat(objectVariantsMerged)
788 : objectVariantsMerged;
789 const objectVariants = mergedVariants.filter(isObjectSchema);
790
791 const collapsed = tryCollapseSimpleObjectUnion(
792 objectVariants,
456 depth,793 depth,
457 componentSchemas,794 componentSchemas,
458 seenRefs,795 seenRefs,
@@ -460,13 +797,21 @@ export function jsonSchemaTypeHintFallback( · 1 unmodified line
460 if (collapsed) return collapsed;797 if (collapsed) return collapsed;
461 798
462 const factored = tryFactorCommonObjectFields(799 const factored = tryFactorCommonObjectFields(
463 anyOf.filter((entry): entry is JsonSchema => Boolean(entry && typeof entry === "object")),800 objectVariants,
464 depth,801 depth,
465 componentSchemas,802 componentSchemas,
466 seenRefs,803 seenRefs,
467 );804 );
468 if (factored) return factored;805 if (factored) return factored;
469 return joinUnion(anyOf.map((entry) => jsonSchemaTypeHintFallback(entry, depth + 1, componentSchemas, seenRefs)));806
807 const partial = tryFactorPartialCommonObjectFields(
808 objectVariants,
809 depth,
810 componentSchemas,
811 seenRefs,
812 );
813 if (partial) return partial;
814 return joinUnion(mergedVariants.map((entry) => jsonSchemaTypeHintFallback(entry, depth + 1, componentSchemas, seenRefs)));
470 }815 }
471 816
472 const allOf = Array.isArray(shape.allOf) ? shape.allOf : undefined;817 const allOf = Array.isArray(shape.allOf) ? shape.allOf : undefined;