@formspec/build 0.1.0-alpha.15 → 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 (52) hide show
  1. package/dist/__tests__/fixtures/edge-cases.d.ts +11 -0
  2. package/dist/__tests__/fixtures/edge-cases.d.ts.map +1 -1
  3. package/dist/__tests__/fixtures/example-numeric-extension.d.ts +20 -0
  4. package/dist/__tests__/fixtures/example-numeric-extension.d.ts.map +1 -0
  5. package/dist/__tests__/fixtures/mixed-authoring-shipping-address.d.ts +31 -0
  6. package/dist/__tests__/fixtures/mixed-authoring-shipping-address.d.ts.map +1 -0
  7. package/dist/__tests__/mixed-authoring.test.d.ts +2 -0
  8. package/dist/__tests__/mixed-authoring.test.d.ts.map +1 -0
  9. package/dist/__tests__/numeric-extension.integration.test.d.ts +2 -0
  10. package/dist/__tests__/numeric-extension.integration.test.d.ts.map +1 -0
  11. package/dist/__tests__/parity/utils.d.ts +5 -3
  12. package/dist/__tests__/parity/utils.d.ts.map +1 -1
  13. package/dist/analyzer/class-analyzer.d.ts +8 -5
  14. package/dist/analyzer/class-analyzer.d.ts.map +1 -1
  15. package/dist/analyzer/jsdoc-constraints.d.ts +3 -2
  16. package/dist/analyzer/jsdoc-constraints.d.ts.map +1 -1
  17. package/dist/analyzer/tsdoc-parser.d.ts +38 -4
  18. package/dist/analyzer/tsdoc-parser.d.ts.map +1 -1
  19. package/dist/browser.cjs +371 -21
  20. package/dist/browser.cjs.map +1 -1
  21. package/dist/browser.d.ts.map +1 -1
  22. package/dist/browser.js +371 -21
  23. package/dist/browser.js.map +1 -1
  24. package/dist/build.d.ts +67 -3
  25. package/dist/canonicalize/tsdoc-canonicalizer.d.ts.map +1 -1
  26. package/dist/cli.cjs +1159 -150
  27. package/dist/cli.cjs.map +1 -1
  28. package/dist/cli.js +1159 -150
  29. package/dist/cli.js.map +1 -1
  30. package/dist/extensions/registry.d.ts +25 -1
  31. package/dist/extensions/registry.d.ts.map +1 -1
  32. package/dist/generators/class-schema.d.ts +4 -4
  33. package/dist/generators/class-schema.d.ts.map +1 -1
  34. package/dist/generators/mixed-authoring.d.ts +45 -0
  35. package/dist/generators/mixed-authoring.d.ts.map +1 -0
  36. package/dist/index.cjs +1146 -149
  37. package/dist/index.cjs.map +1 -1
  38. package/dist/index.d.ts +2 -0
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +1145 -149
  41. package/dist/index.js.map +1 -1
  42. package/dist/internals.cjs +1156 -149
  43. package/dist/internals.cjs.map +1 -1
  44. package/dist/internals.js +1154 -147
  45. package/dist/internals.js.map +1 -1
  46. package/dist/json-schema/ir-generator.d.ts +3 -2
  47. package/dist/json-schema/ir-generator.d.ts.map +1 -1
  48. package/dist/ui-schema/ir-generator.d.ts.map +1 -1
  49. package/dist/validate/constraint-validator.d.ts.map +1 -1
  50. package/package.json +3 -3
  51. package/dist/__tests__/jsdoc-constraints.test.d.ts +0 -9
  52. package/dist/__tests__/jsdoc-constraints.test.d.ts.map +0 -1
