@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
package/dist/internals.js CHANGED
@@ -503,7 +503,7 @@ var LENGTH_CONSTRAINT_MAP = {
503
503
  maxItems: "maxItems"
504
504
  };
505
505
  var TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["pattern", "enumOptions", "defaultValue"]);
506
- function createFormSpecTSDocConfig() {
506
+ function createFormSpecTSDocConfig(extensionTagNames = []) {
507
507
  const config = new TSDocConfiguration();
508
508
  for (const tagName of Object.keys(BUILTIN_CONSTRAINT_DEFINITIONS)) {
509
509
  config.addTagDefinition(
@@ -523,14 +523,34 @@ function createFormSpecTSDocConfig() {
523
523
  })
524
524
  );
525
525
  }
526
+ for (const tagName of extensionTagNames) {
527
+ config.addTagDefinition(
528
+ new TSDocTagDefinition({
529
+ tagName: "@" + tagName,
530
+ syntaxKind: TSDocTagSyntaxKind.BlockTag,
531
+ allowMultiple: true
532
+ })
533
+ );
534
+ }
526
535
  return config;
527
536
  }
528
- var sharedParser;
529
- function getParser() {
530
- sharedParser ??= new TSDocParser(createFormSpecTSDocConfig());
531
- return sharedParser;
532
- }
533
- function parseTSDocTags(node, file = "") {
537
+ var parserCache = /* @__PURE__ */ new Map();
538
+ function getParser(options) {
539
+ const extensionTagNames = [
540
+ ...options?.extensionRegistry?.extensions.flatMap(
541
+ (extension) => (extension.constraintTags ?? []).map((tag) => tag.tagName)
542
+ ) ?? []
543
+ ].sort();
544
+ const cacheKey = extensionTagNames.join("|");
545
+ const existing = parserCache.get(cacheKey);
546
+ if (existing) {
547
+ return existing;
548
+ }
549
+ const parser = new TSDocParser(createFormSpecTSDocConfig(extensionTagNames));
550
+ parserCache.set(cacheKey, parser);
551
+ return parser;
552
+ }
553
+ function parseTSDocTags(node, file = "", options) {
534
554
  const constraints = [];
535
555
  const annotations = [];
536
556
  let displayName;
@@ -551,7 +571,7 @@ function parseTSDocTags(node, file = "") {
551
571
  if (!commentText.startsWith("/**")) {
552
572
  continue;
553
573
  }
554
- const parser = getParser();
574
+ const parser = getParser(options);
555
575
  const parserContext = parser.parseRange(
556
576
  TextRange.fromStringRange(sourceText, range.pos, range.end)
557
577
  );
@@ -590,7 +610,7 @@ function parseTSDocTags(node, file = "") {
590
610
  const expectedType = isBuiltinConstraintName(tagName) ? BUILTIN_CONSTRAINT_DEFINITIONS[tagName] : void 0;
591
611
  if (text === "" && expectedType !== "boolean") continue;
592
612
  const provenance = provenanceForComment(range, sourceFile, file, tagName);
593
- const constraintNode = parseConstraintValue(tagName, text, provenance);
613
+ const constraintNode = parseConstraintValue(tagName, text, provenance, options);
594
614
  if (constraintNode) {
595
615
  constraints.push(constraintNode);
596
616
  }
@@ -650,7 +670,7 @@ function parseTSDocTags(node, file = "") {
650
670
  annotations.push(defaultValueNode);
651
671
  continue;
652
672
  }
653
- const constraintNode = parseConstraintValue(tagName, text, provenance);
673
+ const constraintNode = parseConstraintValue(tagName, text, provenance, options);
654
674
  if (constraintNode) {
655
675
  constraints.push(constraintNode);
656
676
  }
@@ -706,7 +726,11 @@ function extractPlainText(node) {
706
726
  }
707
727
  return result;
708
728
  }
709
- function parseConstraintValue(tagName, text, provenance) {
729
+ function parseConstraintValue(tagName, text, provenance, options) {
730
+ const customConstraint = parseExtensionConstraintValue(tagName, text, provenance, options);
731
+ if (customConstraint) {
732
+ return customConstraint;
733
+ }
710
734
  if (!isBuiltinConstraintName(tagName)) {
711
735
  return null;
712
736
  }
@@ -811,6 +835,83 @@ function parseConstraintValue(tagName, text, provenance) {
811
835
  provenance
812
836
  };
813
837
  }
838
+ function parseExtensionConstraintValue(tagName, text, provenance, options) {
839
+ const pathResult = extractPathTarget(text);
840
+ const effectiveText = pathResult ? pathResult.remainingText : text;
841
+ const path2 = pathResult?.path;
842
+ const registry = options?.extensionRegistry;
843
+ if (registry === void 0) {
844
+ return null;
845
+ }
846
+ const directTag = registry.findConstraintTag(tagName);
847
+ if (directTag !== void 0) {
848
+ return makeCustomConstraintNode(
849
+ directTag.extensionId,
850
+ directTag.registration.constraintName,
851
+ directTag.registration.parseValue(effectiveText),
852
+ provenance,
853
+ path2,
854
+ registry
855
+ );
856
+ }
857
+ if (!isBuiltinConstraintName(tagName)) {
858
+ return null;
859
+ }
860
+ const broadenedTypeId = getBroadenedCustomTypeId(options?.fieldType);
861
+ if (broadenedTypeId === void 0) {
862
+ return null;
863
+ }
864
+ const broadened = registry.findBuiltinConstraintBroadening(broadenedTypeId, tagName);
865
+ if (broadened === void 0) {
866
+ return null;
867
+ }
868
+ return makeCustomConstraintNode(
869
+ broadened.extensionId,
870
+ broadened.registration.constraintName,
871
+ broadened.registration.parseValue(effectiveText),
872
+ provenance,
873
+ path2,
874
+ registry
875
+ );
876
+ }
877
+ function getBroadenedCustomTypeId(fieldType) {
878
+ if (fieldType?.kind === "custom") {
879
+ return fieldType.typeId;
880
+ }
881
+ if (fieldType?.kind !== "union") {
882
+ return void 0;
883
+ }
884
+ const customMembers = fieldType.members.filter(
885
+ (member) => member.kind === "custom"
886
+ );
887
+ if (customMembers.length !== 1) {
888
+ return void 0;
889
+ }
890
+ const nonCustomMembers = fieldType.members.filter((member) => member.kind !== "custom");
891
+ const allOtherMembersAreNull = nonCustomMembers.every(
892
+ (member) => member.kind === "primitive" && member.primitiveKind === "null"
893
+ );
894
+ const customMember = customMembers[0];
895
+ return allOtherMembersAreNull && customMember !== void 0 ? customMember.typeId : void 0;
896
+ }
897
+ function makeCustomConstraintNode(extensionId, constraintName, payload, provenance, path2, registry) {
898
+ const constraintId = `${extensionId}/${constraintName}`;
899
+ const registration = registry.findConstraint(constraintId);
900
+ if (registration === void 0) {
901
+ throw new Error(
902
+ `Custom TSDoc tag resolved to unregistered constraint "${constraintId}". Register the constraint before using its tag.`
903
+ );
904
+ }
905
+ return {
906
+ kind: "constraint",
907
+ constraintKind: "custom",
908
+ constraintId,
909
+ payload,
910
+ compositionRule: registration.compositionRule,
911
+ ...path2 && { path: path2 },
912
+ provenance
913
+ };
914
+ }
814
915
  function parseDefaultValueValue(text, provenance) {
815
916
  const trimmed = text.trim();
816
917
  let value;
@@ -871,12 +972,12 @@ function getTagCommentText(tag) {
871
972
  }
872
973
 
873
974
  // src/analyzer/jsdoc-constraints.ts
874
- function extractJSDocConstraintNodes(node, file = "") {
875
- const result = parseTSDocTags(node, file);
975
+ function extractJSDocConstraintNodes(node, file = "", options) {
976
+ const result = parseTSDocTags(node, file, options);
876
977
  return [...result.constraints];
877
978
  }
878
- function extractJSDocAnnotationNodes(node, file = "") {
879
- const result = parseTSDocTags(node, file);
979
+ function extractJSDocAnnotationNodes(node, file = "", options) {
980
+ const result = parseTSDocTags(node, file, options);
880
981
  return [...result.annotations];
881
982
  }
882
983
  function extractDefaultValueAnnotation(initializer, file = "") {
@@ -925,18 +1026,38 @@ var RESOLVING_TYPE_PLACEHOLDER = {
925
1026
  properties: [],
926
1027
  additionalProperties: true
927
1028
  };
928
- function analyzeClassToIR(classDecl, checker, file = "") {
1029
+ function makeParseOptions(extensionRegistry, fieldType) {
1030
+ if (extensionRegistry === void 0 && fieldType === void 0) {
1031
+ return void 0;
1032
+ }
1033
+ return {
1034
+ ...extensionRegistry !== void 0 && { extensionRegistry },
1035
+ ...fieldType !== void 0 && { fieldType }
1036
+ };
1037
+ }
1038
+ function analyzeClassToIR(classDecl, checker, file = "", extensionRegistry) {
929
1039
  const name = classDecl.name?.text ?? "AnonymousClass";
930
1040
  const fields = [];
931
1041
  const fieldLayouts = [];
932
1042
  const typeRegistry = {};
933
- const annotations = extractJSDocAnnotationNodes(classDecl, file);
1043
+ const annotations = extractJSDocAnnotationNodes(
1044
+ classDecl,
1045
+ file,
1046
+ makeParseOptions(extensionRegistry)
1047
+ );
934
1048
  const visiting = /* @__PURE__ */ new Set();
935
1049
  const instanceMethods = [];
936
1050
  const staticMethods = [];
937
1051
  for (const member of classDecl.members) {
938
1052
  if (ts4.isPropertyDeclaration(member)) {
939
- const fieldNode = analyzeFieldToIR(member, checker, file, typeRegistry, visiting);
1053
+ const fieldNode = analyzeFieldToIR(
1054
+ member,
1055
+ checker,
1056
+ file,
1057
+ typeRegistry,
1058
+ visiting,
1059
+ extensionRegistry
1060
+ );
940
1061
  if (fieldNode) {
941
1062
  fields.push(fieldNode);
942
1063
  fieldLayouts.push({});
@@ -963,15 +1084,26 @@ function analyzeClassToIR(classDecl, checker, file = "") {
963
1084
  staticMethods
964
1085
  };
965
1086
  }
966
- function analyzeInterfaceToIR(interfaceDecl, checker, file = "") {
1087
+ function analyzeInterfaceToIR(interfaceDecl, checker, file = "", extensionRegistry) {
967
1088
  const name = interfaceDecl.name.text;
968
1089
  const fields = [];
969
1090
  const typeRegistry = {};
970
- const annotations = extractJSDocAnnotationNodes(interfaceDecl, file);
1091
+ const annotations = extractJSDocAnnotationNodes(
1092
+ interfaceDecl,
1093
+ file,
1094
+ makeParseOptions(extensionRegistry)
1095
+ );
971
1096
  const visiting = /* @__PURE__ */ new Set();
972
1097
  for (const member of interfaceDecl.members) {
973
1098
  if (ts4.isPropertySignature(member)) {
974
- const fieldNode = analyzeInterfacePropertyToIR(member, checker, file, typeRegistry, visiting);
1099
+ const fieldNode = analyzeInterfacePropertyToIR(
1100
+ member,
1101
+ checker,
1102
+ file,
1103
+ typeRegistry,
1104
+ visiting,
1105
+ extensionRegistry
1106
+ );
975
1107
  if (fieldNode) {
976
1108
  fields.push(fieldNode);
977
1109
  }
@@ -988,7 +1120,7 @@ function analyzeInterfaceToIR(interfaceDecl, checker, file = "") {
988
1120
  staticMethods: []
989
1121
  };
990
1122
  }
991
- function analyzeTypeAliasToIR(typeAlias, checker, file = "") {
1123
+ function analyzeTypeAliasToIR(typeAlias, checker, file = "", extensionRegistry) {
992
1124
  if (!ts4.isTypeLiteralNode(typeAlias.type)) {
993
1125
  const sourceFile = typeAlias.getSourceFile();
994
1126
  const { line } = sourceFile.getLineAndCharacterOfPosition(typeAlias.getStart());
@@ -1001,11 +1133,22 @@ function analyzeTypeAliasToIR(typeAlias, checker, file = "") {
1001
1133
  const name = typeAlias.name.text;
1002
1134
  const fields = [];
1003
1135
  const typeRegistry = {};
1004
- const annotations = extractJSDocAnnotationNodes(typeAlias, file);
1136
+ const annotations = extractJSDocAnnotationNodes(
1137
+ typeAlias,
1138
+ file,
1139
+ makeParseOptions(extensionRegistry)
1140
+ );
1005
1141
  const visiting = /* @__PURE__ */ new Set();
1006
1142
  for (const member of typeAlias.type.members) {
1007
1143
  if (ts4.isPropertySignature(member)) {
1008
- const fieldNode = analyzeInterfacePropertyToIR(member, checker, file, typeRegistry, visiting);
1144
+ const fieldNode = analyzeInterfacePropertyToIR(
1145
+ member,
1146
+ checker,
1147
+ file,
1148
+ typeRegistry,
1149
+ visiting,
1150
+ extensionRegistry
1151
+ );
1009
1152
  if (fieldNode) {
1010
1153
  fields.push(fieldNode);
1011
1154
  }
@@ -1024,7 +1167,7 @@ function analyzeTypeAliasToIR(typeAlias, checker, file = "") {
1024
1167
  }
1025
1168
  };
1026
1169
  }
1027
- function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting) {
1170
+ function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting, extensionRegistry) {
1028
1171
  if (!ts4.isIdentifier(prop.name)) {
1029
1172
  return null;
1030
1173
  }
@@ -1032,14 +1175,26 @@ function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting) {
1032
1175
  const tsType = checker.getTypeAtLocation(prop);
1033
1176
  const optional = prop.questionToken !== void 0;
1034
1177
  const provenance = provenanceForNode(prop, file);
1035
- let type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting, prop);
1178
+ let type = resolveTypeNode(
1179
+ tsType,
1180
+ checker,
1181
+ file,
1182
+ typeRegistry,
1183
+ visiting,
1184
+ prop,
1185
+ extensionRegistry
1186
+ );
1036
1187
  const constraints = [];
1037
1188
  if (prop.type) {
1038
- constraints.push(...extractTypeAliasConstraintNodes(prop.type, checker, file));
1189
+ constraints.push(
1190
+ ...extractTypeAliasConstraintNodes(prop.type, checker, file, extensionRegistry)
1191
+ );
1039
1192
  }
1040
- constraints.push(...extractJSDocConstraintNodes(prop, file));
1193
+ constraints.push(...extractJSDocConstraintNodes(prop, file, makeParseOptions(extensionRegistry, type)));
1041
1194
  let annotations = [];
1042
- annotations.push(...extractJSDocAnnotationNodes(prop, file));
1195
+ annotations.push(
1196
+ ...extractJSDocAnnotationNodes(prop, file, makeParseOptions(extensionRegistry, type))
1197
+ );
1043
1198
  const defaultAnnotation = extractDefaultValueAnnotation(prop.initializer, file);
1044
1199
  if (defaultAnnotation && !annotations.some((a) => a.annotationKind === "defaultValue")) {
1045
1200
  annotations.push(defaultAnnotation);
@@ -1055,7 +1210,7 @@ function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting) {
1055
1210
  provenance
1056
1211
  };
1057
1212
  }
1058
- function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visiting) {
1213
+ function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visiting, extensionRegistry) {
1059
1214
  if (!ts4.isIdentifier(prop.name)) {
1060
1215
  return null;
1061
1216
  }
@@ -1063,14 +1218,26 @@ function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visitin
1063
1218
  const tsType = checker.getTypeAtLocation(prop);
1064
1219
  const optional = prop.questionToken !== void 0;
1065
1220
  const provenance = provenanceForNode(prop, file);
1066
- let type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting, prop);
1221
+ let type = resolveTypeNode(
1222
+ tsType,
1223
+ checker,
1224
+ file,
1225
+ typeRegistry,
1226
+ visiting,
1227
+ prop,
1228
+ extensionRegistry
1229
+ );
1067
1230
  const constraints = [];
1068
1231
  if (prop.type) {
1069
- constraints.push(...extractTypeAliasConstraintNodes(prop.type, checker, file));
1232
+ constraints.push(
1233
+ ...extractTypeAliasConstraintNodes(prop.type, checker, file, extensionRegistry)
1234
+ );
1070
1235
  }
1071
- constraints.push(...extractJSDocConstraintNodes(prop, file));
1236
+ constraints.push(...extractJSDocConstraintNodes(prop, file, makeParseOptions(extensionRegistry, type)));
1072
1237
  let annotations = [];
1073
- annotations.push(...extractJSDocAnnotationNodes(prop, file));
1238
+ annotations.push(
1239
+ ...extractJSDocAnnotationNodes(prop, file, makeParseOptions(extensionRegistry, type))
1240
+ );
1074
1241
  ({ type, annotations } = applyEnumMemberDisplayNames(type, annotations));
1075
1242
  return {
1076
1243
  kind: "field",
@@ -1144,7 +1311,66 @@ function parseEnumMemberDisplayName(value) {
1144
1311
  if (label === "") return null;
1145
1312
  return { value: match[1], label };
1146
1313
  }
1147
- function resolveTypeNode(type, checker, file, typeRegistry, visiting, sourceNode) {
1314
+ function resolveRegisteredCustomType(sourceNode, extensionRegistry, checker) {
1315
+ if (sourceNode === void 0 || extensionRegistry === void 0) {
1316
+ return null;
1317
+ }
1318
+ const typeNode = extractTypeNodeFromSource(sourceNode);
1319
+ if (typeNode === void 0) {
1320
+ return null;
1321
+ }
1322
+ return resolveRegisteredCustomTypeFromTypeNode(typeNode, extensionRegistry, checker);
1323
+ }
1324
+ function resolveRegisteredCustomTypeFromTypeNode(typeNode, extensionRegistry, checker) {
1325
+ if (ts4.isParenthesizedTypeNode(typeNode)) {
1326
+ return resolveRegisteredCustomTypeFromTypeNode(typeNode.type, extensionRegistry, checker);
1327
+ }
1328
+ const typeName = getTypeNodeRegistrationName(typeNode);
1329
+ if (typeName === null) {
1330
+ return null;
1331
+ }
1332
+ const registration = extensionRegistry.findTypeByName(typeName);
1333
+ if (registration !== void 0) {
1334
+ return {
1335
+ kind: "custom",
1336
+ typeId: `${registration.extensionId}/${registration.registration.typeName}`,
1337
+ payload: null
1338
+ };
1339
+ }
1340
+ if (ts4.isTypeReferenceNode(typeNode) && ts4.isIdentifier(typeNode.typeName)) {
1341
+ const aliasDecl = checker.getSymbolAtLocation(typeNode.typeName)?.declarations?.find(ts4.isTypeAliasDeclaration);
1342
+ if (aliasDecl !== void 0) {
1343
+ return resolveRegisteredCustomTypeFromTypeNode(aliasDecl.type, extensionRegistry, checker);
1344
+ }
1345
+ }
1346
+ return null;
1347
+ }
1348
+ function extractTypeNodeFromSource(sourceNode) {
1349
+ if (ts4.isPropertyDeclaration(sourceNode) || ts4.isPropertySignature(sourceNode) || ts4.isParameter(sourceNode) || ts4.isTypeAliasDeclaration(sourceNode)) {
1350
+ return sourceNode.type;
1351
+ }
1352
+ if (ts4.isTypeNode(sourceNode)) {
1353
+ return sourceNode;
1354
+ }
1355
+ return void 0;
1356
+ }
1357
+ function getTypeNodeRegistrationName(typeNode) {
1358
+ if (ts4.isTypeReferenceNode(typeNode)) {
1359
+ return ts4.isIdentifier(typeNode.typeName) ? typeNode.typeName.text : typeNode.typeName.right.text;
1360
+ }
1361
+ if (ts4.isParenthesizedTypeNode(typeNode)) {
1362
+ return getTypeNodeRegistrationName(typeNode.type);
1363
+ }
1364
+ if (typeNode.kind === ts4.SyntaxKind.BigIntKeyword || typeNode.kind === ts4.SyntaxKind.StringKeyword || typeNode.kind === ts4.SyntaxKind.NumberKeyword || typeNode.kind === ts4.SyntaxKind.BooleanKeyword) {
1365
+ return typeNode.getText();
1366
+ }
1367
+ return null;
1368
+ }
1369
+ function resolveTypeNode(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry) {
1370
+ const customType = resolveRegisteredCustomType(sourceNode, extensionRegistry, checker);
1371
+ if (customType) {
1372
+ return customType;
1373
+ }
1148
1374
  if (type.flags & ts4.TypeFlags.String) {
1149
1375
  return { kind: "primitive", primitiveKind: "string" };
1150
1376
  }
@@ -1173,26 +1399,50 @@ function resolveTypeNode(type, checker, file, typeRegistry, visiting, sourceNode
1173
1399
  };
1174
1400
  }
1175
1401
  if (type.isUnion()) {
1176
- return resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNode);
1402
+ return resolveUnionType(
1403
+ type,
1404
+ checker,
1405
+ file,
1406
+ typeRegistry,
1407
+ visiting,
1408
+ sourceNode,
1409
+ extensionRegistry
1410
+ );
1177
1411
  }
1178
1412
  if (checker.isArrayType(type)) {
1179
- return resolveArrayType(type, checker, file, typeRegistry, visiting);
1413
+ return resolveArrayType(
1414
+ type,
1415
+ checker,
1416
+ file,
1417
+ typeRegistry,
1418
+ visiting,
1419
+ sourceNode,
1420
+ extensionRegistry
1421
+ );
1180
1422
  }
1181
1423
  if (isObjectType(type)) {
1182
- return resolveObjectType(type, checker, file, typeRegistry, visiting);
1424
+ return resolveObjectType(type, checker, file, typeRegistry, visiting, extensionRegistry);
1183
1425
  }
1184
1426
  return { kind: "primitive", primitiveKind: "string" };
1185
1427
  }
1186
- function resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNode) {
1428
+ function resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry) {
1187
1429
  const typeName = getNamedTypeName(type);
1188
1430
  const namedDecl = getNamedTypeDeclaration(type);
1189
1431
  if (typeName && typeName in typeRegistry) {
1190
1432
  return { kind: "reference", name: typeName, typeArguments: [] };
1191
1433
  }
1192
1434
  const allTypes = type.types;
1435
+ const unionMemberTypeNodes = extractUnionMemberTypeNodes(sourceNode, checker);
1436
+ const nonNullSourceNodes = unionMemberTypeNodes.filter(
1437
+ (memberTypeNode) => !isNullishTypeNode(resolveAliasedTypeNode(memberTypeNode, checker))
1438
+ );
1193
1439
  const nonNullTypes = allTypes.filter(
1194
- (t) => !(t.flags & (ts4.TypeFlags.Null | ts4.TypeFlags.Undefined))
1440
+ (memberType) => !(memberType.flags & (ts4.TypeFlags.Null | ts4.TypeFlags.Undefined))
1195
1441
  );
1442
+ const nonNullMembers = nonNullTypes.map((memberType, index) => ({
1443
+ memberType,
1444
+ sourceNode: nonNullSourceNodes.length === nonNullTypes.length ? nonNullSourceNodes[index] : void 0
1445
+ }));
1196
1446
  const hasNull = allTypes.some((t) => t.flags & ts4.TypeFlags.Null);
1197
1447
  const memberDisplayNames = /* @__PURE__ */ new Map();
1198
1448
  if (namedDecl) {
@@ -1209,7 +1459,7 @@ function resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNod
1209
1459
  if (!typeName) {
1210
1460
  return result;
1211
1461
  }
1212
- const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file) : void 0;
1462
+ const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file, makeParseOptions(extensionRegistry)) : void 0;
1213
1463
  typeRegistry[typeName] = {
1214
1464
  name: typeName,
1215
1465
  type: result,
@@ -1257,14 +1507,15 @@ function resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNod
1257
1507
  } : enumNode;
1258
1508
  return registerNamed(result);
1259
1509
  }
1260
- if (nonNullTypes.length === 1 && nonNullTypes[0]) {
1510
+ if (nonNullMembers.length === 1 && nonNullMembers[0]) {
1261
1511
  const inner = resolveTypeNode(
1262
- nonNullTypes[0],
1512
+ nonNullMembers[0].memberType,
1263
1513
  checker,
1264
1514
  file,
1265
1515
  typeRegistry,
1266
1516
  visiting,
1267
- sourceNode
1517
+ nonNullMembers[0].sourceNode ?? sourceNode,
1518
+ extensionRegistry
1268
1519
  );
1269
1520
  const result = hasNull ? {
1270
1521
  kind: "union",
@@ -1272,21 +1523,38 @@ function resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNod
1272
1523
  } : inner;
1273
1524
  return registerNamed(result);
1274
1525
  }
1275
- const members = nonNullTypes.map(
1276
- (t) => resolveTypeNode(t, checker, file, typeRegistry, visiting, sourceNode)
1526
+ const members = nonNullMembers.map(
1527
+ ({ memberType, sourceNode: memberSourceNode }) => resolveTypeNode(
1528
+ memberType,
1529
+ checker,
1530
+ file,
1531
+ typeRegistry,
1532
+ visiting,
1533
+ memberSourceNode ?? sourceNode,
1534
+ extensionRegistry
1535
+ )
1277
1536
  );
1278
1537
  if (hasNull) {
1279
1538
  members.push({ kind: "primitive", primitiveKind: "null" });
1280
1539
  }
1281
1540
  return registerNamed({ kind: "union", members });
1282
1541
  }
1283
- function resolveArrayType(type, checker, file, typeRegistry, visiting) {
1542
+ function resolveArrayType(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry) {
1284
1543
  const typeArgs = isTypeReference(type) ? type.typeArguments : void 0;
1285
1544
  const elementType = typeArgs?.[0];
1286
- const items = elementType ? resolveTypeNode(elementType, checker, file, typeRegistry, visiting) : { kind: "primitive", primitiveKind: "string" };
1545
+ const elementSourceNode = extractArrayElementTypeNode(sourceNode, checker);
1546
+ const items = elementType ? resolveTypeNode(
1547
+ elementType,
1548
+ checker,
1549
+ file,
1550
+ typeRegistry,
1551
+ visiting,
1552
+ elementSourceNode,
1553
+ extensionRegistry
1554
+ ) : { kind: "primitive", primitiveKind: "string" };
1287
1555
  return { kind: "array", items };
1288
1556
  }
1289
- function tryResolveRecordType(type, checker, file, typeRegistry, visiting) {
1557
+ function tryResolveRecordType(type, checker, file, typeRegistry, visiting, extensionRegistry) {
1290
1558
  if (type.getProperties().length > 0) {
1291
1559
  return null;
1292
1560
  }
@@ -1294,7 +1562,15 @@ function tryResolveRecordType(type, checker, file, typeRegistry, visiting) {
1294
1562
  if (!indexInfo) {
1295
1563
  return null;
1296
1564
  }
1297
- const valueType = resolveTypeNode(indexInfo.type, checker, file, typeRegistry, visiting);
1565
+ const valueType = resolveTypeNode(
1566
+ indexInfo.type,
1567
+ checker,
1568
+ file,
1569
+ typeRegistry,
1570
+ visiting,
1571
+ void 0,
1572
+ extensionRegistry
1573
+ );
1298
1574
  return { kind: "record", valueType };
1299
1575
  }
1300
1576
  function typeNodeContainsReference(type, targetName) {
@@ -1322,7 +1598,7 @@ function typeNodeContainsReference(type, targetName) {
1322
1598
  }
1323
1599
  }
1324
1600
  }
1325
- function resolveObjectType(type, checker, file, typeRegistry, visiting) {
1601
+ function resolveObjectType(type, checker, file, typeRegistry, visiting, extensionRegistry) {
1326
1602
  const typeName = getNamedTypeName(type);
1327
1603
  const namedTypeName = typeName ?? void 0;
1328
1604
  const namedDecl = getNamedTypeDeclaration(type);
@@ -1353,7 +1629,14 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting) {
1353
1629
  return { kind: "reference", name: namedTypeName, typeArguments: [] };
1354
1630
  }
1355
1631
  }
1356
- const recordNode = tryResolveRecordType(type, checker, file, typeRegistry, visiting);
1632
+ const recordNode = tryResolveRecordType(
1633
+ type,
1634
+ checker,
1635
+ file,
1636
+ typeRegistry,
1637
+ visiting,
1638
+ extensionRegistry
1639
+ );
1357
1640
  if (recordNode) {
1358
1641
  visiting.delete(type);
1359
1642
  if (namedTypeName !== void 0 && shouldRegisterNamedType) {
@@ -1362,7 +1645,7 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting) {
1362
1645
  clearNamedTypeRegistration();
1363
1646
  return recordNode;
1364
1647
  }
1365
- const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file) : void 0;
1648
+ const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file, makeParseOptions(extensionRegistry)) : void 0;
1366
1649
  typeRegistry[namedTypeName] = {
1367
1650
  name: namedTypeName,
1368
1651
  type: recordNode,
@@ -1374,7 +1657,14 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting) {
1374
1657
  return recordNode;
1375
1658
  }
1376
1659
  const properties = [];
1377
- const fieldInfoMap = getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visiting);
1660
+ const fieldInfoMap = getNamedTypeFieldNodeInfoMap(
1661
+ type,
1662
+ checker,
1663
+ file,
1664
+ typeRegistry,
1665
+ visiting,
1666
+ extensionRegistry
1667
+ );
1378
1668
  for (const prop of type.getProperties()) {
1379
1669
  const declaration = prop.valueDeclaration ?? prop.declarations?.[0];
1380
1670
  if (!declaration) continue;
@@ -1386,7 +1676,8 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting) {
1386
1676
  file,
1387
1677
  typeRegistry,
1388
1678
  visiting,
1389
- declaration
1679
+ declaration,
1680
+ extensionRegistry
1390
1681
  );
1391
1682
  const fieldNodeInfo = fieldInfoMap?.get(prop.name);
1392
1683
  properties.push({
@@ -1405,7 +1696,7 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting) {
1405
1696
  additionalProperties: true
1406
1697
  };
1407
1698
  if (namedTypeName !== void 0 && shouldRegisterNamedType) {
1408
- const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file) : void 0;
1699
+ const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file, makeParseOptions(extensionRegistry)) : void 0;
1409
1700
  typeRegistry[namedTypeName] = {
1410
1701
  name: namedTypeName,
1411
1702
  type: objectNode,
@@ -1416,7 +1707,7 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting) {
1416
1707
  }
1417
1708
  return objectNode;
1418
1709
  }
1419
- function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visiting) {
1710
+ function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visiting, extensionRegistry) {
1420
1711
  const symbols = [type.getSymbol(), type.aliasSymbol].filter(
1421
1712
  (s) => s?.declarations != null && s.declarations.length > 0
1422
1713
  );
@@ -1428,7 +1719,14 @@ function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visitin
1428
1719
  const map = /* @__PURE__ */ new Map();
1429
1720
  for (const member of classDecl.members) {
1430
1721
  if (ts4.isPropertyDeclaration(member) && ts4.isIdentifier(member.name)) {
1431
- const fieldNode = analyzeFieldToIR(member, checker, file, typeRegistry, visiting);
1722
+ const fieldNode = analyzeFieldToIR(
1723
+ member,
1724
+ checker,
1725
+ file,
1726
+ typeRegistry,
1727
+ visiting,
1728
+ extensionRegistry
1729
+ );
1432
1730
  if (fieldNode) {
1433
1731
  map.set(fieldNode.name, {
1434
1732
  constraints: [...fieldNode.constraints],
@@ -1442,7 +1740,14 @@ function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visitin
1442
1740
  }
1443
1741
  const interfaceDecl = declarations.find(ts4.isInterfaceDeclaration);
1444
1742
  if (interfaceDecl) {
1445
- return buildFieldNodeInfoMap(interfaceDecl.members, checker, file, typeRegistry, visiting);
1743
+ return buildFieldNodeInfoMap(
1744
+ interfaceDecl.members,
1745
+ checker,
1746
+ file,
1747
+ typeRegistry,
1748
+ visiting,
1749
+ extensionRegistry
1750
+ );
1446
1751
  }
1447
1752
  const typeAliasDecl = declarations.find(ts4.isTypeAliasDeclaration);
1448
1753
  if (typeAliasDecl && ts4.isTypeLiteralNode(typeAliasDecl.type)) {
@@ -1451,17 +1756,68 @@ function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visitin
1451
1756
  checker,
1452
1757
  file,
1453
1758
  typeRegistry,
1454
- visiting
1759
+ visiting,
1760
+ extensionRegistry
1455
1761
  );
1456
1762
  }
1457
1763
  }
1458
1764
  return null;
1459
1765
  }
1460
- function buildFieldNodeInfoMap(members, checker, file, typeRegistry, visiting) {
1766
+ function extractArrayElementTypeNode(sourceNode, checker) {
1767
+ const typeNode = sourceNode === void 0 ? void 0 : extractTypeNodeFromSource(sourceNode);
1768
+ if (typeNode === void 0) {
1769
+ return void 0;
1770
+ }
1771
+ const resolvedTypeNode = resolveAliasedTypeNode(typeNode, checker);
1772
+ if (ts4.isArrayTypeNode(resolvedTypeNode)) {
1773
+ return resolvedTypeNode.elementType;
1774
+ }
1775
+ if (ts4.isTypeReferenceNode(resolvedTypeNode) && ts4.isIdentifier(resolvedTypeNode.typeName) && resolvedTypeNode.typeName.text === "Array" && resolvedTypeNode.typeArguments?.[0]) {
1776
+ return resolvedTypeNode.typeArguments[0];
1777
+ }
1778
+ return void 0;
1779
+ }
1780
+ function extractUnionMemberTypeNodes(sourceNode, checker) {
1781
+ const typeNode = sourceNode === void 0 ? void 0 : extractTypeNodeFromSource(sourceNode);
1782
+ if (!typeNode) {
1783
+ return [];
1784
+ }
1785
+ const resolvedTypeNode = resolveAliasedTypeNode(typeNode, checker);
1786
+ return ts4.isUnionTypeNode(resolvedTypeNode) ? [...resolvedTypeNode.types] : [];
1787
+ }
1788
+ function resolveAliasedTypeNode(typeNode, checker, visited = /* @__PURE__ */ new Set()) {
1789
+ if (ts4.isParenthesizedTypeNode(typeNode)) {
1790
+ return resolveAliasedTypeNode(typeNode.type, checker, visited);
1791
+ }
1792
+ if (!ts4.isTypeReferenceNode(typeNode) || !ts4.isIdentifier(typeNode.typeName)) {
1793
+ return typeNode;
1794
+ }
1795
+ const symbol = checker.getSymbolAtLocation(typeNode.typeName);
1796
+ const aliasDecl = symbol?.declarations?.find(ts4.isTypeAliasDeclaration);
1797
+ if (aliasDecl === void 0 || visited.has(aliasDecl)) {
1798
+ return typeNode;
1799
+ }
1800
+ visited.add(aliasDecl);
1801
+ return resolveAliasedTypeNode(aliasDecl.type, checker, visited);
1802
+ }
1803
+ function isNullishTypeNode(typeNode) {
1804
+ if (typeNode.kind === ts4.SyntaxKind.NullKeyword || typeNode.kind === ts4.SyntaxKind.UndefinedKeyword) {
1805
+ return true;
1806
+ }
1807
+ return ts4.isLiteralTypeNode(typeNode) && (typeNode.literal.kind === ts4.SyntaxKind.NullKeyword || typeNode.literal.kind === ts4.SyntaxKind.UndefinedKeyword);
1808
+ }
1809
+ function buildFieldNodeInfoMap(members, checker, file, typeRegistry, visiting, extensionRegistry) {
1461
1810
  const map = /* @__PURE__ */ new Map();
1462
1811
  for (const member of members) {
1463
1812
  if (ts4.isPropertySignature(member)) {
1464
- const fieldNode = analyzeInterfacePropertyToIR(member, checker, file, typeRegistry, visiting);
1813
+ const fieldNode = analyzeInterfacePropertyToIR(
1814
+ member,
1815
+ checker,
1816
+ file,
1817
+ typeRegistry,
1818
+ visiting,
1819
+ extensionRegistry
1820
+ );
1465
1821
  if (fieldNode) {
1466
1822
  map.set(fieldNode.name, {
1467
1823
  constraints: [...fieldNode.constraints],
@@ -1474,7 +1830,7 @@ function buildFieldNodeInfoMap(members, checker, file, typeRegistry, visiting) {
1474
1830
  return map;
1475
1831
  }
1476
1832
  var MAX_ALIAS_CHAIN_DEPTH = 8;
1477
- function extractTypeAliasConstraintNodes(typeNode, checker, file, depth = 0) {
1833
+ function extractTypeAliasConstraintNodes(typeNode, checker, file, extensionRegistry, depth = 0) {
1478
1834
  if (!ts4.isTypeReferenceNode(typeNode)) return [];
1479
1835
  if (depth >= MAX_ALIAS_CHAIN_DEPTH) {
1480
1836
  const aliasName = typeNode.typeName.getText();
@@ -1487,8 +1843,29 @@ function extractTypeAliasConstraintNodes(typeNode, checker, file, depth = 0) {
1487
1843
  const aliasDecl = symbol.declarations.find(ts4.isTypeAliasDeclaration);
1488
1844
  if (!aliasDecl) return [];
1489
1845
  if (ts4.isTypeLiteralNode(aliasDecl.type)) return [];
1490
- const constraints = extractJSDocConstraintNodes(aliasDecl, file);
1491
- constraints.push(...extractTypeAliasConstraintNodes(aliasDecl.type, checker, file, depth + 1));
1846
+ const aliasFieldType = resolveTypeNode(
1847
+ checker.getTypeAtLocation(aliasDecl.type),
1848
+ checker,
1849
+ file,
1850
+ {},
1851
+ /* @__PURE__ */ new Set(),
1852
+ aliasDecl.type,
1853
+ extensionRegistry
1854
+ );
1855
+ const constraints = extractJSDocConstraintNodes(
1856
+ aliasDecl,
1857
+ file,
1858
+ makeParseOptions(extensionRegistry, aliasFieldType)
1859
+ );
1860
+ constraints.push(
1861
+ ...extractTypeAliasConstraintNodes(
1862
+ aliasDecl.type,
1863
+ checker,
1864
+ file,
1865
+ extensionRegistry,
1866
+ depth + 1
1867
+ )
1868
+ );
1492
1869
  return constraints;
1493
1870
  }
1494
1871
  function provenanceForNode(node, file) {
@@ -1975,7 +2352,12 @@ function applyCustomConstraint(schema, constraint, ctx) {
1975
2352
  `Cannot generate JSON Schema for custom constraint "${constraint.constraintId}" without a matching extension registration`
1976
2353
  );
1977
2354
  }
1978
- Object.assign(schema, registration.toJsonSchema(constraint.payload, ctx.vendorPrefix));
2355
+ assignVendorPrefixedExtensionKeywords(
2356
+ schema,
2357
+ registration.toJsonSchema(constraint.payload, ctx.vendorPrefix),
2358
+ ctx.vendorPrefix,
2359
+ `custom constraint "${constraint.constraintId}"`
2360
+ );
1979
2361
  }
1980
2362
  function applyCustomAnnotation(schema, annotation, ctx) {
1981
2363
  const registration = ctx.extensionRegistry?.findAnnotation(annotation.annotationId);
@@ -1987,7 +2369,22 @@ function applyCustomAnnotation(schema, annotation, ctx) {
1987
2369
  if (registration.toJsonSchema === void 0) {
1988
2370
  return;
1989
2371
  }
1990
- Object.assign(schema, registration.toJsonSchema(annotation.value, ctx.vendorPrefix));
2372
+ assignVendorPrefixedExtensionKeywords(
2373
+ schema,
2374
+ registration.toJsonSchema(annotation.value, ctx.vendorPrefix),
2375
+ ctx.vendorPrefix,
2376
+ `custom annotation "${annotation.annotationId}"`
2377
+ );
2378
+ }
2379
+ function assignVendorPrefixedExtensionKeywords(schema, extensionSchema, vendorPrefix, source) {
2380
+ for (const [key, value] of Object.entries(extensionSchema)) {
2381
+ if (!key.startsWith(`${vendorPrefix}-`)) {
2382
+ throw new Error(
2383
+ `Cannot apply ${source}: extension hooks may only emit "${vendorPrefix}-*" JSON Schema keywords`
2384
+ );
2385
+ }
2386
+ schema[key] = value;
2387
+ }
1991
2388
  }
1992
2389
 
1993
2390
  // src/ui-schema/schema.ts
@@ -2214,15 +2611,16 @@ function generateUiSchemaFromIR(ir) {
2214
2611
  }
2215
2612
 
2216
2613
  // src/generators/class-schema.ts
2217
- function generateClassSchemas(analysis, source) {
2614
+ function generateClassSchemas(analysis, source, options) {
2218
2615
  const ir = canonicalizeTSDoc(analysis, source);
2219
2616
  return {
2220
- jsonSchema: generateJsonSchemaFromIR(ir),
2617
+ jsonSchema: generateJsonSchemaFromIR(ir, options),
2221
2618
  uiSchema: generateUiSchemaFromIR(ir)
2222
2619
  };
2223
2620
  }
2224
2621
 
2225
2622
  // src/validate/constraint-validator.ts
2623
+ import { normalizeConstraintTagName as normalizeConstraintTagName2 } from "@formspec/core";
2226
2624
  function addContradiction(ctx, message, primary, related) {
2227
2625
  ctx.diagnostics.push({
2228
2626
  code: "CONTRADICTING_CONSTRAINTS",
@@ -2268,6 +2666,13 @@ function addConstraintBroadening(ctx, message, primary, related) {
2268
2666
  relatedLocations: [related]
2269
2667
  });
2270
2668
  }
2669
+ function getExtensionIdFromConstraintId(constraintId) {
2670
+ const separator = constraintId.lastIndexOf("/");
2671
+ if (separator <= 0) {
2672
+ return null;
2673
+ }
2674
+ return constraintId.slice(0, separator);
2675
+ }
2271
2676
  function findNumeric(constraints, constraintKind) {
2272
2677
  return constraints.find((c) => c.constraintKind === constraintKind);
2273
2678
  }
@@ -2438,6 +2843,112 @@ function checkConstraintBroadening(ctx, fieldName, constraints) {
2438
2843
  strongestByKey.set(key, constraint);
2439
2844
  }
2440
2845
  }
2846
+ function compareCustomConstraintStrength(current, previous) {
2847
+ const order = current.comparePayloads(current.constraint.payload, previous.constraint.payload);
2848
+ const equalPayloadTiebreaker = order === 0 ? compareSemanticInclusivity(current.role.inclusive, previous.role.inclusive) : order;
2849
+ switch (current.role.bound) {
2850
+ case "lower":
2851
+ return equalPayloadTiebreaker;
2852
+ case "upper":
2853
+ return equalPayloadTiebreaker === 0 ? 0 : -equalPayloadTiebreaker;
2854
+ case "exact":
2855
+ return order === 0 ? 0 : Number.NaN;
2856
+ default: {
2857
+ const _exhaustive = current.role.bound;
2858
+ return _exhaustive;
2859
+ }
2860
+ }
2861
+ }
2862
+ function compareSemanticInclusivity(currentInclusive, previousInclusive) {
2863
+ if (currentInclusive === previousInclusive) {
2864
+ return 0;
2865
+ }
2866
+ return currentInclusive ? -1 : 1;
2867
+ }
2868
+ function customConstraintsContradict(lower, upper) {
2869
+ const order = lower.comparePayloads(lower.constraint.payload, upper.constraint.payload);
2870
+ if (order > 0) {
2871
+ return true;
2872
+ }
2873
+ if (order < 0) {
2874
+ return false;
2875
+ }
2876
+ return !lower.role.inclusive || !upper.role.inclusive;
2877
+ }
2878
+ function describeCustomConstraintTag(constraint) {
2879
+ return constraint.provenance.tagName ?? constraint.constraintId;
2880
+ }
2881
+ function checkCustomConstraintSemantics(ctx, fieldName, constraints) {
2882
+ if (ctx.extensionRegistry === void 0) {
2883
+ return;
2884
+ }
2885
+ const strongestByKey = /* @__PURE__ */ new Map();
2886
+ const lowerByFamily = /* @__PURE__ */ new Map();
2887
+ const upperByFamily = /* @__PURE__ */ new Map();
2888
+ for (const constraint of constraints) {
2889
+ if (constraint.constraintKind !== "custom") {
2890
+ continue;
2891
+ }
2892
+ const registration = ctx.extensionRegistry.findConstraint(constraint.constraintId);
2893
+ if (registration?.comparePayloads === void 0 || registration.semanticRole === void 0) {
2894
+ continue;
2895
+ }
2896
+ const entry = {
2897
+ constraint,
2898
+ comparePayloads: registration.comparePayloads,
2899
+ role: registration.semanticRole
2900
+ };
2901
+ const familyKey = `${registration.semanticRole.family}:${pathKey(constraint)}`;
2902
+ const boundKey = `${familyKey}:${registration.semanticRole.bound}`;
2903
+ const previous = strongestByKey.get(boundKey);
2904
+ if (previous !== void 0) {
2905
+ const strength = compareCustomConstraintStrength(entry, previous);
2906
+ if (Number.isNaN(strength)) {
2907
+ addContradiction(
2908
+ ctx,
2909
+ `Field "${formatPathTargetFieldName(fieldName, constraint.path?.segments ?? [])}": ${describeCustomConstraintTag(constraint)} conflicts with ${describeCustomConstraintTag(previous.constraint)}`,
2910
+ constraint.provenance,
2911
+ previous.constraint.provenance
2912
+ );
2913
+ continue;
2914
+ }
2915
+ if (strength < 0) {
2916
+ addConstraintBroadening(
2917
+ ctx,
2918
+ `Field "${formatPathTargetFieldName(fieldName, constraint.path?.segments ?? [])}": ${describeCustomConstraintTag(constraint)} is broader than earlier ${describeCustomConstraintTag(previous.constraint)}. Constraints can only narrow.`,
2919
+ constraint.provenance,
2920
+ previous.constraint.provenance
2921
+ );
2922
+ continue;
2923
+ }
2924
+ if (strength > 0) {
2925
+ strongestByKey.set(boundKey, entry);
2926
+ }
2927
+ } else {
2928
+ strongestByKey.set(boundKey, entry);
2929
+ }
2930
+ if (registration.semanticRole.bound === "lower") {
2931
+ lowerByFamily.set(familyKey, strongestByKey.get(boundKey) ?? entry);
2932
+ } else if (registration.semanticRole.bound === "upper") {
2933
+ upperByFamily.set(familyKey, strongestByKey.get(boundKey) ?? entry);
2934
+ }
2935
+ }
2936
+ for (const [familyKey, lower] of lowerByFamily) {
2937
+ const upper = upperByFamily.get(familyKey);
2938
+ if (upper === void 0) {
2939
+ continue;
2940
+ }
2941
+ if (!customConstraintsContradict(lower, upper)) {
2942
+ continue;
2943
+ }
2944
+ addContradiction(
2945
+ ctx,
2946
+ `Field "${formatPathTargetFieldName(fieldName, lower.constraint.path?.segments ?? [])}": ${describeCustomConstraintTag(lower.constraint)} contradicts ${describeCustomConstraintTag(upper.constraint)}`,
2947
+ lower.constraint.provenance,
2948
+ upper.constraint.provenance
2949
+ );
2950
+ }
2951
+ }
2441
2952
  function checkNumericContradictions(ctx, fieldName, constraints) {
2442
2953
  const min = findNumeric(constraints, "minimum");
2443
2954
  const max = findNumeric(constraints, "maximum");
@@ -2749,8 +3260,30 @@ function checkCustomConstraint(ctx, fieldName, type, constraint) {
2749
3260
  );
2750
3261
  return;
2751
3262
  }
2752
- if (registration.applicableTypes === null) return;
2753
- if (!registration.applicableTypes.includes(type.kind)) {
3263
+ const normalizedTagName = constraint.provenance.tagName === void 0 ? void 0 : normalizeConstraintTagName2(constraint.provenance.tagName.replace(/^@/, ""));
3264
+ if (normalizedTagName !== void 0) {
3265
+ const tagRegistration = ctx.extensionRegistry.findConstraintTag(normalizedTagName);
3266
+ const extensionId = getExtensionIdFromConstraintId(constraint.constraintId);
3267
+ if (extensionId !== null && tagRegistration?.extensionId === extensionId && tagRegistration.registration.constraintName === registration.constraintName && tagRegistration.registration.isApplicableToType?.(type) === false) {
3268
+ addTypeMismatch(
3269
+ ctx,
3270
+ `Field "${fieldName}": custom constraint "${constraint.constraintId}" is not applicable to type "${typeLabel(type)}"`,
3271
+ constraint.provenance
3272
+ );
3273
+ return;
3274
+ }
3275
+ }
3276
+ if (registration.applicableTypes === null) {
3277
+ if (registration.isApplicableToType?.(type) === false) {
3278
+ addTypeMismatch(
3279
+ ctx,
3280
+ `Field "${fieldName}": custom constraint "${constraint.constraintId}" is not applicable to type "${typeLabel(type)}"`,
3281
+ constraint.provenance
3282
+ );
3283
+ }
3284
+ return;
3285
+ }
3286
+ if (!registration.applicableTypes.includes(type.kind) || registration.isApplicableToType?.(type) === false) {
2754
3287
  addTypeMismatch(
2755
3288
  ctx,
2756
3289
  `Field "${fieldName}": custom constraint "${constraint.constraintId}" is not applicable to type "${typeLabel(type)}"`,
@@ -2781,6 +3314,7 @@ function validateConstraints(ctx, name, type, constraints) {
2781
3314
  checkAllowedMembersContradiction(ctx, name, constraints);
2782
3315
  checkConstContradictions(ctx, name, constraints);
2783
3316
  checkConstraintBroadening(ctx, name, constraints);
3317
+ checkCustomConstraintSemantics(ctx, name, constraints);
2784
3318
  checkTypeApplicability(ctx, name, type, constraints);
2785
3319
  }
2786
3320
  function validateElement(ctx, element) {
@@ -2822,7 +3356,10 @@ function validateIR(ir, options) {
2822
3356
  // src/extensions/registry.ts
2823
3357
  function createExtensionRegistry(extensions) {
2824
3358
  const typeMap = /* @__PURE__ */ new Map();
3359
+ const typeNameMap = /* @__PURE__ */ new Map();
2825
3360
  const constraintMap = /* @__PURE__ */ new Map();
3361
+ const constraintTagMap = /* @__PURE__ */ new Map();
3362
+ const builtinBroadeningMap = /* @__PURE__ */ new Map();
2826
3363
  const annotationMap = /* @__PURE__ */ new Map();
2827
3364
  for (const ext of extensions) {
2828
3365
  if (ext.types !== void 0) {
@@ -2832,6 +3369,27 @@ function createExtensionRegistry(extensions) {
2832
3369
  throw new Error(`Duplicate custom type ID: "${qualifiedId}"`);
2833
3370
  }
2834
3371
  typeMap.set(qualifiedId, type);
3372
+ for (const sourceTypeName of type.tsTypeNames ?? [type.typeName]) {
3373
+ if (typeNameMap.has(sourceTypeName)) {
3374
+ throw new Error(`Duplicate custom type source name: "${sourceTypeName}"`);
3375
+ }
3376
+ typeNameMap.set(sourceTypeName, {
3377
+ extensionId: ext.extensionId,
3378
+ registration: type
3379
+ });
3380
+ }
3381
+ if (type.builtinConstraintBroadenings !== void 0) {
3382
+ for (const broadening of type.builtinConstraintBroadenings) {
3383
+ const key = `${qualifiedId}:${broadening.tagName}`;
3384
+ if (builtinBroadeningMap.has(key)) {
3385
+ throw new Error(`Duplicate built-in constraint broadening: "${key}"`);
3386
+ }
3387
+ builtinBroadeningMap.set(key, {
3388
+ extensionId: ext.extensionId,
3389
+ registration: broadening
3390
+ });
3391
+ }
3392
+ }
2835
3393
  }
2836
3394
  }
2837
3395
  if (ext.constraints !== void 0) {
@@ -2843,6 +3401,17 @@ function createExtensionRegistry(extensions) {
2843
3401
  constraintMap.set(qualifiedId, constraint);
2844
3402
  }
2845
3403
  }
3404
+ if (ext.constraintTags !== void 0) {
3405
+ for (const tag of ext.constraintTags) {
3406
+ if (constraintTagMap.has(tag.tagName)) {
3407
+ throw new Error(`Duplicate custom constraint tag: "@${tag.tagName}"`);
3408
+ }
3409
+ constraintTagMap.set(tag.tagName, {
3410
+ extensionId: ext.extensionId,
3411
+ registration: tag
3412
+ });
3413
+ }
3414
+ }
2846
3415
  if (ext.annotations !== void 0) {
2847
3416
  for (const annotation of ext.annotations) {
2848
3417
  const qualifiedId = `${ext.extensionId}/${annotation.annotationName}`;
@@ -2856,7 +3425,10 @@ function createExtensionRegistry(extensions) {
2856
3425
  return {
2857
3426
  extensions,
2858
3427
  findType: (typeId) => typeMap.get(typeId),
3428
+ findTypeByName: (typeName) => typeNameMap.get(typeName),
2859
3429
  findConstraint: (constraintId) => constraintMap.get(constraintId),
3430
+ findConstraintTag: (tagName) => constraintTagMap.get(tagName),
3431
+ findBuiltinConstraintBroadening: (typeId, tagName) => builtinBroadeningMap.get(`${typeId}:${tagName}`),
2860
3432
  findAnnotation: (annotationId) => annotationMap.get(annotationId)
2861
3433
  };
2862
3434
  }