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

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 (65) hide show
  1. package/README.md +74 -128
  2. package/dist/__tests__/class-schema.test.d.ts +2 -0
  3. package/dist/__tests__/class-schema.test.d.ts.map +1 -0
  4. package/dist/__tests__/date-extension.integration.test.d.ts +2 -0
  5. package/dist/__tests__/date-extension.integration.test.d.ts.map +1 -0
  6. package/dist/__tests__/fixtures/class-schema-regressions.d.ts +83 -0
  7. package/dist/__tests__/fixtures/class-schema-regressions.d.ts.map +1 -0
  8. package/dist/__tests__/fixtures/example-date-extension.d.ts +12 -0
  9. package/dist/__tests__/fixtures/example-date-extension.d.ts.map +1 -0
  10. package/dist/__tests__/fixtures/example-numeric-extension.d.ts +20 -0
  11. package/dist/__tests__/fixtures/example-numeric-extension.d.ts.map +1 -0
  12. package/dist/__tests__/fixtures/extension-forms.d.ts +7 -0
  13. package/dist/__tests__/fixtures/extension-forms.d.ts.map +1 -0
  14. package/dist/__tests__/fixtures/mixed-authoring-shipping-address.d.ts +1 -0
  15. package/dist/__tests__/fixtures/mixed-authoring-shipping-address.d.ts.map +1 -1
  16. package/dist/__tests__/fixtures/named-primitive-aliases.d.ts +15 -0
  17. package/dist/__tests__/fixtures/named-primitive-aliases.d.ts.map +1 -0
  18. package/dist/__tests__/fixtures/nested-array-path-constraints.d.ts +14 -0
  19. package/dist/__tests__/fixtures/nested-array-path-constraints.d.ts.map +1 -0
  20. package/dist/__tests__/fixtures/sample-forms.d.ts +10 -0
  21. package/dist/__tests__/fixtures/sample-forms.d.ts.map +1 -1
  22. package/dist/__tests__/generate-schemas.test.d.ts +2 -0
  23. package/dist/__tests__/generate-schemas.test.d.ts.map +1 -0
  24. package/dist/__tests__/numeric-extension.integration.test.d.ts +2 -0
  25. package/dist/__tests__/numeric-extension.integration.test.d.ts.map +1 -0
  26. package/dist/__tests__/parity/parity.test.d.ts +6 -2
  27. package/dist/__tests__/parity/parity.test.d.ts.map +1 -1
  28. package/dist/__tests__/parity/utils.d.ts +9 -4
  29. package/dist/__tests__/parity/utils.d.ts.map +1 -1
  30. package/dist/analyzer/class-analyzer.d.ts +5 -4
  31. package/dist/analyzer/class-analyzer.d.ts.map +1 -1
  32. package/dist/analyzer/jsdoc-constraints.d.ts +3 -2
  33. package/dist/analyzer/jsdoc-constraints.d.ts.map +1 -1
  34. package/dist/analyzer/program.d.ts +15 -0
  35. package/dist/analyzer/program.d.ts.map +1 -1
  36. package/dist/analyzer/tsdoc-parser.d.ts +23 -2
  37. package/dist/analyzer/tsdoc-parser.d.ts.map +1 -1
  38. package/dist/browser.cjs +269 -11
  39. package/dist/browser.cjs.map +1 -1
  40. package/dist/browser.js +269 -11
  41. package/dist/browser.js.map +1 -1
  42. package/dist/build.d.ts +28 -2
  43. package/dist/canonicalize/chain-dsl-canonicalizer.d.ts.map +1 -1
  44. package/dist/canonicalize/tsdoc-canonicalizer.d.ts.map +1 -1
  45. package/dist/cli.cjs +1640 -282
  46. package/dist/cli.cjs.map +1 -1
  47. package/dist/cli.js +1638 -281
  48. package/dist/cli.js.map +1 -1
  49. package/dist/extensions/registry.d.ts +25 -1
  50. package/dist/extensions/registry.d.ts.map +1 -1
  51. package/dist/generators/class-schema.d.ts +4 -4
  52. package/dist/generators/class-schema.d.ts.map +1 -1
  53. package/dist/generators/method-schema.d.ts.map +1 -1
  54. package/dist/generators/mixed-authoring.d.ts.map +1 -1
  55. package/dist/index.cjs +1615 -271
  56. package/dist/index.cjs.map +1 -1
  57. package/dist/index.js +1615 -271
  58. package/dist/index.js.map +1 -1
  59. package/dist/internals.cjs +990 -236
  60. package/dist/internals.cjs.map +1 -1
  61. package/dist/internals.js +988 -234
  62. package/dist/internals.js.map +1 -1
  63. package/dist/json-schema/ir-generator.d.ts.map +1 -1
  64. package/dist/validate/constraint-validator.d.ts.map +1 -1
  65. package/package.json +3 -3