package/dist/browser.cjs CHANGED
@@ -380,6 +380,9 @@ function generateJsonSchemaFromIR(ir, options) {
380
380
  const ctx = makeContext(options);
381
381
  for (const [name, typeDef] of Object.entries(ir.typeRegistry)) {
382
382
  ctx.defs[name] = generateTypeNode(typeDef.type, ctx);
383
+ if (typeDef.annotations && typeDef.annotations.length > 0) {
384
+ applyAnnotations(ctx.defs[name], typeDef.annotations, ctx);
385
+ }
383
386
  }
384
387
  const properties = {};
385
388
  const required = [];
@@ -391,6 +394,9 @@ function generateJsonSchemaFromIR(ir, options) {
391
394
  properties,
392
395
  ...uniqueRequired.length > 0 && { required: uniqueRequired }
393
396
  };
397
+ if (ir.annotations && ir.annotations.length > 0) {
398
+ applyAnnotations(result, ir.annotations, ctx);
399
+ }
394
400
  if (Object.keys(ctx.defs).length > 0) {
395
401
  result.$defs = ctx.defs;
396
402
  }
@@ -420,22 +426,51 @@ function collectFields(elements, properties, required, ctx) {
420
426
  }
421
427
  function generateFieldSchema(field, ctx) {
422
428
  const schema = generateTypeNode(field.type, ctx);
429
+ const itemStringSchema = schema.type === "array" && schema.items?.type === "string" ? schema.items : void 0;
423
430
  const directConstraints = [];
431
+ const itemConstraints = [];
424
432
  const pathConstraints = [];
425
433
  for (const c of field.constraints) {
426
434
  if (c.path) {
427
435
  pathConstraints.push(c);
436
+ } else if (itemStringSchema !== void 0 && isStringItemConstraint(c)) {
437
+ itemConstraints.push(c);
428
438
  } else {
429
439
  directConstraints.push(c);
430
440
  }
431
441
  }
432
442
  applyConstraints(schema, directConstraints, ctx);
433
- applyAnnotations(schema, field.annotations, ctx);
443
+ if (itemStringSchema !== void 0) {
444
+ applyConstraints(itemStringSchema, itemConstraints, ctx);
445
+ }
446
+ const rootAnnotations = [];
447
+ const itemAnnotations = [];
448
+ for (const annotation of field.annotations) {
449
+ if (itemStringSchema !== void 0 && annotation.annotationKind === "format") {
450
+ itemAnnotations.push(annotation);
451
+ } else {
452
+ rootAnnotations.push(annotation);
453
+ }
454
+ }
455
+ applyAnnotations(schema, rootAnnotations, ctx);
456
+ if (itemStringSchema !== void 0) {
457
+ applyAnnotations(itemStringSchema, itemAnnotations, ctx);
458
+ }
434
459
  if (pathConstraints.length === 0) {
435
460
  return schema;
436
461
  }
437
462
  return applyPathTargetedConstraints(schema, pathConstraints, ctx);
438
463
  }
464
+ function isStringItemConstraint(constraint) {
465
+ switch (constraint.constraintKind) {
466
+ case "minLength":
467
+ case "maxLength":
468
+ case "pattern":
469
+ return true;
470
+ default:
471
+ return false;
472
+ }
473
+ }
439
474
  function applyPathTargetedConstraints(schema, pathConstraints, ctx) {
440
475
  if (schema.type === "array" && schema.items) {
441
476
  schema.items = applyPathTargetedConstraints(schema.items, pathConstraints, ctx);
@@ -653,6 +688,9 @@ function applyConstraints(schema, constraints, ctx) {
653
688
  case "uniqueItems":
654
689
  schema.uniqueItems = constraint.value;
655
690
  break;
691
+ case "const":
692
+ schema.const = constraint.value;
693
+ break;
656
694
  case "allowedMembers":
657
695
  break;
658
696
  case "custom":
@@ -677,8 +715,14 @@ function applyAnnotations(schema, annotations, ctx) {
677
715
  case "defaultValue":
678
716
  schema.default = annotation.value;
679
717
  break;
718
+ case "format":
719
+ schema.format = annotation.value;
720
+ break;
680
721
  case "deprecated":
681
722
  schema.deprecated = true;
723
+ if (annotation.message !== void 0 && annotation.message !== "") {
724
+ schema["x-formspec-deprecation-description"] = annotation.message;
725
+ }
682
726
  break;
683
727
  case "placeholder":
684
728
  break;
@@ -710,7 +754,12 @@ function applyCustomConstraint(schema, constraint, ctx) {
710
754
  `Cannot generate JSON Schema for custom constraint "${constraint.constraintId}" without a matching extension registration`
711
755
  );
712
756
  }
713
- Object.assign(schema, registration.toJsonSchema(constraint.payload, ctx.vendorPrefix));
757
+ assignVendorPrefixedExtensionKeywords(
758
+ schema,
759
+ registration.toJsonSchema(constraint.payload, ctx.vendorPrefix),
760
+ ctx.vendorPrefix,
761
+ `custom constraint "${constraint.constraintId}"`
762
+ );
714
763
  }
715
764
  function applyCustomAnnotation(schema, annotation, ctx) {
716
765
  const registration = ctx.extensionRegistry?.findAnnotation(annotation.annotationId);
@@ -722,7 +771,22 @@ function applyCustomAnnotation(schema, annotation, ctx) {
722
771
  if (registration.toJsonSchema === void 0) {
723
772
  return;
724
773
  }
725
- Object.assign(schema, registration.toJsonSchema(annotation.value, ctx.vendorPrefix));
774
+ assignVendorPrefixedExtensionKeywords(
775
+ schema,
776
+ registration.toJsonSchema(annotation.value, ctx.vendorPrefix),
777
+ ctx.vendorPrefix,
778
+ `custom annotation "${annotation.annotationId}"`
779
+ );
780
+ }
781
+ function assignVendorPrefixedExtensionKeywords(schema, extensionSchema, vendorPrefix, source) {
782
+ for (const [key, value] of Object.entries(extensionSchema)) {
783
+ if (!key.startsWith(`${vendorPrefix}-`)) {
784
+ throw new Error(
785
+ `Cannot apply ${source}: extension hooks may only emit "${vendorPrefix}-*" JSON Schema keywords`
786
+ );
787
+ }
788
+ schema[key] = value;
789
+ }
726
790
  }
727
791
 
728
792
  // src/json-schema/generator.ts
@@ -866,25 +930,31 @@ function createShowRule(fieldName, value) {
866
930
  }
867
931
  };
868
932
  }
933
+ function flattenConditionSchema(scope, schema) {
934
+ if (schema.allOf === void 0) {
935
+ if (scope === "#") {
936
+ return [schema];
937
+ }
938
+ const fieldName = scope.replace("#/properties/", "");
939
+ return [
940
+ {
941
+ properties: {
942
+ [fieldName]: schema
943
+ }
944
+ }
945
+ ];
946
+ }
947
+ return schema.allOf.flatMap((member) => flattenConditionSchema(scope, member));
948
+ }
869
949
  function combineRules(parentRule, childRule) {
870
- const parentCondition = parentRule.condition;
871
- const childCondition = childRule.condition;
872
950
  return {
873
951
  effect: "SHOW",
874
952
  condition: {
875
953
  scope: "#",
876
954
  schema: {
877
955
  allOf: [
878
- {
879
- properties: {
880
- [parentCondition.scope.replace("#/properties/", "")]: parentCondition.schema
881
- }
882
- },
883
- {
884
- properties: {
885
- [childCondition.scope.replace("#/properties/", "")]: childCondition.schema
886
- }
887
- }
956
+ ...flattenConditionSchema(parentRule.condition.scope, parentRule.condition.schema),
957
+ ...flattenConditionSchema(childRule.condition.scope, childRule.condition.schema)
888
958
  ]
889
959
  }
890
960
  }
@@ -892,10 +962,14 @@ function combineRules(parentRule, childRule) {
892
962
  }
893
963
  function fieldNodeToControl(field, parentRule) {
894
964
  const displayNameAnnotation = field.annotations.find((a) => a.annotationKind === "displayName");
965
+ const placeholderAnnotation = field.annotations.find((a) => a.annotationKind === "placeholder");
895
966
  const control = {
896
967
  type: "Control",
897
968
  scope: fieldToScope(field.name),
898
969
  ...displayNameAnnotation !== void 0 && { label: displayNameAnnotation.value },
970
+ ...placeholderAnnotation !== void 0 && {
971
+ options: { placeholder: placeholderAnnotation.value }
972
+ },
899
973
  ...parentRule !== void 0 && { rule: parentRule }
900
974
  };
901
975
  return control;
@@ -961,7 +1035,10 @@ function getSchemaExtension(schema, key) {
961
1035
  // src/extensions/registry.ts
962
1036
  function createExtensionRegistry(extensions) {
963
1037
  const typeMap = /* @__PURE__ */ new Map();
1038
+ const typeNameMap = /* @__PURE__ */ new Map();
964
1039
  const constraintMap = /* @__PURE__ */ new Map();
1040
+ const constraintTagMap = /* @__PURE__ */ new Map();
1041
+ const builtinBroadeningMap = /* @__PURE__ */ new Map();
965
1042
  const annotationMap = /* @__PURE__ */ new Map();
966
1043
  for (const ext of extensions) {
967
1044
  if (ext.types !== void 0) {
@@ -971,6 +1048,27 @@ function createExtensionRegistry(extensions) {
971
1048
  throw new Error(`Duplicate custom type ID: "${qualifiedId}"`);
972
1049
  }
973
1050
  typeMap.set(qualifiedId, type);
1051
+ for (const sourceTypeName of type.tsTypeNames ?? [type.typeName]) {
1052
+ if (typeNameMap.has(sourceTypeName)) {
1053
+ throw new Error(`Duplicate custom type source name: "${sourceTypeName}"`);
1054
+ }
1055
+ typeNameMap.set(sourceTypeName, {
1056
+ extensionId: ext.extensionId,
1057
+ registration: type
1058
+ });
1059
+ }
1060
+ if (type.builtinConstraintBroadenings !== void 0) {
1061
+ for (const broadening of type.builtinConstraintBroadenings) {
1062
+ const key = `${qualifiedId}:${broadening.tagName}`;
1063
+ if (builtinBroadeningMap.has(key)) {
1064
+ throw new Error(`Duplicate built-in constraint broadening: "${key}"`);
1065
+ }
1066
+ builtinBroadeningMap.set(key, {
1067
+ extensionId: ext.extensionId,
1068
+ registration: broadening
1069
+ });
1070
+ }
1071
+ }
974
1072
  }
975
1073
  }
976
1074
  if (ext.constraints !== void 0) {
@@ -982,6 +1080,17 @@ function createExtensionRegistry(extensions) {
982
1080
  constraintMap.set(qualifiedId, constraint);
983
1081
  }
984
1082
  }
1083
+ if (ext.constraintTags !== void 0) {
1084
+ for (const tag of ext.constraintTags) {
1085
+ if (constraintTagMap.has(tag.tagName)) {
1086
+ throw new Error(`Duplicate custom constraint tag: "@${tag.tagName}"`);
1087
+ }
1088
+ constraintTagMap.set(tag.tagName, {
1089
+ extensionId: ext.extensionId,
1090
+ registration: tag
1091
+ });
1092
+ }
1093
+ }
985
1094
  if (ext.annotations !== void 0) {
986
1095
  for (const annotation of ext.annotations) {
987
1096
  const qualifiedId = `${ext.extensionId}/${annotation.annotationName}`;
@@ -995,7 +1104,10 @@ function createExtensionRegistry(extensions) {
995
1104
  return {
996
1105
  extensions,
997
1106
  findType: (typeId) => typeMap.get(typeId),
1107
+ findTypeByName: (typeName) => typeNameMap.get(typeName),
998
1108
  findConstraint: (constraintId) => constraintMap.get(constraintId),
1109
+ findConstraintTag: (tagName) => constraintTagMap.get(tagName),
1110
+ findBuiltinConstraintBroadening: (typeId, tagName) => builtinBroadeningMap.get(`${typeId}:${tagName}`),
999
1111
  findAnnotation: (annotationId) => annotationMap.get(annotationId)
1000
1112
  };
1001
1113
  }
@@ -1063,6 +1175,7 @@ var jsonSchema7Schema = import_zod3.z.lazy(
1063
1175
  );
1064
1176
 
1065
1177
  // src/validate/constraint-validator.ts
1178
+ var import_core3 = require("@formspec/core");
1066
1179
  function addContradiction(ctx, message, primary, related) {
1067
1180
  ctx.diagnostics.push({
1068
1181
  code: "CONTRADICTING_CONSTRAINTS",
@@ -1090,6 +1203,15 @@ function addUnknownExtension(ctx, message, primary) {
1090
1203
  relatedLocations: []
1091
1204
  });
1092
1205
  }
1206
+ function addUnknownPathTarget(ctx, message, primary) {
1207
+ ctx.diagnostics.push({
1208
+ code: "UNKNOWN_PATH_TARGET",
1209
+ message,
1210
+ severity: "error",
1211
+ primaryLocation: primary,
1212
+ relatedLocations: []
1213
+ });
1214
+ }
1093
1215
  function addConstraintBroadening(ctx, message, primary, related) {
1094
1216
  ctx.diagnostics.push({
1095
1217
  code: "CONSTRAINT_BROADENING",
@@ -1099,6 +1221,13 @@ function addConstraintBroadening(ctx, message, primary, related) {
1099
1221
  relatedLocations: [related]
1100
1222
  });
1101
1223
  }
1224
+ function getExtensionIdFromConstraintId(constraintId) {
1225
+ const separator = constraintId.lastIndexOf("/");
1226
+ if (separator <= 0) {
1227
+ return null;
1228
+ }
1229
+ return constraintId.slice(0, separator);
1230
+ }
1102
1231
  function findNumeric(constraints, constraintKind) {
1103
1232
  return constraints.find((c) => c.constraintKind === constraintKind);
1104
1233
  }
@@ -1110,6 +1239,45 @@ function findAllowedMembers(constraints) {
1110
1239
  (c) => c.constraintKind === "allowedMembers"
1111
1240
  );
1112
1241
  }
1242
+ function findConstConstraints(constraints) {
1243
+ return constraints.filter(
1244
+ (c) => c.constraintKind === "const"
1245
+ );
1246
+ }
1247
+ function jsonValueEquals(left, right) {
1248
+ if (left === right) {
1249
+ return true;
1250
+ }
1251
+ if (Array.isArray(left) || Array.isArray(right)) {
1252
+ if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) {
1253
+ return false;
1254
+ }
1255
+ return left.every((item, index) => jsonValueEquals(item, right[index]));
1256
+ }
1257
+ if (isJsonObject(left) || isJsonObject(right)) {
1258
+ if (!isJsonObject(left) || !isJsonObject(right)) {
1259
+ return false;
1260
+ }
1261
+ const leftKeys = Object.keys(left).sort();
1262
+ const rightKeys = Object.keys(right).sort();
1263
+ if (leftKeys.length !== rightKeys.length) {
1264
+ return false;
1265
+ }
1266
+ return leftKeys.every((key, index) => {
1267
+ const rightKey = rightKeys[index];
1268
+ if (rightKey !== key) {
1269
+ return false;
1270
+ }
1271
+ const leftValue = left[key];
1272
+ const rightValue = right[rightKey];
1273
+ return leftValue !== void 0 && rightValue !== void 0 && jsonValueEquals(leftValue, rightValue);
1274
+ });
1275
+ }
1276
+ return false;
1277
+ }
1278
+ function isJsonObject(value) {
1279
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1280
+ }
1113
1281
  function isOrderedBoundConstraint(constraint) {
1114
1282
  return constraint.constraintKind === "minimum" || constraint.constraintKind === "exclusiveMinimum" || constraint.constraintKind === "minLength" || constraint.constraintKind === "minItems" || constraint.constraintKind === "maximum" || constraint.constraintKind === "exclusiveMaximum" || constraint.constraintKind === "maxLength" || constraint.constraintKind === "maxItems";
1115
1283
  }
@@ -1230,6 +1398,112 @@ function checkConstraintBroadening(ctx, fieldName, constraints) {
1230
1398
  strongestByKey.set(key, constraint);
1231
1399
  }
1232
1400
  }
1401
+ function compareCustomConstraintStrength(current, previous) {
1402
+ const order = current.comparePayloads(current.constraint.payload, previous.constraint.payload);
1403
+ const equalPayloadTiebreaker = order === 0 ? compareSemanticInclusivity(current.role.inclusive, previous.role.inclusive) : order;
1404
+ switch (current.role.bound) {
1405
+ case "lower":
1406
+ return equalPayloadTiebreaker;
1407
+ case "upper":
1408
+ return equalPayloadTiebreaker === 0 ? 0 : -equalPayloadTiebreaker;
1409
+ case "exact":
1410
+ return order === 0 ? 0 : Number.NaN;
1411
+ default: {
1412
+ const _exhaustive = current.role.bound;
1413
+ return _exhaustive;
1414
+ }
1415
+ }
1416
+ }
1417
+ function compareSemanticInclusivity(currentInclusive, previousInclusive) {
1418
+ if (currentInclusive === previousInclusive) {
1419
+ return 0;
1420
+ }
1421
+ return currentInclusive ? -1 : 1;
1422
+ }
1423
+ function customConstraintsContradict(lower, upper) {
1424
+ const order = lower.comparePayloads(lower.constraint.payload, upper.constraint.payload);
1425
+ if (order > 0) {
1426
+ return true;
1427
+ }
1428
+ if (order < 0) {
1429
+ return false;
1430
+ }
1431
+ return !lower.role.inclusive || !upper.role.inclusive;
1432
+ }
1433
+ function describeCustomConstraintTag(constraint) {
1434
+ return constraint.provenance.tagName ?? constraint.constraintId;
1435
+ }
1436
+ function checkCustomConstraintSemantics(ctx, fieldName, constraints) {
1437
+ if (ctx.extensionRegistry === void 0) {
1438
+ return;
1439
+ }
1440
+ const strongestByKey = /* @__PURE__ */ new Map();
1441
+ const lowerByFamily = /* @__PURE__ */ new Map();
1442
+ const upperByFamily = /* @__PURE__ */ new Map();
1443
+ for (const constraint of constraints) {
1444
+ if (constraint.constraintKind !== "custom") {
1445
+ continue;
1446
+ }
1447
+ const registration = ctx.extensionRegistry.findConstraint(constraint.constraintId);
1448
+ if (registration?.comparePayloads === void 0 || registration.semanticRole === void 0) {
1449
+ continue;
1450
+ }
1451
+ const entry = {
1452
+ constraint,
1453
+ comparePayloads: registration.comparePayloads,
1454
+ role: registration.semanticRole
1455
+ };
1456
+ const familyKey = `${registration.semanticRole.family}:${pathKey(constraint)}`;
1457
+ const boundKey = `${familyKey}:${registration.semanticRole.bound}`;
1458
+ const previous = strongestByKey.get(boundKey);
1459
+ if (previous !== void 0) {
1460
+ const strength = compareCustomConstraintStrength(entry, previous);
1461
+ if (Number.isNaN(strength)) {
1462
+ addContradiction(
1463
+ ctx,
1464
+ `Field "${formatPathTargetFieldName(fieldName, constraint.path?.segments ?? [])}": ${describeCustomConstraintTag(constraint)} conflicts with ${describeCustomConstraintTag(previous.constraint)}`,
1465
+ constraint.provenance,
1466
+ previous.constraint.provenance
1467
+ );
1468
+ continue;
1469
+ }
1470
+ if (strength < 0) {
1471
+ addConstraintBroadening(
1472
+ ctx,
1473
+ `Field "${formatPathTargetFieldName(fieldName, constraint.path?.segments ?? [])}": ${describeCustomConstraintTag(constraint)} is broader than earlier ${describeCustomConstraintTag(previous.constraint)}. Constraints can only narrow.`,
1474
+ constraint.provenance,
1475
+ previous.constraint.provenance
1476
+ );
1477
+ continue;
1478
+ }
1479
+ if (strength > 0) {
1480
+ strongestByKey.set(boundKey, entry);
1481
+ }
1482
+ } else {
1483
+ strongestByKey.set(boundKey, entry);
1484
+ }
1485
+ if (registration.semanticRole.bound === "lower") {
1486
+ lowerByFamily.set(familyKey, strongestByKey.get(boundKey) ?? entry);
1487
+ } else if (registration.semanticRole.bound === "upper") {
1488
+ upperByFamily.set(familyKey, strongestByKey.get(boundKey) ?? entry);
1489
+ }
1490
+ }
1491
+ for (const [familyKey, lower] of lowerByFamily) {
1492
+ const upper = upperByFamily.get(familyKey);
1493
+ if (upper === void 0) {
1494
+ continue;
1495
+ }
1496
+ if (!customConstraintsContradict(lower, upper)) {
1497
+ continue;
1498
+ }
1499
+ addContradiction(
1500
+ ctx,
1501
+ `Field "${formatPathTargetFieldName(fieldName, lower.constraint.path?.segments ?? [])}": ${describeCustomConstraintTag(lower.constraint)} contradicts ${describeCustomConstraintTag(upper.constraint)}`,
1502
+ lower.constraint.provenance,
1503
+ upper.constraint.provenance
1504
+ );
1505
+ }
1506
+ }
1233
1507
  function checkNumericContradictions(ctx, fieldName, constraints) {
1234
1508
  const min = findNumeric(constraints, "minimum");
1235
1509
  const max = findNumeric(constraints, "maximum");
@@ -1316,6 +1590,25 @@ function checkAllowedMembersContradiction(ctx, fieldName, constraints) {
1316
1590
  }
1317
1591
  }
1318
1592
  }
1593
+ function checkConstContradictions(ctx, fieldName, constraints) {
1594
+ const constConstraints = findConstConstraints(constraints);
1595
+ if (constConstraints.length < 2) return;
1596
+ const first = constConstraints[0];
1597
+ if (first === void 0) return;
1598
+ for (let i = 1; i < constConstraints.length; i++) {
1599
+ const current = constConstraints[i];
1600
+ if (current === void 0) continue;
1601
+ if (jsonValueEquals(first.value, current.value)) {
1602
+ continue;
1603
+ }
1604
+ addContradiction(
1605
+ ctx,
1606
+ `Field "${fieldName}": conflicting @const constraints require both ${JSON.stringify(first.value)} and ${JSON.stringify(current.value)}`,
1607
+ first.provenance,
1608
+ current.provenance
1609
+ );
1610
+ }
1611
+ }
1319
1612
  function typeLabel(type) {
1320
1613
  switch (type.kind) {
1321
1614
  case "primitive":
@@ -1388,6 +1681,8 @@ function checkConstraintOnType(ctx, fieldName, type, constraint) {
1388
1681
  const isString = effectiveType.kind === "primitive" && effectiveType.primitiveKind === "string";
1389
1682
  const isArray = effectiveType.kind === "array";
1390
1683
  const isEnum = effectiveType.kind === "enum";
1684
+ const arrayItemType = effectiveType.kind === "array" ? dereferenceType(ctx, effectiveType.items) : void 0;
1685
+ const isStringArray = arrayItemType?.kind === "primitive" && arrayItemType.primitiveKind === "string";
1391
1686
  const label = typeLabel(effectiveType);
1392
1687
  const ck = constraint.constraintKind;
1393
1688
  switch (ck) {
@@ -1408,10 +1703,10 @@ function checkConstraintOnType(ctx, fieldName, type, constraint) {
1408
1703
  case "minLength":
1409
1704
  case "maxLength":
1410
1705
  case "pattern": {
1411
- if (!isString) {
1706
+ if (!isString && !isStringArray) {
1412
1707
  addTypeMismatch(
1413
1708
  ctx,
1414
- `Field "${fieldName}": constraint "${ck}" is only valid on string fields, but field type is "${label}"`,
1709
+ `Field "${fieldName}": constraint "${ck}" is only valid on string fields or string array items, but field type is "${label}"`,
1415
1710
  constraint.provenance
1416
1711
  );
1417
1712
  }
@@ -1439,6 +1734,37 @@ function checkConstraintOnType(ctx, fieldName, type, constraint) {
1439
1734
  }
1440
1735
  break;
1441
1736
  }
1737
+ case "const": {
1738
+ const isPrimitiveConstType = effectiveType.kind === "primitive" && ["string", "number", "boolean", "null"].includes(effectiveType.primitiveKind) || effectiveType.kind === "enum";
1739
+ if (!isPrimitiveConstType) {
1740
+ addTypeMismatch(
1741
+ ctx,
1742
+ `Field "${fieldName}": constraint "const" is only valid on primitive or enum fields, but field type is "${label}"`,
1743
+ constraint.provenance
1744
+ );
1745
+ break;
1746
+ }
1747
+ if (effectiveType.kind === "primitive") {
1748
+ const valueType = constraint.value === null ? "null" : Array.isArray(constraint.value) ? "array" : typeof constraint.value;
1749
+ if (valueType !== effectiveType.primitiveKind) {
1750
+ addTypeMismatch(
1751
+ ctx,
1752
+ `Field "${fieldName}": @const value type "${valueType}" is incompatible with field type "${effectiveType.primitiveKind}"`,
1753
+ constraint.provenance
1754
+ );
1755
+ }
1756
+ break;
1757
+ }
1758
+ const memberValues = effectiveType.members.map((member) => member.value);
1759
+ if (!memberValues.some((member) => jsonValueEquals(member, constraint.value))) {
1760
+ addTypeMismatch(
1761
+ ctx,
1762
+ `Field "${fieldName}": @const value ${JSON.stringify(constraint.value)} is not one of the enum members`,
1763
+ constraint.provenance
1764
+ );
1765
+ }
1766
+ break;
1767
+ }
1442
1768
  case "custom": {
1443
1769
  checkCustomConstraint(ctx, fieldName, effectiveType, constraint);
1444
1770
  break;
@@ -1457,9 +1783,9 @@ function checkTypeApplicability(ctx, fieldName, type, constraints) {
1457
1783
  const resolution = resolvePathTargetType(ctx, type, constraint.path.segments);
1458
1784
  const targetFieldName = formatPathTargetFieldName(fieldName, constraint.path.segments);
1459
1785
  if (resolution.kind === "missing-property") {
1460
- addTypeMismatch(
1786
+ addUnknownPathTarget(
1461
1787
  ctx,
1462
- `Field "${fieldName}": path-targeted constraint "${constraint.constraintKind}" references unknown path segment "${resolution.segment}"`,
1788
+ `Field "${targetFieldName}": path-targeted constraint "${constraint.constraintKind}" references unknown path segment "${resolution.segment}"`,
1463
1789
  constraint.provenance
1464
1790
  );
1465
1791
  continue;
@@ -1489,8 +1815,30 @@ function checkCustomConstraint(ctx, fieldName, type, constraint) {
1489
1815
  );
1490
1816
  return;
1491
1817
  }
1492
- if (registration.applicableTypes === null) return;
1493
- if (!registration.applicableTypes.includes(type.kind)) {
1818
+ const normalizedTagName = constraint.provenance.tagName === void 0 ? void 0 : (0, import_core3.normalizeConstraintTagName)(constraint.provenance.tagName.replace(/^@/, ""));
1819
+ if (normalizedTagName !== void 0) {
1820
+ const tagRegistration = ctx.extensionRegistry.findConstraintTag(normalizedTagName);
1821
+ const extensionId = getExtensionIdFromConstraintId(constraint.constraintId);
1822
+ if (extensionId !== null && tagRegistration?.extensionId === extensionId && tagRegistration.registration.constraintName === registration.constraintName && tagRegistration.registration.isApplicableToType?.(type) === false) {
1823
+ addTypeMismatch(
1824
+ ctx,
1825
+ `Field "${fieldName}": custom constraint "${constraint.constraintId}" is not applicable to type "${typeLabel(type)}"`,
1826
+ constraint.provenance
1827
+ );
1828
+ return;
1829
+ }
1830
+ }
1831
+ if (registration.applicableTypes === null) {
1832
+ if (registration.isApplicableToType?.(type) === false) {
1833
+ addTypeMismatch(
1834
+ ctx,
1835
+ `Field "${fieldName}": custom constraint "${constraint.constraintId}" is not applicable to type "${typeLabel(type)}"`,
1836
+ constraint.provenance
1837
+ );
1838
+ }
1839
+ return;
1840
+ }
1841
+ if (!registration.applicableTypes.includes(type.kind) || registration.isApplicableToType?.(type) === false) {
1494
1842
  addTypeMismatch(
1495
1843
  ctx,
1496
1844
  `Field "${fieldName}": custom constraint "${constraint.constraintId}" is not applicable to type "${typeLabel(type)}"`,
@@ -1519,7 +1867,9 @@ function validateConstraints(ctx, name, type, constraints) {
1519
1867
  checkNumericContradictions(ctx, name, constraints);
1520
1868
  checkLengthContradictions(ctx, name, constraints);
1521
1869
  checkAllowedMembersContradiction(ctx, name, constraints);
1870
+ checkConstContradictions(ctx, name, constraints);
1522
1871
  checkConstraintBroadening(ctx, name, constraints);
1872
+ checkCustomConstraintSemantics(ctx, name, constraints);
1523
1873
  checkTypeApplicability(ctx, name, type, constraints);
1524
1874
  }
1525
1875
  function validateElement(ctx, element) {