@formspec/build 0.1.0-alpha.16 → 0.1.0-alpha.17

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.
Files changed (35) hide show
  1. package/dist/__tests__/fixtures/example-numeric-extension.d.ts +20 -0
  2. package/dist/__tests__/fixtures/example-numeric-extension.d.ts.map +1 -0
  3. package/dist/__tests__/fixtures/mixed-authoring-shipping-address.d.ts +1 -0
  4. package/dist/__tests__/fixtures/mixed-authoring-shipping-address.d.ts.map +1 -1
  5. package/dist/__tests__/numeric-extension.integration.test.d.ts +2 -0
  6. package/dist/__tests__/numeric-extension.integration.test.d.ts.map +1 -0
  7. package/dist/analyzer/class-analyzer.d.ts +5 -4
  8. package/dist/analyzer/class-analyzer.d.ts.map +1 -1
  9. package/dist/analyzer/jsdoc-constraints.d.ts +3 -2
  10. package/dist/analyzer/jsdoc-constraints.d.ts.map +1 -1
  11. package/dist/analyzer/tsdoc-parser.d.ts +18 -2
  12. package/dist/analyzer/tsdoc-parser.d.ts.map +1 -1
  13. package/dist/browser.cjs +199 -4
  14. package/dist/browser.cjs.map +1 -1
  15. package/dist/browser.js +199 -4
  16. package/dist/browser.js.map +1 -1
  17. package/dist/build.d.ts +28 -2
  18. package/dist/cli.cjs +547 -84
  19. package/dist/cli.cjs.map +1 -1
  20. package/dist/cli.js +547 -84
  21. package/dist/cli.js.map +1 -1
  22. package/dist/extensions/registry.d.ts +25 -1
  23. package/dist/extensions/registry.d.ts.map +1 -1
  24. package/dist/generators/class-schema.d.ts +4 -4
  25. package/dist/generators/class-schema.d.ts.map +1 -1
  26. package/dist/index.cjs +546 -84
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.js +546 -84
  29. package/dist/index.js.map +1 -1
  30. package/dist/internals.cjs +645 -73
  31. package/dist/internals.cjs.map +1 -1
  32. package/dist/internals.js +643 -71
  33. package/dist/internals.js.map +1 -1
  34. package/dist/validate/constraint-validator.d.ts.map +1 -1
  35. package/package.json +3 -3
@@ -542,7 +542,7 @@ var LENGTH_CONSTRAINT_MAP = {
542
542
  maxItems: "maxItems"
543
543
  };
544
544
  var TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["pattern", "enumOptions", "defaultValue"]);
545
- function createFormSpecTSDocConfig() {
545
+ function createFormSpecTSDocConfig(extensionTagNames = []) {
546
546
  const config = new import_tsdoc.TSDocConfiguration();
547
547
  for (const tagName of Object.keys(import_core3.BUILTIN_CONSTRAINT_DEFINITIONS)) {
548
548
  config.addTagDefinition(
@@ -562,14 +562,34 @@ function createFormSpecTSDocConfig() {
562
562
  })
563
563
  );
564
564
  }
565
+ for (const tagName of extensionTagNames) {
566
+ config.addTagDefinition(
567
+ new import_tsdoc.TSDocTagDefinition({
568
+ tagName: "@" + tagName,
569
+ syntaxKind: import_tsdoc.TSDocTagSyntaxKind.BlockTag,
570
+ allowMultiple: true
571
+ })
572
+ );
573
+ }
565
574
  return config;
566
575
  }