package/dist/browser.js CHANGED
@@ -20,6 +20,7 @@ function canonicalizeChainDSL(form) {
20
20
  kind: "form-ir",
21
21
  irVersion: IR_VERSION,
22
22
  elements: canonicalizeElements(form.elements),
23
+ rootAnnotations: [],
23
24
  typeRegistry: {},
24
25
  provenance: CHAIN_DSL_PROVENANCE
25
26
  };
@@ -330,6 +331,9 @@ function generateJsonSchemaFromIR(ir, options) {
330
331
  const ctx = makeContext(options);
331
332
  for (const [name, typeDef] of Object.entries(ir.typeRegistry)) {
332
333
  ctx.defs[name] = generateTypeNode(typeDef.type, ctx);
334
+ if (typeDef.constraints && typeDef.constraints.length > 0) {
335
+ applyConstraints(ctx.defs[name], typeDef.constraints, ctx);
336
+ }
333
337
  if (typeDef.annotations && typeDef.annotations.length > 0) {
334
338
  applyAnnotations(ctx.defs[name], typeDef.annotations, ctx);
335
339
  }
@@ -498,7 +502,9 @@ function generateTypeNode(type, ctx) {
498
502
  }
499
503
  }
500
504
  function generatePrimitiveType(type) {
501
- return { type: type.primitiveKind };
505
+ return {
506
+ type: type.primitiveKind === "integer" || type.primitiveKind === "bigint" ? "integer" : type.primitiveKind
507
+ };
502
508
  }
503
509
  function generateEnumType(type) {
504
510
  const hasDisplayNames = type.members.some((m) => m.displayName !== void 0);
@@ -671,7 +677,7 @@ function applyAnnotations(schema, annotations, ctx) {
671
677
  case "deprecated":
672
678
  schema.deprecated = true;
673
679
  if (annotation.message !== void 0 && annotation.message !== "") {
674
- schema["x-formspec-deprecation-description"] = annotation.message;
680
+ schema[`${ctx.vendorPrefix}-deprecation-description`] = annotation.message;
675
681
  }
676
682
  break;
677
683
  case "placeholder":
@@ -704,7 +710,12 @@ function applyCustomConstraint(schema, constraint, ctx) {
704
710
  `Cannot generate JSON Schema for custom constraint "${constraint.constraintId}" without a matching extension registration`
705
711
  );
706
712
  }
707
- Object.assign(schema, registration.toJsonSchema(constraint.payload, ctx.vendorPrefix));
713
+ assignVendorPrefixedExtensionKeywords(
714
+ schema,
715
+ registration.toJsonSchema(constraint.payload, ctx.vendorPrefix),
716
+ ctx.vendorPrefix,
717
+ `custom constraint "${constraint.constraintId}"`
718
+ );
708
719
  }
709
720
  function applyCustomAnnotation(schema, annotation, ctx) {
710
721
  const registration = ctx.extensionRegistry?.findAnnotation(annotation.annotationId);
@@ -716,7 +727,22 @@ function applyCustomAnnotation(schema, annotation, ctx) {
716
727
  if (registration.toJsonSchema === void 0) {
717
728
  return;
718
729
  }
719
- Object.assign(schema, registration.toJsonSchema(annotation.value, ctx.vendorPrefix));
730
+ assignVendorPrefixedExtensionKeywords(
731
+ schema,
732
+ registration.toJsonSchema(annotation.value, ctx.vendorPrefix),
733
+ ctx.vendorPrefix,
734
+ `custom annotation "${annotation.annotationId}"`
735
+ );
736
+ }
737
+ function assignVendorPrefixedExtensionKeywords(schema, extensionSchema, vendorPrefix, source) {
738
+ for (const [key, value] of Object.entries(extensionSchema)) {
739
+ if (!key.startsWith(`${vendorPrefix}-`)) {
740
+ throw new Error(
741
+ `Cannot apply ${source}: extension hooks may only emit "${vendorPrefix}-*" JSON Schema keywords`
742
+ );
743
+ }
744
+ schema[key] = value;
745
+ }
720
746
  }
721
747
 
722
748
  // src/json-schema/generator.ts
@@ -965,7 +991,10 @@ function getSchemaExtension(schema, key) {
965
991
  // src/extensions/registry.ts
966
992
  function createExtensionRegistry(extensions) {
967
993
  const typeMap = /* @__PURE__ */ new Map();
994
+ const typeNameMap = /* @__PURE__ */ new Map();
968
995
  const constraintMap = /* @__PURE__ */ new Map();
996
+ const constraintTagMap = /* @__PURE__ */ new Map();
997
+ const builtinBroadeningMap = /* @__PURE__ */ new Map();
969
998
  const annotationMap = /* @__PURE__ */ new Map();
970
999
  for (const ext of extensions) {
971
1000
  if (ext.types !== void 0) {
@@ -975,6 +1004,27 @@ function createExtensionRegistry(extensions) {
975
1004
  throw new Error(`Duplicate custom type ID: "${qualifiedId}"`);
976
1005
  }
977
1006
  typeMap.set(qualifiedId, type);
1007
+ for (const sourceTypeName of type.tsTypeNames ?? [type.typeName]) {
1008
+ if (typeNameMap.has(sourceTypeName)) {
1009
+ throw new Error(`Duplicate custom type source name: "${sourceTypeName}"`);
1010
+ }
1011
+ typeNameMap.set(sourceTypeName, {
1012
+ extensionId: ext.extensionId,
1013
+ registration: type
1014
+ });
1015
+ }
1016
+ if (type.builtinConstraintBroadenings !== void 0) {
1017
+ for (const broadening of type.builtinConstraintBroadenings) {
1018
+ const key = `${qualifiedId}:${broadening.tagName}`;
1019
+ if (builtinBroadeningMap.has(key)) {
1020
+ throw new Error(`Duplicate built-in constraint broadening: "${key}"`);
1021
+ }
1022
+ builtinBroadeningMap.set(key, {
1023
+ extensionId: ext.extensionId,
1024
+ registration: broadening
1025
+ });
1026
+ }
1027
+ }
978
1028
  }
979
1029
  }
980
1030
  if (ext.constraints !== void 0) {
@@ -986,6 +1036,17 @@ function createExtensionRegistry(extensions) {
986
1036
  constraintMap.set(qualifiedId, constraint);
987
1037
  }
988
1038
  }
1039
+ if (ext.constraintTags !== void 0) {
1040
+ for (const tag of ext.constraintTags) {
1041
+ if (constraintTagMap.has(tag.tagName)) {
1042
+ throw new Error(`Duplicate custom constraint tag: "@${tag.tagName}"`);
1043
+ }
1044
+ constraintTagMap.set(tag.tagName, {
1045
+ extensionId: ext.extensionId,
1046
+ registration: tag
1047
+ });
1048
+ }
1049
+ }
989
1050
  if (ext.annotations !== void 0) {
990
1051
  for (const annotation of ext.annotations) {
991
1052
  const qualifiedId = `${ext.extensionId}/${annotation.annotationName}`;
@@ -999,7 +1060,10 @@ function createExtensionRegistry(extensions) {
999
1060
  return {
1000
1061
  extensions,
1001
1062
  findType: (typeId) => typeMap.get(typeId),
1063
+ findTypeByName: (typeName) => typeNameMap.get(typeName),
1002
1064
  findConstraint: (constraintId) => constraintMap.get(constraintId),
1065
+ findConstraintTag: (tagName) => constraintTagMap.get(tagName),
1066
+ findBuiltinConstraintBroadening: (typeId, tagName) => builtinBroadeningMap.get(`${typeId}:${tagName}`),
1003
1067
  findAnnotation: (annotationId) => annotationMap.get(annotationId)
1004
1068
  };
1005
1069
  }
@@ -1067,6 +1131,7 @@ var jsonSchema7Schema = z3.lazy(
1067
1131
  );
1068
1132
 
1069
1133
  // src/validate/constraint-validator.ts
1134
+ import { normalizeConstraintTagName } from "@formspec/core";
1070
1135
  function addContradiction(ctx, message, primary, related) {
1071
1136
  ctx.diagnostics.push({
1072
1137
  code: "CONTRADICTING_CONSTRAINTS",
@@ -1112,6 +1177,13 @@ function addConstraintBroadening(ctx, message, primary, related) {
1112
1177
  relatedLocations: [related]
1113
1178
  });
1114
1179
  }
1180
+ function getExtensionIdFromConstraintId(constraintId) {
1181
+ const separator = constraintId.lastIndexOf("/");
1182
+ if (separator <= 0) {
1183
+ return null;
1184
+ }
1185
+ return constraintId.slice(0, separator);
1186
+ }
1115
1187
  function findNumeric(constraints, constraintKind) {
1116
1188
  return constraints.find((c) => c.constraintKind === constraintKind);
1117
1189
  }
@@ -1282,6 +1354,112 @@ function checkConstraintBroadening(ctx, fieldName, constraints) {
1282
1354
  strongestByKey.set(key, constraint);
1283
1355
  }
1284
1356
  }
1357
+ function compareCustomConstraintStrength(current, previous) {
1358
+ const order = current.comparePayloads(current.constraint.payload, previous.constraint.payload);
1359
+ const equalPayloadTiebreaker = order === 0 ? compareSemanticInclusivity(current.role.inclusive, previous.role.inclusive) : order;
1360
+ switch (current.role.bound) {
1361
+ case "lower":
1362
+ return equalPayloadTiebreaker;
1363
+ case "upper":
1364
+ return equalPayloadTiebreaker === 0 ? 0 : -equalPayloadTiebreaker;
1365
+ case "exact":
1366
+ return order === 0 ? 0 : Number.NaN;
1367
+ default: {
1368
+ const _exhaustive = current.role.bound;
1369
+ return _exhaustive;
1370
+ }
1371
+ }
1372
+ }
1373
+ function compareSemanticInclusivity(currentInclusive, previousInclusive) {
1374
+ if (currentInclusive === previousInclusive) {
1375
+ return 0;
1376
+ }
1377
+ return currentInclusive ? -1 : 1;
1378
+ }
1379
+ function customConstraintsContradict(lower, upper) {
1380
+ const order = lower.comparePayloads(lower.constraint.payload, upper.constraint.payload);
1381
+ if (order > 0) {
1382
+ return true;
1383
+ }
1384
+ if (order < 0) {
1385
+ return false;
1386
+ }
1387
+ return !lower.role.inclusive || !upper.role.inclusive;
1388
+ }
1389
+ function describeCustomConstraintTag(constraint) {
1390
+ return constraint.provenance.tagName ?? constraint.constraintId;
1391
+ }
1392
+ function checkCustomConstraintSemantics(ctx, fieldName, constraints) {
1393
+ if (ctx.extensionRegistry === void 0) {
1394
+ return;
1395
+ }
1396
+ const strongestByKey = /* @__PURE__ */ new Map();
1397
+ const lowerByFamily = /* @__PURE__ */ new Map();
1398
+ const upperByFamily = /* @__PURE__ */ new Map();
1399
+ for (const constraint of constraints) {
1400
+ if (constraint.constraintKind !== "custom") {
1401
+ continue;
1402
+ }
1403
+ const registration = ctx.extensionRegistry.findConstraint(constraint.constraintId);
1404
+ if (registration?.comparePayloads === void 0 || registration.semanticRole === void 0) {
1405
+ continue;
1406
+ }
1407
+ const entry = {
1408
+ constraint,
1409
+ comparePayloads: registration.comparePayloads,
1410
+ role: registration.semanticRole
1411
+ };
1412
+ const familyKey = `${registration.semanticRole.family}:${pathKey(constraint)}`;
1413
+ const boundKey = `${familyKey}:${registration.semanticRole.bound}`;
1414
+ const previous = strongestByKey.get(boundKey);
1415
+ if (previous !== void 0) {
1416
+ const strength = compareCustomConstraintStrength(entry, previous);
1417
+ if (Number.isNaN(strength)) {
1418
+ addContradiction(
1419
+ ctx,
1420
+ `Field "${formatPathTargetFieldName(fieldName, constraint.path?.segments ?? [])}": ${describeCustomConstraintTag(constraint)} conflicts with ${describeCustomConstraintTag(previous.constraint)}`,
1421
+ constraint.provenance,
1422
+ previous.constraint.provenance
1423
+ );
1424
+ continue;
1425
+ }
1426
+ if (strength < 0) {
1427
+ addConstraintBroadening(
1428
+ ctx,
1429
+ `Field "${formatPathTargetFieldName(fieldName, constraint.path?.segments ?? [])}": ${describeCustomConstraintTag(constraint)} is broader than earlier ${describeCustomConstraintTag(previous.constraint)}. Constraints can only narrow.`,
1430
+ constraint.provenance,
1431
+ previous.constraint.provenance
1432
+ );
1433
+ continue;
1434
+ }
1435
+ if (strength > 0) {
1436
+ strongestByKey.set(boundKey, entry);
1437
+ }
1438
+ } else {
1439
+ strongestByKey.set(boundKey, entry);
1440
+ }
1441
+ if (registration.semanticRole.bound === "lower") {
1442
+ lowerByFamily.set(familyKey, strongestByKey.get(boundKey) ?? entry);
1443
+ } else if (registration.semanticRole.bound === "upper") {
1444
+ upperByFamily.set(familyKey, strongestByKey.get(boundKey) ?? entry);
1445
+ }
1446
+ }
1447
+ for (const [familyKey, lower] of lowerByFamily) {
1448
+ const upper = upperByFamily.get(familyKey);
1449
+ if (upper === void 0) {
1450
+ continue;
1451
+ }
1452
+ if (!customConstraintsContradict(lower, upper)) {
1453
+ continue;
1454
+ }
1455
+ addContradiction(
1456
+ ctx,
1457
+ `Field "${formatPathTargetFieldName(fieldName, lower.constraint.path?.segments ?? [])}": ${describeCustomConstraintTag(lower.constraint)} contradicts ${describeCustomConstraintTag(upper.constraint)}`,
1458
+ lower.constraint.provenance,
1459
+ upper.constraint.provenance
1460
+ );
1461
+ }
1462
+ }
1285
1463
  function checkNumericContradictions(ctx, fieldName, constraints) {
1286
1464
  const min = findNumeric(constraints, "minimum");
1287
1465
  const max = findNumeric(constraints, "maximum");
@@ -1429,6 +1607,26 @@ function dereferenceType(ctx, type) {
1429
1607
  }
1430
1608
  return current;
1431
1609
  }
1610
+ function collectReferencedTypeConstraints(ctx, type) {
1611
+ const collected = [];
1612
+ let current = type;
1613
+ const seen = /* @__PURE__ */ new Set();
1614
+ while (current.kind === "reference") {
1615
+ if (seen.has(current.name)) {
1616
+ break;
1617
+ }
1618
+ seen.add(current.name);
1619
+ const definition = ctx.typeRegistry[current.name];
1620
+ if (definition === void 0) {
1621
+ break;
1622
+ }
1623
+ if (definition.constraints !== void 0) {
1624
+ collected.push(...definition.constraints);
1625
+ }
1626
+ current = definition.type;
1627
+ }
1628
+ return collected;
1629
+ }
1432
1630
  function resolvePathTargetType(ctx, type, segments) {
1433
1631
  const effectiveType = dereferenceType(ctx, type);
1434
1632
  if (segments.length === 0) {
@@ -1450,12 +1648,33 @@ function resolvePathTargetType(ctx, type, segments) {
1450
1648
  }
1451
1649
  return { kind: "unresolvable", type: effectiveType };
1452
1650
  }
1651
+ function isNullType(type) {
1652
+ return type.kind === "primitive" && type.primitiveKind === "null";
1653
+ }
1654
+ function collectCustomConstraintCandidateTypes(ctx, type) {
1655
+ const effectiveType = dereferenceType(ctx, type);
1656
+ const candidates = [effectiveType];
1657
+ if (effectiveType.kind === "array") {
1658
+ candidates.push(...collectCustomConstraintCandidateTypes(ctx, effectiveType.items));
1659
+ }
1660
+ if (effectiveType.kind === "union") {
1661
+ const memberTypes = effectiveType.members.map((member) => dereferenceType(ctx, member));
1662
+ const nonNullMembers = memberTypes.filter((member) => !isNullType(member));
1663
+ if (nonNullMembers.length === 1 && nonNullMembers.length < memberTypes.length) {
1664
+ const [nullableMember] = nonNullMembers;
1665
+ if (nullableMember !== void 0) {
1666
+ candidates.push(...collectCustomConstraintCandidateTypes(ctx, nullableMember));
1667
+ }
1668
+ }
1669
+ }
1670
+ return candidates;
1671
+ }
1453
1672
  function formatPathTargetFieldName(fieldName, path) {
1454
1673
  return path.length === 0 ? fieldName : `${fieldName}.${path.join(".")}`;
1455
1674
  }
1456
1675
  function checkConstraintOnType(ctx, fieldName, type, constraint) {
1457
1676
  const effectiveType = dereferenceType(ctx, type);
1458
- const isNumber = effectiveType.kind === "primitive" && effectiveType.primitiveKind === "number";
1677
+ const isNumber = effectiveType.kind === "primitive" && ["number", "integer", "bigint"].includes(effectiveType.primitiveKind);
1459
1678
  const isString = effectiveType.kind === "primitive" && effectiveType.primitiveKind === "string";
1460
1679
  const isArray = effectiveType.kind === "array";
1461
1680
  const isEnum = effectiveType.kind === "enum";
@@ -1513,7 +1732,9 @@ function checkConstraintOnType(ctx, fieldName, type, constraint) {
1513
1732
  break;
1514
1733
  }
1515
1734
  case "const": {
1516
- const isPrimitiveConstType = effectiveType.kind === "primitive" && ["string", "number", "boolean", "null"].includes(effectiveType.primitiveKind) || effectiveType.kind === "enum";
1735
+ const isPrimitiveConstType = effectiveType.kind === "primitive" && ["string", "number", "integer", "bigint", "boolean", "null"].includes(
1736
+ effectiveType.primitiveKind
1737
+ ) || effectiveType.kind === "enum";
1517
1738
  if (!isPrimitiveConstType) {
1518
1739
  addTypeMismatch(
1519
1740
  ctx,
@@ -1524,7 +1745,8 @@ function checkConstraintOnType(ctx, fieldName, type, constraint) {
1524
1745
  }
1525
1746
  if (effectiveType.kind === "primitive") {
1526
1747
  const valueType = constraint.value === null ? "null" : Array.isArray(constraint.value) ? "array" : typeof constraint.value;
1527
- if (valueType !== effectiveType.primitiveKind) {
1748
+ const expectedValueType = effectiveType.primitiveKind === "integer" || effectiveType.primitiveKind === "bigint" ? "number" : effectiveType.primitiveKind;
1749
+ if (valueType !== expectedValueType) {
1528
1750
  addTypeMismatch(
1529
1751
  ctx,
1530
1752
  `Field "${fieldName}": @const value type "${valueType}" is incompatible with field type "${effectiveType.primitiveKind}"`,
@@ -1593,8 +1815,37 @@ function checkCustomConstraint(ctx, fieldName, type, constraint) {
1593
1815
  );
1594
1816
  return;
1595
1817
  }
1596
- if (registration.applicableTypes === null) return;
1597
- if (!registration.applicableTypes.includes(type.kind)) {
1818
+ const candidateTypes = collectCustomConstraintCandidateTypes(ctx, type);
1819
+ const normalizedTagName = constraint.provenance.tagName === void 0 ? void 0 : normalizeConstraintTagName(constraint.provenance.tagName.replace(/^@/, ""));
1820
+ if (normalizedTagName !== void 0) {
1821
+ const tagRegistration = ctx.extensionRegistry.findConstraintTag(normalizedTagName);
1822
+ const extensionId = getExtensionIdFromConstraintId(constraint.constraintId);
1823
+ if (extensionId !== null && tagRegistration?.extensionId === extensionId && tagRegistration.registration.constraintName === registration.constraintName && !candidateTypes.some(
1824
+ (candidateType) => tagRegistration.registration.isApplicableToType?.(candidateType) !== false
1825
+ )) {
1826
+ addTypeMismatch(
1827
+ ctx,
1828
+ `Field "${fieldName}": custom constraint "${constraint.constraintId}" is not applicable to type "${typeLabel(type)}"`,
1829
+ constraint.provenance
1830
+ );
1831
+ return;
1832
+ }
1833
+ }
1834
+ if (registration.applicableTypes === null) {
1835
+ if (!candidateTypes.some((candidateType) => registration.isApplicableToType?.(candidateType) !== false)) {
1836
+ addTypeMismatch(
1837
+ ctx,
1838
+ `Field "${fieldName}": custom constraint "${constraint.constraintId}" is not applicable to type "${typeLabel(type)}"`,
1839
+ constraint.provenance
1840
+ );
1841
+ }
1842
+ return;
1843
+ }
1844
+ const applicableTypes = registration.applicableTypes;
1845
+ const matchesApplicableType = candidateTypes.some(
1846
+ (candidateType) => applicableTypes.includes(candidateType.kind) && registration.isApplicableToType?.(candidateType) !== false
1847
+ );
1848
+ if (!matchesApplicableType) {
1598
1849
  addTypeMismatch(
1599
1850
  ctx,
1600
1851
  `Field "${fieldName}": custom constraint "${constraint.constraintId}" is not applicable to type "${typeLabel(type)}"`,
@@ -1603,7 +1854,10 @@ function checkCustomConstraint(ctx, fieldName, type, constraint) {
1603
1854
  }
1604
1855
  }
1605
1856
  function validateFieldNode(ctx, field) {
1606
- validateConstraints(ctx, field.name, field.type, field.constraints);
1857
+ validateConstraints(ctx, field.name, field.type, [
1858
+ ...collectReferencedTypeConstraints(ctx, field.type),
1859
+ ...field.constraints
1860
+ ]);
1607
1861
  if (field.type.kind === "object") {
1608
1862
  for (const prop of field.type.properties) {
1609
1863
  validateObjectProperty(ctx, field.name, prop);
@@ -1612,7 +1866,10 @@ function validateFieldNode(ctx, field) {
1612
1866
  }
1613
1867
  function validateObjectProperty(ctx, parentName, prop) {
1614
1868
  const qualifiedName = `${parentName}.${prop.name}`;
1615
- validateConstraints(ctx, qualifiedName, prop.type, prop.constraints);
1869
+ validateConstraints(ctx, qualifiedName, prop.type, [
1870
+ ...collectReferencedTypeConstraints(ctx, prop.type),
1871
+ ...prop.constraints
1872
+ ]);
1616
1873
  if (prop.type.kind === "object") {
1617
1874
  for (const nestedProp of prop.type.properties) {
1618
1875
  validateObjectProperty(ctx, qualifiedName, nestedProp);
@@ -1625,6 +1882,7 @@ function validateConstraints(ctx, name, type, constraints) {
1625
1882
  checkAllowedMembersContradiction(ctx, name, constraints);
1626
1883
  checkConstContradictions(ctx, name, constraints);
1627
1884
  checkConstraintBroadening(ctx, name, constraints);
1885
+ checkCustomConstraintSemantics(ctx, name, constraints);
1628
1886
  checkTypeApplicability(ctx, name, type, constraints);
1629
1887
  }
1630
1888
  function validateElement(ctx, element) {