@arcteninc/core 0.0.44 → 0.0.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcteninc/core",
3
- "version": "0.0.44",
3
+ "version": "0.0.46",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",
@@ -33,6 +33,7 @@ interface FunctionMetadata {
33
33
  type: 'object';
34
34
  properties: Record<string, JsonSchemaProperty>;
35
35
  required?: string[]; // Array of required parameter names
36
+ $defs?: Record<string, JsonSchemaProperty>; // Type definitions for recursive types
36
37
  };
37
38
  returnType?: string;
38
39
  isAsync?: boolean;
@@ -627,6 +628,48 @@ function getTypeId(type: ts.Type): number | undefined {
627
628
  return (type as any).id;
628
629
  }
629
630
 
631
+ /**
632
+ * Get a stable name for a type to use in $defs
633
+ * Returns the type name if it's a named type (interface, type alias, class)
634
+ * Otherwise returns a generated name based on type ID
635
+ */
636
+ function getTypeNameForDefs(
637
+ type: ts.Type,
638
+ checker: ts.TypeChecker
639
+ ): string | null {
640
+ const symbol = type.getSymbol();
641
+ if (symbol) {
642
+ const name = symbol.getName();
643
+ if (name && name.length > 0) {
644
+ // Check if it's a named type (not a primitive)
645
+ const declarations = symbol.getDeclarations();
646
+ if (declarations && declarations.length > 0) {
647
+ // Check if it's a type alias, interface, or class
648
+ for (const decl of declarations) {
649
+ if (
650
+ ts.isTypeAliasDeclaration(decl) ||
651
+ ts.isInterfaceDeclaration(decl) ||
652
+ ts.isClassDeclaration(decl)
653
+ ) {
654
+ return name;
655
+ }
656
+ }
657
+ }
658
+ }
659
+ }
660
+
661
+ // Fallback: try to get a name from typeToString
662
+ const typeString = checker.typeToString(type);
663
+ // If it's a simple type name (not a complex expression), use it
664
+ if (typeString && !typeString.includes('|') && !typeString.includes('&') &&
665
+ !typeString.includes('<') && !typeString.includes('(') &&
666
+ typeString.length < 50 && /^[A-Za-z_][A-Za-z0-9_]*$/.test(typeString)) {
667
+ return typeString;
668
+ }
669
+
670
+ return null;
671
+ }
672
+
630
673
  /**
631
674
  * Serialize a literal type (boolean, number, or string literal) to JSON Schema
632
675
  * Returns null if the type is not a literal
@@ -698,9 +741,10 @@ function isFallbackAnySchema(
698
741
  type: ts.Type,
699
742
  checker: ts.TypeChecker
700
743
  ): boolean {
701
- // If it's not 'any', it's a real schema
702
- if (schema.type !== 'any') {
703
- return false;
744
+ // Empty schema {} means "any value" in JSON Schema draft 2020-12
745
+ // If schema is empty (no keys), it's a fallback "any" schema
746
+ if (Object.keys(schema).length === 0) {
747
+ return true;
704
748
  }
705
749
 
706
750
  // If it has properties, anyOf, or items, it's a real schema
@@ -708,6 +752,11 @@ function isFallbackAnySchema(
708
752
  return false;
709
753
  }
710
754
 
755
+ // If it has a type property (and it's not 'any'), it's a real schema
756
+ if (schema.type && schema.type !== 'any') {
757
+ return false;
758
+ }
759
+
711
760
  // Check if it's actually a type reference we couldn't resolve
712
761
  // (not a primitive, not an object we processed, etc.)
713
762
  return isTypeReference(type, checker);
@@ -749,17 +798,36 @@ function serializeType(
749
798
  type: ts.Type,
750
799
  checker: ts.TypeChecker,
751
800
  visited = new Set<number>(),
752
- depth = 0
801
+ depth = 0,
802
+ defs?: Record<string, JsonSchemaProperty>,
803
+ defsVisited = new Set<number>()
753
804
  ): { isOptional: boolean; schema: JsonSchemaProperty } {
754
805
  const typeString = checker.typeToString(type);
755
806
 
756
807
  if (depth > MAX_SERIALIZATION_DEPTH) {
757
- return { isOptional: false, schema: { type: 'any' } };
808
+ return { isOptional: false, schema: {} }; // Empty schema = any value (JSON Schema draft 2020-12)
758
809
  }
759
810
 
760
811
  const typeId = getTypeId(type);
812
+
813
+ // Handle recursive types with $defs support
761
814
  if (typeId !== undefined && visited.has(typeId)) {
762
- return { isOptional: false, schema: { type: 'any' } };
815
+ // Check if this is a named type that we can reference
816
+ if (defs) {
817
+ const typeName = getTypeNameForDefs(type, checker);
818
+ if (typeName) {
819
+ // If the definition already exists, use $ref
820
+ if (defs[typeName]) {
821
+ return { isOptional: false, schema: { $ref: `#/$defs/${typeName}` } };
822
+ }
823
+ // If we're currently building this definition, use $ref (circular reference)
824
+ if (defsVisited.has(typeId)) {
825
+ return { isOptional: false, schema: { $ref: `#/$defs/${typeName}` } };
826
+ }
827
+ }
828
+ }
829
+ // Fallback: empty schema for anonymous recursive types
830
+ return { isOptional: false, schema: {} }; // Empty schema = any value (JSON Schema draft 2020-12)
763
831
  }
764
832
 
765
833
  // Handle primitives
@@ -819,7 +887,7 @@ function serializeType(
819
887
 
820
888
  // Handle optional (T | undefined) - make it optional instead of union
821
889
  if (hasUndefined && nonNullUndefinedTypes.length === 1 && !hasNull) {
822
- const result = serializeType(nonNullUndefinedTypes[0], checker, visited, depth + 1);
890
+ const result = serializeType(nonNullUndefinedTypes[0], checker, visited, depth + 1, defs, defsVisited);
823
891
  return { isOptional: true, schema: result.schema };
824
892
  }
825
893
 
@@ -854,7 +922,7 @@ function serializeType(
854
922
  // Handle array types
855
923
  const typeArgs = (unionType as ts.TypeReference).typeArguments;
856
924
  if (typeArgs && typeArgs.length > 0) {
857
- const itemResult = serializeType(typeArgs[0], checker, visited, depth + 1);
925
+ const itemResult = serializeType(typeArgs[0], checker, visited, depth + 1, defs, defsVisited);
858
926
  anyOf.push({ type: 'array', items: itemResult.schema });
859
927
  } else {
860
928
  anyOf.push({ type: 'array' });
@@ -863,7 +931,7 @@ function serializeType(
863
931
  // Try to serialize the type (handles objects, type references, etc.)
864
932
  // This will recursively resolve type aliases and interfaces
865
933
  try {
866
- const refResult = serializeType(unionType, checker, visited, depth + 1);
934
+ const refResult = serializeType(unionType, checker, visited, depth + 1, defs, defsVisited);
867
935
 
868
936
  // Check if we got a valid schema (not just 'any' fallback)
869
937
  if (!isFallbackAnySchema(refResult.schema, unionType, checker)) {
@@ -873,25 +941,25 @@ function serializeType(
873
941
  const resolvedType = resolveTypeReference(unionType, checker);
874
942
  if (resolvedType) {
875
943
  try {
876
- const resolvedResult = serializeType(resolvedType, checker, visited, depth + 1);
944
+ const resolvedResult = serializeType(resolvedType, checker, visited, depth + 1, defs, defsVisited);
877
945
  if (!isFallbackAnySchema(resolvedResult.schema, resolvedType, checker)) {
878
946
  anyOf.push(resolvedResult.schema);
879
947
  } else {
880
- anyOf.push({ type: 'any' });
948
+ anyOf.push({}); // Empty schema = any value (JSON Schema draft 2020-12)
881
949
  }
882
950
  } catch (error) {
883
- anyOf.push({ type: 'any' });
951
+ anyOf.push({}); // Empty schema = any value (JSON Schema draft 2020-12)
884
952
  }
885
953
  } else {
886
- // It's a type reference we couldn't fully resolve - use 'any'
887
- anyOf.push({ type: 'any' });
954
+ // It's a type reference we couldn't fully resolve - use empty schema (any value)
955
+ anyOf.push({}); // Empty schema = any value (JSON Schema draft 2020-12)
888
956
  }
889
957
  }
890
958
  } catch (error) {
891
959
  // Fallback for unresolvable types
892
960
  const unionTypeString = checker.typeToString(unionType);
893
961
  console.warn(`Warning: Could not serialize union type: ${unionTypeString}`, error);
894
- anyOf.push({ type: 'any' });
962
+ anyOf.push({}); // Empty schema = any value (JSON Schema draft 2020-12)
895
963
  }
896
964
  }
897
965
  }
@@ -921,7 +989,7 @@ function serializeType(
921
989
  // Fallback
922
990
  return {
923
991
  isOptional: false,
924
- schema: { type: 'any' }
992
+ schema: {} // Empty schema = any value (JSON Schema draft 2020-12)
925
993
  };
926
994
  }
927
995
 
@@ -932,7 +1000,7 @@ function serializeType(
932
1000
  const allRequired: string[] = [];
933
1001
 
934
1002
  for (const intersectionType of types) {
935
- const result = serializeType(intersectionType, checker, visited, depth + 1);
1003
+ const result = serializeType(intersectionType, checker, visited, depth + 1, defs, defsVisited);
936
1004
  if (result.schema.type === 'object' && result.schema.properties) {
937
1005
  Object.assign(allProperties, result.schema.properties);
938
1006
  if (result.schema.required) {
@@ -957,7 +1025,7 @@ function serializeType(
957
1025
  if (checker.isArrayType(type)) {
958
1026
  const typeArgs = (type as ts.TypeReference).typeArguments;
959
1027
  if (typeArgs && typeArgs.length > 0) {
960
- const itemResult = serializeType(typeArgs[0], checker, visited, depth + 1);
1028
+ const itemResult = serializeType(typeArgs[0], checker, visited, depth + 1, defs, defsVisited);
961
1029
  return {
962
1030
  isOptional: false,
963
1031
  schema: { type: 'array', items: itemResult.schema }
@@ -975,6 +1043,28 @@ function serializeType(
975
1043
  const properties: Record<string, JsonSchemaProperty> = {};
976
1044
  const required: string[] = [];
977
1045
  const props = checker.getPropertiesOfType(type);
1046
+
1047
+ // Check if this is a named type that should go in $defs
1048
+ const typeName = defs ? getTypeNameForDefs(type, checker) : null;
1049
+ const shouldUseDefs = defs && typeName && props.length > 0;
1050
+
1051
+ if (shouldUseDefs && typeId !== undefined) {
1052
+ // Check if we're already building this definition
1053
+ if (defsVisited.has(typeId)) {
1054
+ // Circular reference - return $ref
1055
+ return { isOptional: false, schema: { $ref: `#/$defs/${typeName}` } };
1056
+ }
1057
+
1058
+ // Check if definition already exists
1059
+ if (defs[typeName]) {
1060
+ return { isOptional: false, schema: { $ref: `#/$defs/${typeName}` } };
1061
+ }
1062
+
1063
+ // Start building the definition
1064
+ defsVisited.add(typeId);
1065
+ // Create placeholder in defs (will be filled below)
1066
+ defs[typeName] = { type: 'object', properties: {}, required: [] };
1067
+ }
978
1068
 
979
1069
  if (props.length > 0) {
980
1070
  for (const prop of props) {
@@ -989,7 +1079,7 @@ function serializeType(
989
1079
  const propType = checker.getTypeOfSymbolAtLocation(prop, propDeclaration);
990
1080
  const isOptional = (prop.flags & ts.SymbolFlags.Optional) !== 0;
991
1081
 
992
- const propResult = serializeType(propType, checker, visited, depth + 1);
1082
+ const propResult = serializeType(propType, checker, visited, depth + 1, defs, defsVisited);
993
1083
  properties[prop.name] = propResult.schema;
994
1084
 
995
1085
  // Track required properties
@@ -1009,7 +1099,7 @@ function serializeType(
1009
1099
  const indexInfo = indexSignatures[0];
1010
1100
  if (indexInfo.declaration) {
1011
1101
  const indexType = checker.getTypeAtLocation(indexInfo.declaration);
1012
- const indexResult = serializeType(indexType, checker, visited, depth + 1);
1102
+ const indexResult = serializeType(indexType, checker, visited, depth + 1, defs, defsVisited);
1013
1103
  // JSON Schema uses additionalProperties for index signatures
1014
1104
  if (indexResult.schema.type !== 'any' || indexResult.schema.properties) {
1015
1105
  // Only set if it's a meaningful type (not just 'any' fallback)
@@ -1033,6 +1123,13 @@ function serializeType(
1033
1123
  schema.required = required;
1034
1124
  }
1035
1125
 
1126
+ // If we're building a definition, update it and return $ref
1127
+ if (shouldUseDefs && typeName && defs) {
1128
+ defs[typeName] = schema;
1129
+ defsVisited.delete(typeId!);
1130
+ return { isOptional: false, schema: { $ref: `#/$defs/${typeName}` } };
1131
+ }
1132
+
1036
1133
  return { isOptional: false, schema };
1037
1134
  }
1038
1135
  }
@@ -1080,8 +1177,8 @@ function serializeType(
1080
1177
  } catch (error) {
1081
1178
  console.warn(`Warning: Could not extract enum values for type: ${typeString}`, error);
1082
1179
  }
1083
- // If we can't extract enum values, use 'any'
1084
- return { isOptional: false, schema: { type: 'any' } };
1180
+ // If we can't extract enum values, use empty schema (any value)
1181
+ return { isOptional: false, schema: {} }; // Empty schema = any value (JSON Schema draft 2020-12)
1085
1182
  }
1086
1183
 
1087
1184
  // Check if it's a type reference (like InternalFilterType.StringFilter)
@@ -1093,7 +1190,7 @@ function serializeType(
1093
1190
  // Empty object or type reference we can't resolve
1094
1191
  // Check if typeString suggests it's a named type reference
1095
1192
  if (typeString && typeString.includes('.')) {
1096
- return { isOptional: false, schema: { type: 'any' } };
1193
+ return { isOptional: false, schema: {} }; // Empty schema = any value (JSON Schema draft 2020-12)
1097
1194
  }
1098
1195
  }
1099
1196
  }
@@ -1115,7 +1212,7 @@ function serializeType(
1115
1212
  const resolvedType = resolveTypeReference(type, checker);
1116
1213
  if (resolvedType) {
1117
1214
  try {
1118
- const resolvedResult = serializeType(resolvedType, checker, visited, depth + 1);
1215
+ const resolvedResult = serializeType(resolvedType, checker, visited, depth + 1, defs, defsVisited);
1119
1216
  if (!isFallbackAnySchema(resolvedResult.schema, resolvedType, checker)) {
1120
1217
  return resolvedResult;
1121
1218
  }
@@ -1129,17 +1226,18 @@ function serializeType(
1129
1226
  return { isOptional: false, schema: { type: 'null' } };
1130
1227
  }
1131
1228
  if (type.flags & ts.TypeFlags.Unknown) {
1132
- return { isOptional: false, schema: { type: 'any' } };
1229
+ return { isOptional: false, schema: {} }; // Empty schema = any value (JSON Schema draft 2020-12)
1133
1230
  }
1134
1231
  if (type.flags & ts.TypeFlags.Any) {
1135
- return { isOptional: false, schema: { type: 'any' } };
1232
+ return { isOptional: false, schema: {} }; // Empty schema = any value (JSON Schema draft 2020-12)
1136
1233
  }
1137
1234
 
1138
- // For any other unrecognized type, use 'any' instead of invalid type strings
1235
+ // For any other unrecognized type, use empty schema instead of invalid type strings
1139
1236
  // This prevents errors like "type": "InternalFilterType.StringFilter"
1237
+ // Empty schema {} means "allow any value" in JSON Schema draft 2020-12
1140
1238
  return {
1141
1239
  isOptional: false,
1142
- schema: { type: 'any' }
1240
+ schema: {} // Empty schema = any value (JSON Schema draft 2020-12)
1143
1241
  };
1144
1242
  }
1145
1243
 
@@ -1279,6 +1377,7 @@ function extractFunctionMetadata(
1279
1377
 
1280
1378
  const properties: Record<string, JsonSchemaProperty> = {};
1281
1379
  const required: string[] = [];
1380
+ const defs: Record<string, JsonSchemaProperty> = {}; // Type definitions for recursive types (shared across all parameters)
1282
1381
 
1283
1382
  // Check if async - works for both function declarations and arrow functions
1284
1383
  const isAsync =
@@ -1310,6 +1409,9 @@ function extractFunctionMetadata(
1310
1409
  }
1311
1410
  }
1312
1411
 
1412
+ // Shared defsVisited across all parameters (tracks which definitions we're currently building)
1413
+ const defsVisited = new Set<number>();
1414
+
1313
1415
  for (const param of parameters) {
1314
1416
  const paramName = param.name.getText(sourceFile);
1315
1417
  const hasDefault = param.initializer !== undefined;
@@ -1333,11 +1435,14 @@ function extractFunctionMetadata(
1333
1435
  }
1334
1436
  }
1335
1437
 
1336
- const result = serializeType(type, checker);
1438
+ // Use a fresh visited set for each parameter to avoid false cycle detection
1439
+ // but share defs and defsVisited so recursive types are defined once
1440
+ const visited = new Set<number>();
1441
+ const result = serializeType(type, checker, visited, 0, defs, defsVisited);
1337
1442
  propSchema = result.schema;
1338
1443
  isOptional = result.isOptional;
1339
1444
  } else {
1340
- propSchema = { type: 'any' };
1445
+ propSchema = {}; // Empty schema = any value (JSON Schema draft 2020-12)
1341
1446
  }
1342
1447
 
1343
1448
  // Check if parameter has default value or question mark
@@ -1391,14 +1496,21 @@ function extractFunctionMetadata(
1391
1496
  }
1392
1497
  }
1393
1498
 
1499
+ const parametersSchema: FunctionMetadata['parameters'] = {
1500
+ type: 'object',
1501
+ properties,
1502
+ required: required.length > 0 ? required : undefined
1503
+ };
1504
+
1505
+ // Add $defs if we have any type definitions
1506
+ if (Object.keys(defs).length > 0) {
1507
+ parametersSchema.$defs = defs;
1508
+ }
1509
+
1394
1510
  return {
1395
1511
  name: functionName,
1396
1512
  description,
1397
- parameters: {
1398
- type: 'object',
1399
- properties,
1400
- required: required.length > 0 ? required : undefined
1401
- },
1513
+ parameters: parametersSchema,
1402
1514
  returnType,
1403
1515
  isAsync,
1404
1516
  };