567
- var sharedParser;
568
- function getParser() {
569
- sharedParser ??= new import_tsdoc.TSDocParser(createFormSpecTSDocConfig());
570
- return sharedParser;
571
- }
572
- function parseTSDocTags(node, file = "") {
576
+ var parserCache = /* @__PURE__ */ new Map();
577
+ function getParser(options) {
578
+ const extensionTagNames = [
579
+ ...options?.extensionRegistry?.extensions.flatMap(
580
+ (extension) => (extension.constraintTags ?? []).map((tag) => tag.tagName)
581
+ ) ?? []
582
+ ].sort();
583
+ const cacheKey = extensionTagNames.join("|");
584
+ const existing = parserCache.get(cacheKey);
585
+ if (existing) {
586
+ return existing;
587
+ }
588
+ const parser = new import_tsdoc.TSDocParser(createFormSpecTSDocConfig(extensionTagNames));
589
+ parserCache.set(cacheKey, parser);
590
+ return parser;
591
+ }
592
+ function parseTSDocTags(node, file = "", options) {
573
593
  const constraints = [];
574
594
  const annotations = [];
575
595
  let displayName;
@@ -590,7 +610,7 @@ function parseTSDocTags(node, file = "") {
590
610
  if (!commentText.startsWith("/**")) {
591
611
  continue;
592
612
  }
593
- const parser = getParser();
613
+ const parser = getParser(options);
594
614
  const parserContext = parser.parseRange(
595
615
  import_tsdoc.TextRange.fromStringRange(sourceText, range.pos, range.end)
596
616
  );
@@ -629,7 +649,7 @@ function parseTSDocTags(node, file = "") {
629
649
  const expectedType = (0, import_core3.isBuiltinConstraintName)(tagName) ? import_core3.BUILTIN_CONSTRAINT_DEFINITIONS[tagName] : void 0;
630
650
  if (text === "" && expectedType !== "boolean") continue;
631
651
  const provenance = provenanceForComment(range, sourceFile, file, tagName);
632
- const constraintNode = parseConstraintValue(tagName, text, provenance);
652
+ const constraintNode = parseConstraintValue(tagName, text, provenance, options);
633
653
  if (constraintNode) {
634
654
  constraints.push(constraintNode);
635
655
  }
@@ -689,7 +709,7 @@ function parseTSDocTags(node, file = "") {
689
709
  annotations.push(defaultValueNode);
690
710
  continue;
691
711
  }
692
- const constraintNode = parseConstraintValue(tagName, text, provenance);
712
+ const constraintNode = parseConstraintValue(tagName, text, provenance, options);
693
713
  if (constraintNode) {
694
714
  constraints.push(constraintNode);
695
715
  }
@@ -745,7 +765,11 @@ function extractPlainText(node) {
745
765
  }
746
766
  return result;
747
767
  }
748
- function parseConstraintValue(tagName, text, provenance) {
768
+ function parseConstraintValue(tagName, text, provenance, options) {
769
+ const customConstraint = parseExtensionConstraintValue(tagName, text, provenance, options);
770
+ if (customConstraint) {
771
+ return customConstraint;
772
+ }
749
773
  if (!(0, import_core3.isBuiltinConstraintName)(tagName)) {
750
774
  return null;
751
775
  }
@@ -850,6 +874,83 @@ function parseConstraintValue(tagName, text, provenance) {
850
874
  provenance
851
875
  };
852
876
  }
877
+ function parseExtensionConstraintValue(tagName, text, provenance, options) {
878
+ const pathResult = extractPathTarget(text);
879
+ const effectiveText = pathResult ? pathResult.remainingText : text;
880
+ const path2 = pathResult?.path;
881
+ const registry = options?.extensionRegistry;
882
+ if (registry === void 0) {
883
+ return null;
884
+ }
885
+ const directTag = registry.findConstraintTag(tagName);
886
+ if (directTag !== void 0) {
887
+ return makeCustomConstraintNode(
888
+ directTag.extensionId,
889
+ directTag.registration.constraintName,
890
+ directTag.registration.parseValue(effectiveText),
891
+ provenance,
892
+ path2,
893
+ registry
894
+ );
895
+ }
896
+ if (!(0, import_core3.isBuiltinConstraintName)(tagName)) {
897
+ return null;
898
+ }
899
+ const broadenedTypeId = getBroadenedCustomTypeId(options?.fieldType);
900
+ if (broadenedTypeId === void 0) {
901
+ return null;
902
+ }
903
+ const broadened = registry.findBuiltinConstraintBroadening(broadenedTypeId, tagName);
904
+ if (broadened === void 0) {
905
+ return null;
906
+ }
907
+ return makeCustomConstraintNode(
908
+ broadened.extensionId,
909
+ broadened.registration.constraintName,
910
+ broadened.registration.parseValue(effectiveText),
911
+ provenance,
912
+ path2,
913
+ registry
914
+ );
915
+ }
916
+ function getBroadenedCustomTypeId(fieldType) {
917
+ if (fieldType?.kind === "custom") {
918
+ return fieldType.typeId;
919
+ }
920
+ if (fieldType?.kind !== "union") {
921
+ return void 0;
922
+ }
923
+ const customMembers = fieldType.members.filter(
924
+ (member) => member.kind === "custom"
925
+ );
926
+ if (customMembers.length !== 1) {
927
+ return void 0;
928
+ }
929
+ const nonCustomMembers = fieldType.members.filter((member) => member.kind !== "custom");
930
+ const allOtherMembersAreNull = nonCustomMembers.every(
931
+ (member) => member.kind === "primitive" && member.primitiveKind === "null"
932
+ );
933
+ const customMember = customMembers[0];
934
+ return allOtherMembersAreNull && customMember !== void 0 ? customMember.typeId : void 0;
935
+ }
936
+ function makeCustomConstraintNode(extensionId, constraintName, payload, provenance, path2, registry) {
937
+ const constraintId = `${extensionId}/${constraintName}`;
938
+ const registration = registry.findConstraint(constraintId);
939
+ if (registration === void 0) {
940
+ throw new Error(
941
+ `Custom TSDoc tag resolved to unregistered constraint "${constraintId}". Register the constraint before using its tag.`
942
+ );
943
+ }
944
+ return {
945
+ kind: "constraint",
946
+ constraintKind: "custom",
947
+ constraintId,
948
+ payload,
949
+ compositionRule: registration.compositionRule,
950
+ ...path2 && { path: path2 },
951
+ provenance
952
+ };
953
+ }
853
954
  function parseDefaultValueValue(text, provenance) {
854
955
  const trimmed = text.trim();
855
956
  let value;
@@ -910,12 +1011,12 @@ function getTagCommentText(tag) {
910
1011
  }
911
1012
 
912
1013
  // src/analyzer/jsdoc-constraints.ts
913
- function extractJSDocConstraintNodes(node, file = "") {
914
- const result = parseTSDocTags(node, file);
1014
+ function extractJSDocConstraintNodes(node, file = "", options) {
1015
+ const result = parseTSDocTags(node, file, options);
915
1016
  return [...result.constraints];
916
1017
  }
917
- function extractJSDocAnnotationNodes(node, file = "") {
918
- const result = parseTSDocTags(node, file);
1018
+ function extractJSDocAnnotationNodes(node, file = "", options) {
1019
+ const result = parseTSDocTags(node, file, options);
919
1020
  return [...result.annotations];
920
1021
  }
921
1022
  function extractDefaultValueAnnotation(initializer, file = "") {
@@ -964,18 +1065,38 @@ var RESOLVING_TYPE_PLACEHOLDER = {
964
1065
  properties: [],
965
1066
  additionalProperties: true
966
1067
  };
967
- function analyzeClassToIR(classDecl, checker, file = "") {
1068
+ function makeParseOptions(extensionRegistry, fieldType) {
1069
+ if (extensionRegistry === void 0 && fieldType === void 0) {
1070
+ return void 0;
1071
+ }
1072
+ return {
1073
+ ...extensionRegistry !== void 0 && { extensionRegistry },
1074
+ ...fieldType !== void 0 && { fieldType }
1075
+ };
1076
+ }
1077
+ function analyzeClassToIR(classDecl, checker, file = "", extensionRegistry) {
968
1078
  const name = classDecl.name?.text ?? "AnonymousClass";
969
1079
  const fields = [];
970
1080
  const fieldLayouts = [];
971
1081
  const typeRegistry = {};
972
- const annotations = extractJSDocAnnotationNodes(classDecl, file);
1082
+ const annotations = extractJSDocAnnotationNodes(
1083
+ classDecl,
1084
+ file,
1085
+ makeParseOptions(extensionRegistry)
1086
+ );
973
1087
  const visiting = /* @__PURE__ */ new Set();
974
1088
  const instanceMethods = [];
975
1089
  const staticMethods = [];
976
1090
  for (const member of classDecl.members) {
977
1091
  if (ts4.isPropertyDeclaration(member)) {
978
- const fieldNode = analyzeFieldToIR(member, checker, file, typeRegistry, visiting);
1092
+ const fieldNode = analyzeFieldToIR(
1093
+ member,
1094
+ checker,
1095
+ file,
1096
+ typeRegistry,
1097
+ visiting,
1098
+ extensionRegistry
1099
+ );
979
1100
  if (fieldNode) {
980
1101
  fields.push(fieldNode);
981
1102
  fieldLayouts.push({});
@@ -1002,15 +1123,26 @@ function analyzeClassToIR(classDecl, checker, file = "") {
1002
1123
  staticMethods
1003
1124
  };
1004
1125
  }
1005
- function analyzeInterfaceToIR(interfaceDecl, checker, file = "") {
1126
+ function analyzeInterfaceToIR(interfaceDecl, checker, file = "", extensionRegistry) {
1006
1127
  const name = interfaceDecl.name.text;
1007
1128
  const fields = [];
1008
1129
  const typeRegistry = {};
1009
- const annotations = extractJSDocAnnotationNodes(interfaceDecl, file);
1130
+ const annotations = extractJSDocAnnotationNodes(
1131
+ interfaceDecl,
1132
+ file,
1133
+ makeParseOptions(extensionRegistry)
1134
+ );
1010
1135
  const visiting = /* @__PURE__ */ new Set();
1011
1136
  for (const member of interfaceDecl.members) {
1012
1137
  if (ts4.isPropertySignature(member)) {
1013
- const fieldNode = analyzeInterfacePropertyToIR(member, checker, file, typeRegistry, visiting);
1138
+ const fieldNode = analyzeInterfacePropertyToIR(
1139
+ member,
1140
+ checker,
1141
+ file,
1142
+ typeRegistry,
1143
+ visiting,
1144
+ extensionRegistry
1145
+ );
1014
1146
  if (fieldNode) {
1015
1147
  fields.push(fieldNode);
1016
1148
  }
@@ -1027,7 +1159,7 @@ function analyzeInterfaceToIR(interfaceDecl, checker, file = "") {
1027
1159
  staticMethods: []
1028
1160
  };
1029
1161
  }
1030
- function analyzeTypeAliasToIR(typeAlias, checker, file = "") {
1162
+ function analyzeTypeAliasToIR(typeAlias, checker, file = "", extensionRegistry) {
1031
1163
  if (!ts4.isTypeLiteralNode(typeAlias.type)) {
1032
1164
  const sourceFile = typeAlias.getSourceFile();
1033
1165
  const { line } = sourceFile.getLineAndCharacterOfPosition(typeAlias.getStart());
@@ -1040,11 +1172,22 @@ function analyzeTypeAliasToIR(typeAlias, checker, file = "") {
1040
1172
  const name = typeAlias.name.text;
1041
1173
  const fields = [];
1042
1174
  const typeRegistry = {};
1043
- const annotations = extractJSDocAnnotationNodes(typeAlias, file);
1175
+ const annotations = extractJSDocAnnotationNodes(
1176
+ typeAlias,
1177
+ file,
1178
+ makeParseOptions(extensionRegistry)
1179
+ );
1044
1180
  const visiting = /* @__PURE__ */ new Set();
1045
1181
  for (const member of typeAlias.type.members) {
1046
1182
  if (ts4.isPropertySignature(member)) {
1047
- const fieldNode = analyzeInterfacePropertyToIR(member, checker, file, typeRegistry, visiting);
1183
+ const fieldNode = analyzeInterfacePropertyToIR(
1184
+ member,
1185
+ checker,
1186
+ file,
1187
+ typeRegistry,
1188
+ visiting,
1189
+ extensionRegistry
1190
+ );
1048
1191
  if (fieldNode) {
1049
1192
  fields.push(fieldNode);
1050
1193
  }
@@ -1063,7 +1206,7 @@ function analyzeTypeAliasToIR(typeAlias, checker, file = "") {
1063
1206
  }
1064
1207
  };
1065
1208
  }
1066
- function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting) {
1209
+ function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting, extensionRegistry) {
1067
1210
  if (!ts4.isIdentifier(prop.name)) {
1068
1211
  return null;
1069
1212
  }
@@ -1071,14 +1214,26 @@ function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting) {
1071
1214
  const tsType = checker.getTypeAtLocation(prop);
1072
1215
  const optional = prop.questionToken !== void 0;
1073
1216
  const provenance = provenanceForNode(prop, file);
1074
- let type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting, prop);
1217
+ let type = resolveTypeNode(
1218
+ tsType,
1219
+ checker,
1220
+ file,
1221
+ typeRegistry,
1222
+ visiting,
1223
+ prop,
1224
+ extensionRegistry
1225
+ );
1075
1226
  const constraints = [];
1076
1227
  if (prop.type) {
1077
- constraints.push(...extractTypeAliasConstraintNodes(prop.type, checker, file));
1228
+ constraints.push(
1229
+ ...extractTypeAliasConstraintNodes(prop.type, checker, file, extensionRegistry)
1230
+ );
1078
1231
  }
1079
- constraints.push(...extractJSDocConstraintNodes(prop, file));
1232
+ constraints.push(...extractJSDocConstraintNodes(prop, file, makeParseOptions(extensionRegistry, type)));
1080
1233
  let annotations = [];
1081
- annotations.push(...extractJSDocAnnotationNodes(prop, file));
1234
+ annotations.push(
1235
+ ...extractJSDocAnnotationNodes(prop, file, makeParseOptions(extensionRegistry, type))
1236
+ );
1082
1237
  const defaultAnnotation = extractDefaultValueAnnotation(prop.initializer, file);
1083
1238
  if (defaultAnnotation && !annotations.some((a) => a.annotationKind === "defaultValue")) {
1084
1239
  annotations.push(defaultAnnotation);
@@ -1094,7 +1249,7 @@ function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting) {
1094
1249
  provenance
1095
1250
  };
1096
1251
  }
1097
- function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visiting) {
1252
+ function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visiting, extensionRegistry) {
1098
1253
  if (!ts4.isIdentifier(prop.name)) {
1099
1254
  return null;
1100
1255
  }
@@ -1102,14 +1257,26 @@ function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visitin
1102
1257
  const tsType = checker.getTypeAtLocation(prop);
1103
1258
  const optional = prop.questionToken !== void 0;
1104
1259
  const provenance = provenanceForNode(prop, file);
1105
- let type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting, prop);
1260
+ let type = resolveTypeNode(
1261
+ tsType,
1262
+ checker,
1263
+ file,
1264
+ typeRegistry,
1265
+ visiting,
1266
+ prop,
1267
+ extensionRegistry
1268
+ );
1106
1269
  const constraints = [];
1107
1270
  if (prop.type) {
1108
- constraints.push(...extractTypeAliasConstraintNodes(prop.type, checker, file));
1271
+ constraints.push(
1272
+ ...extractTypeAliasConstraintNodes(prop.type, checker, file, extensionRegistry)
1273
+ );
1109
1274
  }
1110
- constraints.push(...extractJSDocConstraintNodes(prop, file));
1275
+ constraints.push(...extractJSDocConstraintNodes(prop, file, makeParseOptions(extensionRegistry, type)));
1111
1276
  let annotations = [];
1112
- annotations.push(...extractJSDocAnnotationNodes(prop, file));
1277
+ annotations.push(
1278
+ ...extractJSDocAnnotationNodes(prop, file, makeParseOptions(extensionRegistry, type))
1279
+ );
1113
1280
  ({ type, annotations } = applyEnumMemberDisplayNames(type, annotations));
1114
1281
  return {
1115
1282
  kind: "field",
@@ -1183,7 +1350,66 @@ function parseEnumMemberDisplayName(value) {
1183
1350
  if (label === "") return null;
1184
1351
  return { value: match[1], label };
1185
1352
  }
1186
- function resolveTypeNode(type, checker, file, typeRegistry, visiting, sourceNode) {
1353
+ function resolveRegisteredCustomType(sourceNode, extensionRegistry, checker) {
1354
+ if (sourceNode === void 0 || extensionRegistry === void 0) {
1355
+ return null;
1356
+ }
1357
+ const typeNode = extractTypeNodeFromSource(sourceNode);
1358
+ if (typeNode === void 0) {
1359
+ return null;
1360
+ }
1361
+ return resolveRegisteredCustomTypeFromTypeNode(typeNode, extensionRegistry, checker);
1362
+ }
1363
+ function resolveRegisteredCustomTypeFromTypeNode(typeNode, extensionRegistry, checker) {
1364
+ if (ts4.isParenthesizedTypeNode(typeNode)) {
1365
+ return resolveRegisteredCustomTypeFromTypeNode(typeNode.type, extensionRegistry, checker);
1366
+ }
1367
+ const typeName = getTypeNodeRegistrationName(typeNode);
1368
+ if (typeName === null) {
1369
+ return null;
1370
+ }
1371
+ const registration = extensionRegistry.findTypeByName(typeName);
1372
+ if (registration !== void 0) {
1373
+ return {
1374
+ kind: "custom",
1375
+ typeId: `${registration.extensionId}/${registration.registration.typeName}`,
1376
+ payload: null
1377
+ };
1378
+ }
1379
+ if (ts4.isTypeReferenceNode(typeNode) && ts4.isIdentifier(typeNode.typeName)) {
1380
+ const aliasDecl = checker.getSymbolAtLocation(typeNode.typeName)?.declarations?.find(ts4.isTypeAliasDeclaration);
1381
+ if (aliasDecl !== void 0) {
1382
+ return resolveRegisteredCustomTypeFromTypeNode(aliasDecl.type, extensionRegistry, checker);
1383
+ }
1384
+ }
1385
+ return null;
1386
+ }
1387
+ function extractTypeNodeFromSource(sourceNode) {
1388
+ if (ts4.isPropertyDeclaration(sourceNode) || ts4.isPropertySignature(sourceNode) || ts4.isParameter(sourceNode) || ts4.isTypeAliasDeclaration(sourceNode)) {
1389
+ return sourceNode.type;
1390
+ }
1391
+ if (ts4.isTypeNode(sourceNode)) {
1392
+ return sourceNode;
1393
+ }
1394
+ return void 0;
1395
+ }
1396
+ function getTypeNodeRegistrationName(typeNode) {
1397
+ if (ts4.isTypeReferenceNode(typeNode)) {
1398
+ return ts4.isIdentifier(typeNode.typeName) ? typeNode.typeName.text : typeNode.typeName.right.text;
1399
+ }
1400
+ if (ts4.isParenthesizedTypeNode(typeNode)) {
1401
+ return getTypeNodeRegistrationName(typeNode.type);
1402
+ }
1403
+ if (typeNode.kind === ts4.SyntaxKind.BigIntKeyword || typeNode.kind === ts4.SyntaxKind.StringKeyword || typeNode.kind === ts4.SyntaxKind.NumberKeyword || typeNode.kind === ts4.SyntaxKind.BooleanKeyword) {
1404
+ return typeNode.getText();
1405
+ }
1406
+ return null;
1407
+ }
1408
+ function resolveTypeNode(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry) {
1409
+ const customType = resolveRegisteredCustomType(sourceNode, extensionRegistry, checker);
1410
+ if (customType) {
1411
+ return customType;
1412
+ }
1187
1413
  if (type.flags & ts4.TypeFlags.String) {
1188
1414
  return { kind: "primitive", primitiveKind: "string" };
1189
1415
  }
@@ -1212,26 +1438,50 @@ function resolveTypeNode(type, checker, file, typeRegistry, visiting, sourceNode
1212
1438
  };
1213
1439
  }
1214
1440
  if (type.isUnion()) {
1215
- return resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNode);
1441
+ return resolveUnionType(
1442
+ type,
1443
+ checker,
1444
+ file,
1445
+ typeRegistry,
1446
+ visiting,
1447
+ sourceNode,
1448
+ extensionRegistry
1449
+ );
1216
1450
  }
1217
1451
  if (checker.isArrayType(type)) {
1218
- return resolveArrayType(type, checker, file, typeRegistry, visiting);
1452
+ return resolveArrayType(
1453
+ type,
1454
+ checker,
1455
+ file,
1456
+ typeRegistry,
1457
+ visiting,
1458
+ sourceNode,
1459
+ extensionRegistry
1460
+ );
1219
1461
  }
1220
1462
  if (isObjectType(type)) {
1221
- return resolveObjectType(type, checker, file, typeRegistry, visiting);
1463
+ return resolveObjectType(type, checker, file, typeRegistry, visiting, extensionRegistry);
1222
1464
  }
1223
1465
  return { kind: "primitive", primitiveKind: "string" };
1224
1466
  }
1225
- function resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNode) {
1467
+ function resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry) {
1226
1468
  const typeName = getNamedTypeName(type);
1227
1469
  const namedDecl = getNamedTypeDeclaration(type);
1228
1470
  if (typeName && typeName in typeRegistry) {
1229
1471
  return { kind: "reference", name: typeName, typeArguments: [] };
1230
1472
  }
1231
1473
  const allTypes = type.types;
1474
+ const unionMemberTypeNodes = extractUnionMemberTypeNodes(sourceNode, checker);
1475
+ const nonNullSourceNodes = unionMemberTypeNodes.filter(
1476
+ (memberTypeNode) => !isNullishTypeNode(resolveAliasedTypeNode(memberTypeNode, checker))
1477
+ );
1232
1478
  const nonNullTypes = allTypes.filter(
1233
- (t) => !(t.flags & (ts4.TypeFlags.Null | ts4.TypeFlags.Undefined))
1479
+ (memberType) => !(memberType.flags & (ts4.TypeFlags.Null | ts4.TypeFlags.Undefined))
1234
1480
  );
1481
+ const nonNullMembers = nonNullTypes.map((memberType, index) => ({
1482
+ memberType,
1483
+ sourceNode: nonNullSourceNodes.length === nonNullTypes.length ? nonNullSourceNodes[index] : void 0
1484
+ }));
1235
1485
  const hasNull = allTypes.some((t) => t.flags & ts4.TypeFlags.Null);
1236
1486
  const memberDisplayNames = /* @__PURE__ */ new Map();
1237
1487
  if (namedDecl) {
@@ -1248,7 +1498,7 @@ function resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNod
1248
1498
  if (!typeName) {
1249
1499
  return result;
1250
1500
  }
1251
- const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file) : void 0;
1501
+ const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file, makeParseOptions(extensionRegistry)) : void 0;
1252
1502
  typeRegistry[typeName] = {
1253
1503
  name: typeName,
1254
1504
  type: result,
@@ -1296,14 +1546,15 @@ function resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNod
1296
1546
  } : enumNode;
1297
1547
  return registerNamed(result);
1298
1548
  }
1299
- if (nonNullTypes.length === 1 && nonNullTypes[0]) {
1549
+ if (nonNullMembers.length === 1 && nonNullMembers[0]) {
1300
1550
  const inner = resolveTypeNode(
1301
- nonNullTypes[0],
1551
+ nonNullMembers[0].memberType,
1302
1552
  checker,
1303
1553
  file,
1304
1554
  typeRegistry,
1305
1555
  visiting,
1306
- sourceNode
1556
+ nonNullMembers[0].sourceNode ?? sourceNode,
1557
+ extensionRegistry
1307
1558
  );
1308
1559
  const result = hasNull ? {
1309
1560
  kind: "union",
@@ -1311,21 +1562,38 @@ function resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNod
1311
1562
  } : inner;
1312
1563
  return registerNamed(result);
1313
1564
  }
1314
- const members = nonNullTypes.map(
1315
- (t) => resolveTypeNode(t, checker, file, typeRegistry, visiting, sourceNode)
1565
+ const members = nonNullMembers.map(
1566
+ ({ memberType, sourceNode: memberSourceNode }) => resolveTypeNode(
1567
+ memberType,
1568
+ checker,
1569
+ file,
1570
+ typeRegistry,
1571
+ visiting,
1572
+ memberSourceNode ?? sourceNode,
1573
+ extensionRegistry
1574
+ )
1316
1575
  );
1317
1576
  if (hasNull) {
1318
1577
  members.push({ kind: "primitive", primitiveKind: "null" });
1319
1578
  }
1320
1579
  return registerNamed({ kind: "union", members });
1321
1580
  }
1322
- function resolveArrayType(type, checker, file, typeRegistry, visiting) {
1581
+ function resolveArrayType(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry) {
1323
1582
  const typeArgs = isTypeReference(type) ? type.typeArguments : void 0;
1324
1583
  const elementType = typeArgs?.[0];
1325
- const items = elementType ? resolveTypeNode(elementType, checker, file, typeRegistry, visiting) : { kind: "primitive", primitiveKind: "string" };
1584
+ const elementSourceNode = extractArrayElementTypeNode(sourceNode, checker);
1585
+ const items = elementType ? resolveTypeNode(
1586
+ elementType,
1587
+ checker,
1588
+ file,
1589
+ typeRegistry,
1590
+ visiting,
1591
+ elementSourceNode,
1592
+ extensionRegistry
1593
+ ) : { kind: "primitive", primitiveKind: "string" };
1326
1594
  return { kind: "array", items };
1327
1595
  }
1328
- function tryResolveRecordType(type, checker, file, typeRegistry, visiting) {
1596
+ function tryResolveRecordType(type, checker, file, typeRegistry, visiting, extensionRegistry) {
1329
1597
  if (type.getProperties().length > 0) {
1330
1598
  return null;
1331
1599
  }
@@ -1333,7 +1601,15 @@ function tryResolveRecordType(type, checker, file, typeRegistry, visiting) {
1333
1601
  if (!indexInfo) {
1334
1602
  return null;
1335
1603
  }
1336
- const valueType = resolveTypeNode(indexInfo.type, checker, file, typeRegistry, visiting);
1604
+ const valueType = resolveTypeNode(
1605
+ indexInfo.type,
1606
+ checker,
1607
+ file,
1608
+ typeRegistry,
1609
+ visiting,
1610
+ void 0,
1611
+ extensionRegistry
1612
+ );
1337
1613
  return { kind: "record", valueType };
1338
1614
  }
1339
1615
  function typeNodeContainsReference(type, targetName) {
@@ -1361,7 +1637,7 @@ function typeNodeContainsReference(type, targetName) {
1361
1637
  }
1362
1638
  }
1363
1639
  }
1364
- function resolveObjectType(type, checker, file, typeRegistry, visiting) {
1640
+ function resolveObjectType(type, checker, file, typeRegistry, visiting, extensionRegistry) {
1365
1641
  const typeName = getNamedTypeName(type);
1366
1642
  const namedTypeName = typeName ?? void 0;
1367
1643
  const namedDecl = getNamedTypeDeclaration(type);
@@ -1392,7 +1668,14 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting) {
1392
1668
  return { kind: "reference", name: namedTypeName, typeArguments: [] };
1393
1669
  }
1394
1670
  }
1395
- const recordNode = tryResolveRecordType(type, checker, file, typeRegistry, visiting);
1671
+ const recordNode = tryResolveRecordType(
1672
+ type,
1673
+ checker,
1674
+ file,
1675
+ typeRegistry,
1676
+ visiting,
1677
+ extensionRegistry
1678
+ );
1396
1679
  if (recordNode) {
1397
1680
  visiting.delete(type);
1398
1681
  if (namedTypeName !== void 0 && shouldRegisterNamedType) {
@@ -1401,7 +1684,7 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting) {
1401
1684
  clearNamedTypeRegistration();
1402
1685
  return recordNode;
1403
1686
  }
1404
- const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file) : void 0;
1687
+ const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file, makeParseOptions(extensionRegistry)) : void 0;
1405
1688
  typeRegistry[namedTypeName] = {
1406
1689
  name: namedTypeName,
1407
1690
  type: recordNode,
@@ -1413,7 +1696,14 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting) {
1413
1696
  return recordNode;
1414
1697
  }
1415
1698
  const properties = [];
1416
- const fieldInfoMap = getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visiting);
1699
+ const fieldInfoMap = getNamedTypeFieldNodeInfoMap(
1700
+ type,
1701
+ checker,
1702
+ file,
1703
+ typeRegistry,
1704
+ visiting,
1705
+ extensionRegistry
1706
+ );
1417
1707
  for (const prop of type.getProperties()) {
1418
1708
  const declaration = prop.valueDeclaration ?? prop.declarations?.[0];
1419
1709
  if (!declaration) continue;
@@ -1425,7 +1715,8 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting) {
1425
1715
  file,
1426
1716
  typeRegistry,
1427
1717
  visiting,
1428
- declaration
1718
+ declaration,
1719
+ extensionRegistry
1429
1720
  );
1430
1721
  const fieldNodeInfo = fieldInfoMap?.get(prop.name);
1431
1722
  properties.push({
@@ -1444,7 +1735,7 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting) {
1444
1735
  additionalProperties: true
1445
1736
  };
1446
1737
  if (namedTypeName !== void 0 && shouldRegisterNamedType) {
1447
- const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file) : void 0;
1738
+ const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file, makeParseOptions(extensionRegistry)) : void 0;
1448
1739
  typeRegistry[namedTypeName] = {
1449
1740
  name: namedTypeName,
1450
1741
  type: objectNode,
@@ -1455,7 +1746,7 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting) {
1455
1746
  }
1456
1747
  return objectNode;
1457
1748
  }
1458
- function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visiting) {
1749
+ function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visiting, extensionRegistry) {
1459
1750
  const symbols = [type.getSymbol(), type.aliasSymbol].filter(
1460
1751
  (s) => s?.declarations != null && s.declarations.length > 0
1461
1752
  );
@@ -1467,7 +1758,14 @@ function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visitin
1467
1758
  const map = /* @__PURE__ */ new Map();
1468
1759
  for (const member of classDecl.members) {
1469
1760
  if (ts4.isPropertyDeclaration(member) && ts4.isIdentifier(member.name)) {
1470
- const fieldNode = analyzeFieldToIR(member, checker, file, typeRegistry, visiting);
1761
+ const fieldNode = analyzeFieldToIR(
1762
+ member,
1763
+ checker,
1764
+ file,
1765
+ typeRegistry,
1766
+ visiting,
1767
+ extensionRegistry
1768
+ );
1471
1769
  if (fieldNode) {
1472
1770
  map.set(fieldNode.name, {
1473
1771
  constraints: [...fieldNode.constraints],
@@ -1481,7 +1779,14 @@ function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visitin
1481
1779
  }
1482
1780
  const interfaceDecl = declarations.find(ts4.isInterfaceDeclaration);
1483
1781
  if (interfaceDecl) {
1484
- return buildFieldNodeInfoMap(interfaceDecl.members, checker, file, typeRegistry, visiting);
1782
+ return buildFieldNodeInfoMap(
1783
+ interfaceDecl.members,
1784
+ checker,
1785
+ file,
1786
+ typeRegistry,
1787
+ visiting,
1788
+ extensionRegistry
1789
+ );
1485
1790
  }
1486
1791
  const typeAliasDecl = declarations.find(ts4.isTypeAliasDeclaration);
1487
1792
  if (typeAliasDecl && ts4.isTypeLiteralNode(typeAliasDecl.type)) {
@@ -1490,17 +1795,68 @@ function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visitin
1490
1795
  checker,
1491
1796
  file,
1492
1797
  typeRegistry,
1493
- visiting
1798
+ visiting,
1799
+ extensionRegistry
1494
1800
  );
1495
1801
  }
1496
1802
  }
1497
1803
  return null;
1498
1804
  }
1499
- function buildFieldNodeInfoMap(members, checker, file, typeRegistry, visiting) {
1805
+ function extractArrayElementTypeNode(sourceNode, checker) {
1806
+ const typeNode = sourceNode === void 0 ? void 0 : extractTypeNodeFromSource(sourceNode);
1807
+ if (typeNode === void 0) {
1808
+ return void 0;
1809
+ }
1810
+ const resolvedTypeNode = resolveAliasedTypeNode(typeNode, checker);
1811
+ if (ts4.isArrayTypeNode(resolvedTypeNode)) {
1812
+ return resolvedTypeNode.elementType;
1813
+ }
1814
+ if (ts4.isTypeReferenceNode(resolvedTypeNode) && ts4.isIdentifier(resolvedTypeNode.typeName) && resolvedTypeNode.typeName.text === "Array" && resolvedTypeNode.typeArguments?.[0]) {
1815
+ return resolvedTypeNode.typeArguments[0];
1816
+ }
1817
+ return void 0;
1818
+ }
1819
+ function extractUnionMemberTypeNodes(sourceNode, checker) {
1820
+ const typeNode = sourceNode === void 0 ? void 0 : extractTypeNodeFromSource(sourceNode);
1821
+ if (!typeNode) {
1822
+ return [];
1823
+ }
1824
+ const resolvedTypeNode = resolveAliasedTypeNode(typeNode, checker);
1825
+ return ts4.isUnionTypeNode(resolvedTypeNode) ? [...resolvedTypeNode.types] : [];
1826
+ }
1827
+ function resolveAliasedTypeNode(typeNode, checker, visited = /* @__PURE__ */ new Set()) {
1828
+ if (ts4.isParenthesizedTypeNode(typeNode)) {
1829
+ return resolveAliasedTypeNode(typeNode.type, checker, visited);
1830
+ }
1831
+ if (!ts4.isTypeReferenceNode(typeNode) || !ts4.isIdentifier(typeNode.typeName)) {
1832
+ return typeNode;
1833
+ }
1834
+ const symbol = checker.getSymbolAtLocation(typeNode.typeName);
1835
+ const aliasDecl = symbol?.declarations?.find(ts4.isTypeAliasDeclaration);
1836
+ if (aliasDecl === void 0 || visited.has(aliasDecl)) {
1837
+ return typeNode;
1838
+ }
1839
+ visited.add(aliasDecl);
1840
+ return resolveAliasedTypeNode(aliasDecl.type, checker, visited);
1841
+ }
1842
+ function isNullishTypeNode(typeNode) {
1843
+ if (typeNode.kind === ts4.SyntaxKind.NullKeyword || typeNode.kind === ts4.SyntaxKind.UndefinedKeyword) {
1844
+ return true;
1845
+ }
1846
+ return ts4.isLiteralTypeNode(typeNode) && (typeNode.literal.kind === ts4.SyntaxKind.NullKeyword || typeNode.literal.kind === ts4.SyntaxKind.UndefinedKeyword);
1847
+ }
1848
+ function buildFieldNodeInfoMap(members, checker, file, typeRegistry, visiting, extensionRegistry) {
1500
1849
  const map = /* @__PURE__ */ new Map();
1501
1850
  for (const member of members) {
1502
1851
  if (ts4.isPropertySignature(member)) {
1503
- const fieldNode = analyzeInterfacePropertyToIR(member, checker, file, typeRegistry, visiting);
1852
+ const fieldNode = analyzeInterfacePropertyToIR(
1853
+ member,
1854
+ checker,
1855
+ file,
1856
+ typeRegistry,
1857
+ visiting,
1858
+ extensionRegistry
1859
+ );
1504
1860
  if (fieldNode) {
1505
1861
  map.set(fieldNode.name, {
1506
1862
  constraints: [...fieldNode.constraints],
@@ -1513,7 +1869,7 @@ function buildFieldNodeInfoMap(members, checker, file, typeRegistry, visiting) {
1513
1869
  return map;
1514
1870
  }
1515
1871
  var MAX_ALIAS_CHAIN_DEPTH = 8;
1516
- function extractTypeAliasConstraintNodes(typeNode, checker, file, depth = 0) {
1872
+ function extractTypeAliasConstraintNodes(typeNode, checker, file, extensionRegistry, depth = 0) {
1517
1873
  if (!ts4.isTypeReferenceNode(typeNode)) return [];
1518
1874
  if (depth >= MAX_ALIAS_CHAIN_DEPTH) {
1519
1875
  const aliasName = typeNode.typeName.getText();
@@ -1526,8 +1882,29 @@ function extractTypeAliasConstraintNodes(typeNode, checker, file, depth = 0) {
1526
1882
  const aliasDecl = symbol.declarations.find(ts4.isTypeAliasDeclaration);
1527
1883
  if (!aliasDecl) return [];
1528
1884
  if (ts4.isTypeLiteralNode(aliasDecl.type)) return [];
1529
- const constraints = extractJSDocConstraintNodes(aliasDecl, file);
1530
- constraints.push(...extractTypeAliasConstraintNodes(aliasDecl.type, checker, file, depth + 1));
1885
+ const aliasFieldType = resolveTypeNode(
1886
+ checker.getTypeAtLocation(aliasDecl.type),
1887
+ checker,
1888
+ file,
1889
+ {},
1890
+ /* @__PURE__ */ new Set(),
1891
+ aliasDecl.type,
1892
+ extensionRegistry
1893
+ );
1894
+ const constraints = extractJSDocConstraintNodes(
1895
+ aliasDecl,
1896
+ file,
1897
+ makeParseOptions(extensionRegistry, aliasFieldType)
1898
+ );
1899
+ constraints.push(
1900
+ ...extractTypeAliasConstraintNodes(
1901
+ aliasDecl.type,
1902
+ checker,
1903
+ file,
1904
+ extensionRegistry,
1905
+ depth + 1
1906
+ )
1907
+ );
1531
1908
  return constraints;
1532
1909
  }
1533
1910
  function provenanceForNode(node, file) {
@@ -2014,7 +2391,12 @@ function applyCustomConstraint(schema, constraint, ctx) {
2014
2391
  `Cannot generate JSON Schema for custom constraint "${constraint.constraintId}" without a matching extension registration`
2015
2392
  );
2016
2393
  }
2017
- Object.assign(schema, registration.toJsonSchema(constraint.payload, ctx.vendorPrefix));
2394
+ assignVendorPrefixedExtensionKeywords(
2395
+ schema,
2396
+ registration.toJsonSchema(constraint.payload, ctx.vendorPrefix),
2397
+ ctx.vendorPrefix,
2398
+ `custom constraint "${constraint.constraintId}"`
2399
+ );
2018
2400
  }
2019
2401
  function applyCustomAnnotation(schema, annotation, ctx) {
2020
2402
  const registration = ctx.extensionRegistry?.findAnnotation(annotation.annotationId);
@@ -2026,7 +2408,22 @@ function applyCustomAnnotation(schema, annotation, ctx) {
2026
2408
  if (registration.toJsonSchema === void 0) {
2027
2409
  return;
2028
2410
  }
2029
- Object.assign(schema, registration.toJsonSchema(annotation.value, ctx.vendorPrefix));
2411
+ assignVendorPrefixedExtensionKeywords(
2412
+ schema,
2413
+ registration.toJsonSchema(annotation.value, ctx.vendorPrefix),
2414
+ ctx.vendorPrefix,
2415
+ `custom annotation "${annotation.annotationId}"`
2416
+ );
2417
+ }
2418
+ function assignVendorPrefixedExtensionKeywords(schema, extensionSchema, vendorPrefix, source) {
2419
+ for (const [key, value] of Object.entries(extensionSchema)) {
2420
+ if (!key.startsWith(`${vendorPrefix}-`)) {
2421
+ throw new Error(
2422
+ `Cannot apply ${source}: extension hooks may only emit "${vendorPrefix}-*" JSON Schema keywords`
2423
+ );
2424
+ }
2425
+ schema[key] = value;
2426
+ }
2030
2427
  }
2031
2428
 
2032
2429
  // src/ui-schema/schema.ts
@@ -2253,15 +2650,16 @@ function generateUiSchemaFromIR(ir) {
2253
2650
  }
2254
2651
 
2255
2652
  // src/generators/class-schema.ts
2256
- function generateClassSchemas(analysis, source) {
2653
+ function generateClassSchemas(analysis, source, options) {
2257
2654
  const ir = canonicalizeTSDoc(analysis, source);
2258
2655
  return {
2259
- jsonSchema: generateJsonSchemaFromIR(ir),
2656
+ jsonSchema: generateJsonSchemaFromIR(ir, options),
2260
2657
  uiSchema: generateUiSchemaFromIR(ir)
2261
2658
  };
2262
2659
  }
2263
2660
 
2264
2661
  // src/validate/constraint-validator.ts
2662
+ var import_core4 = require("@formspec/core");
2265
2663
  function addContradiction(ctx, message, primary, related) {
2266
2664
  ctx.diagnostics.push({
2267
2665
  code: "CONTRADICTING_CONSTRAINTS",
@@ -2307,6 +2705,13 @@ function addConstraintBroadening(ctx, message, primary, related) {
2307
2705
  relatedLocations: [related]
2308
2706
  });
2309
2707
  }
2708
+ function getExtensionIdFromConstraintId(constraintId) {
2709
+ const separator = constraintId.lastIndexOf("/");
2710
+ if (separator <= 0) {
2711
+ return null;
2712
+ }
2713
+ return constraintId.slice(0, separator);
2714
+ }
2310
2715
  function findNumeric(constraints, constraintKind) {
2311
2716
  return constraints.find((c) => c.constraintKind === constraintKind);
2312
2717
  }
@@ -2477,6 +2882,112 @@ function checkConstraintBroadening(ctx, fieldName, constraints) {
2477
2882
  strongestByKey.set(key, constraint);
2478
2883
  }
2479
2884
  }
2885
+ function compareCustomConstraintStrength(current, previous) {
2886
+ const order = current.comparePayloads(current.constraint.payload, previous.constraint.payload);
2887
+ const equalPayloadTiebreaker = order === 0 ? compareSemanticInclusivity(current.role.inclusive, previous.role.inclusive) : order;
2888
+ switch (current.role.bound) {
2889
+ case "lower":
2890
+ return equalPayloadTiebreaker;
2891
+ case "upper":
2892
+ return equalPayloadTiebreaker === 0 ? 0 : -equalPayloadTiebreaker;
2893
+ case "exact":
2894
+ return order === 0 ? 0 : Number.NaN;
2895
+ default: {
2896
+ const _exhaustive = current.role.bound;
2897
+ return _exhaustive;
2898
+ }
2899
+ }
2900
+ }
2901
+ function compareSemanticInclusivity(currentInclusive, previousInclusive) {
2902
+ if (currentInclusive === previousInclusive) {
2903
+ return 0;
2904
+ }
2905
+ return currentInclusive ? -1 : 1;
2906
+ }
2907
+ function customConstraintsContradict(lower, upper) {
2908
+ const order = lower.comparePayloads(lower.constraint.payload, upper.constraint.payload);
2909
+ if (order > 0) {
2910
+ return true;
2911
+ }
2912
+ if (order < 0) {
2913
+ return false;
2914
+ }
2915
+ return !lower.role.inclusive || !upper.role.inclusive;
2916
+ }
2917
+ function describeCustomConstraintTag(constraint) {
2918
+ return constraint.provenance.tagName ?? constraint.constraintId;
2919
+ }
2920
+ function checkCustomConstraintSemantics(ctx, fieldName, constraints) {
2921
+ if (ctx.extensionRegistry === void 0) {
2922
+ return;
2923
+ }
2924
+ const strongestByKey = /* @__PURE__ */ new Map();
2925
+ const lowerByFamily = /* @__PURE__ */ new Map();
2926
+ const upperByFamily = /* @__PURE__ */ new Map();
2927
+ for (const constraint of constraints) {
2928
+ if (constraint.constraintKind !== "custom") {
2929
+ continue;
2930
+ }
2931
+ const registration = ctx.extensionRegistry.findConstraint(constraint.constraintId);
2932
+ if (registration?.comparePayloads === void 0 || registration.semanticRole === void 0) {
2933
+ continue;
2934
+ }
2935
+ const entry = {
2936
+ constraint,
2937
+ comparePayloads: registration.comparePayloads,
2938
+ role: registration.semanticRole
2939
+ };
2940
+ const familyKey = `${registration.semanticRole.family}:${pathKey(constraint)}`;
2941
+ const boundKey = `${familyKey}:${registration.semanticRole.bound}`;
2942
+ const previous = strongestByKey.get(boundKey);
2943
+ if (previous !== void 0) {
2944
+ const strength = compareCustomConstraintStrength(entry, previous);
2945
+ if (Number.isNaN(strength)) {
2946
+ addContradiction(
2947
+ ctx,
2948
+ `Field "${formatPathTargetFieldName(fieldName, constraint.path?.segments ?? [])}": ${describeCustomConstraintTag(constraint)} conflicts with ${describeCustomConstraintTag(previous.constraint)}`,
2949
+ constraint.provenance,
2950
+ previous.constraint.provenance
2951
+ );
2952
+ continue;
2953
+ }
2954
+ if (strength < 0) {
2955
+ addConstraintBroadening(
2956
+ ctx,
2957
+ `Field "${formatPathTargetFieldName(fieldName, constraint.path?.segments ?? [])}": ${describeCustomConstraintTag(constraint)} is broader than earlier ${describeCustomConstraintTag(previous.constraint)}. Constraints can only narrow.`,
2958
+ constraint.provenance,
2959
+ previous.constraint.provenance
2960
+ );
2961
+ continue;
2962
+ }
2963
+ if (strength > 0) {
2964
+ strongestByKey.set(boundKey, entry);
2965
+ }
2966
+ } else {
2967
+ strongestByKey.set(boundKey, entry);
2968
+ }
2969
+ if (registration.semanticRole.bound === "lower") {
2970
+ lowerByFamily.set(familyKey, strongestByKey.get(boundKey) ?? entry);
2971
+ } else if (registration.semanticRole.bound === "upper") {
2972
+ upperByFamily.set(familyKey, strongestByKey.get(boundKey) ?? entry);
2973
+ }
2974
+ }
2975
+ for (const [familyKey, lower] of lowerByFamily) {
2976
+ const upper = upperByFamily.get(familyKey);
2977
+ if (upper === void 0) {
2978
+ continue;
2979
+ }
2980
+ if (!customConstraintsContradict(lower, upper)) {
2981
+ continue;
2982
+ }
2983
+ addContradiction(
2984
+ ctx,
2985
+ `Field "${formatPathTargetFieldName(fieldName, lower.constraint.path?.segments ?? [])}": ${describeCustomConstraintTag(lower.constraint)} contradicts ${describeCustomConstraintTag(upper.constraint)}`,
2986
+ lower.constraint.provenance,
2987
+ upper.constraint.provenance
2988
+ );
2989
+ }
2990
+ }
2480
2991
  function checkNumericContradictions(ctx, fieldName, constraints) {
2481
2992
  const min = findNumeric(constraints, "minimum");
2482
2993
  const max = findNumeric(constraints, "maximum");
@@ -2788,8 +3299,30 @@ function checkCustomConstraint(ctx, fieldName, type, constraint) {
2788
3299
  );
2789
3300
  return;
2790
3301
  }
2791
- if (registration.applicableTypes === null) return;
2792
- if (!registration.applicableTypes.includes(type.kind)) {
3302
+ const normalizedTagName = constraint.provenance.tagName === void 0 ? void 0 : (0, import_core4.normalizeConstraintTagName)(constraint.provenance.tagName.replace(/^@/, ""));
3303
+ if (normalizedTagName !== void 0) {
3304
+ const tagRegistration = ctx.extensionRegistry.findConstraintTag(normalizedTagName);
3305
+ const extensionId = getExtensionIdFromConstraintId(constraint.constraintId);
3306
+ if (extensionId !== null && tagRegistration?.extensionId === extensionId && tagRegistration.registration.constraintName === registration.constraintName && tagRegistration.registration.isApplicableToType?.(type) === false) {
3307
+ addTypeMismatch(
3308
+ ctx,
3309
+ `Field "${fieldName}": custom constraint "${constraint.constraintId}" is not applicable to type "${typeLabel(type)}"`,
3310
+ constraint.provenance
3311
+ );
3312
+ return;
3313
+ }
3314
+ }
3315
+ if (registration.applicableTypes === null) {
3316
+ if (registration.isApplicableToType?.(type) === false) {
3317
+ addTypeMismatch(
3318
+ ctx,
3319
+ `Field "${fieldName}": custom constraint "${constraint.constraintId}" is not applicable to type "${typeLabel(type)}"`,
3320
+ constraint.provenance
3321
+ );
3322
+ }
3323
+ return;
3324
+ }
3325
+ if (!registration.applicableTypes.includes(type.kind) || registration.isApplicableToType?.(type) === false) {
2793
3326
  addTypeMismatch(
2794
3327
  ctx,
2795
3328
  `Field "${fieldName}": custom constraint "${constraint.constraintId}" is not applicable to type "${typeLabel(type)}"`,
@@ -2820,6 +3353,7 @@ function validateConstraints(ctx, name, type, constraints) {
2820
3353
  checkAllowedMembersContradiction(ctx, name, constraints);
2821
3354
  checkConstContradictions(ctx, name, constraints);
2822
3355
  checkConstraintBroadening(ctx, name, constraints);
3356
+ checkCustomConstraintSemantics(ctx, name, constraints);
2823
3357
  checkTypeApplicability(ctx, name, type, constraints);
2824
3358
  }
2825
3359
  function validateElement(ctx, element) {
@@ -2861,7 +3395,10 @@ function validateIR(ir, options) {
2861
3395
  // src/extensions/registry.ts
2862
3396
  function createExtensionRegistry(extensions) {
2863
3397
  const typeMap = /* @__PURE__ */ new Map();
3398
+ const typeNameMap = /* @__PURE__ */ new Map();
2864
3399
  const constraintMap = /* @__PURE__ */ new Map();
3400
+ const constraintTagMap = /* @__PURE__ */ new Map();
3401
+ const builtinBroadeningMap = /* @__PURE__ */ new Map();
2865
3402
  const annotationMap = /* @__PURE__ */ new Map();
2866
3403
  for (const ext of extensions) {
2867
3404
  if (ext.types !== void 0) {
@@ -2871,6 +3408,27 @@ function createExtensionRegistry(extensions) {
2871
3408
  throw new Error(`Duplicate custom type ID: "${qualifiedId}"`);
2872
3409
  }
2873
3410
  typeMap.set(qualifiedId, type);
3411
+ for (const sourceTypeName of type.tsTypeNames ?? [type.typeName]) {
3412
+ if (typeNameMap.has(sourceTypeName)) {
3413
+ throw new Error(`Duplicate custom type source name: "${sourceTypeName}"`);
3414
+ }
3415
+ typeNameMap.set(sourceTypeName, {
3416
+ extensionId: ext.extensionId,
3417
+ registration: type
3418
+ });
3419
+ }
3420
+ if (type.builtinConstraintBroadenings !== void 0) {
3421
+ for (const broadening of type.builtinConstraintBroadenings) {
3422
+ const key = `${qualifiedId}:${broadening.tagName}`;
3423
+ if (builtinBroadeningMap.has(key)) {
3424
+ throw new Error(`Duplicate built-in constraint broadening: "${key}"`);
3425
+ }
3426
+ builtinBroadeningMap.set(key, {
3427
+ extensionId: ext.extensionId,
3428
+ registration: broadening
3429
+ });
3430
+ }
3431
+ }
2874
3432
  }
2875
3433
  }
2876
3434
  if (ext.constraints !== void 0) {
@@ -2882,6 +3440,17 @@ function createExtensionRegistry(extensions) {
2882
3440
  constraintMap.set(qualifiedId, constraint);
2883
3441
  }
2884
3442
  }
3443
+ if (ext.constraintTags !== void 0) {
3444
+ for (const tag of ext.constraintTags) {
3445
+ if (constraintTagMap.has(tag.tagName)) {
3446
+ throw new Error(`Duplicate custom constraint tag: "@${tag.tagName}"`);
3447
+ }
3448
+ constraintTagMap.set(tag.tagName, {
3449
+ extensionId: ext.extensionId,
3450
+ registration: tag
3451
+ });
3452
+ }
3453
+ }
2885
3454
  if (ext.annotations !== void 0) {
2886
3455
  for (const annotation of ext.annotations) {
2887
3456
  const qualifiedId = `${ext.extensionId}/${annotation.annotationName}`;
@@ -2895,13 +3464,16 @@ function createExtensionRegistry(extensions) {
2895
3464
  return {
2896
3465
  extensions,
2897
3466
  findType: (typeId) => typeMap.get(typeId),
3467
+ findTypeByName: (typeName) => typeNameMap.get(typeName),
2898
3468
  findConstraint: (constraintId) => constraintMap.get(constraintId),
3469
+ findConstraintTag: (tagName) => constraintTagMap.get(tagName),
3470
+ findBuiltinConstraintBroadening: (typeId, tagName) => builtinBroadeningMap.get(`${typeId}:${tagName}`),
2899
3471
  findAnnotation: (annotationId) => annotationMap.get(annotationId)
2900
3472
  };
2901
3473
  }
2902
3474
 
2903
3475
  // src/generators/method-schema.ts
2904
- var import_core4 = require("@formspec/core");
3476
+ var import_core5 = require("@formspec/core");
2905
3477
  function typeToJsonSchema(type, checker) {
2906
3478
  const typeRegistry = {};
2907
3479
  const visiting = /* @__PURE__ */ new Set();
@@ -2909,7 +3481,7 @@ function typeToJsonSchema(type, checker) {
2909
3481
  const fieldProvenance = { surface: "tsdoc", file: "", line: 0, column: 0 };
2910
3482
  const ir = {
2911
3483
  kind: "form-ir",
2912
- irVersion: import_core4.IR_VERSION,
3484
+ irVersion: import_core5.IR_VERSION,
2913
3485
  elements: [
2914
3486
  {
2915
3487
  kind: "field",