@formspec/build 0.1.0-alpha.13 → 0.1.0-alpha.15

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 (71) hide show
  1. package/README.md +20 -20
  2. package/dist/__tests__/alias-chain-propagation.test.d.ts +9 -0
  3. package/dist/__tests__/alias-chain-propagation.test.d.ts.map +1 -0
  4. package/dist/__tests__/extension-runtime.integration.test.d.ts +2 -0
  5. package/dist/__tests__/extension-runtime.integration.test.d.ts.map +1 -0
  6. package/dist/__tests__/fixtures/alias-chains.d.ts +37 -0
  7. package/dist/__tests__/fixtures/alias-chains.d.ts.map +1 -0
  8. package/dist/__tests__/fixtures/edge-cases.d.ts +11 -0
  9. package/dist/__tests__/fixtures/edge-cases.d.ts.map +1 -1
  10. package/dist/__tests__/fixtures/example-a-builtins.d.ts +13 -13
  11. package/dist/__tests__/fixtures/example-interface-types.d.ts +33 -33
  12. package/dist/__tests__/fixtures/example-interface-types.d.ts.map +1 -1
  13. package/dist/__tests__/jsdoc-constraints.test.d.ts +4 -5
  14. package/dist/__tests__/jsdoc-constraints.test.d.ts.map +1 -1
  15. package/dist/__tests__/json-utils.test.d.ts +5 -0
  16. package/dist/__tests__/json-utils.test.d.ts.map +1 -0
  17. package/dist/__tests__/parity/fixtures/plan-status/chain-dsl.d.ts +19 -0
  18. package/dist/__tests__/parity/fixtures/plan-status/chain-dsl.d.ts.map +1 -0
  19. package/dist/__tests__/parity/fixtures/plan-status/expected-ir.d.ts +6 -0
  20. package/dist/__tests__/parity/fixtures/plan-status/expected-ir.d.ts.map +1 -0
  21. package/dist/__tests__/parity/fixtures/plan-status/tsdoc.d.ts +17 -0
  22. package/dist/__tests__/parity/fixtures/plan-status/tsdoc.d.ts.map +1 -0
  23. package/dist/__tests__/parity/fixtures/usd-cents/chain-dsl.d.ts +9 -0
  24. package/dist/__tests__/parity/fixtures/usd-cents/chain-dsl.d.ts.map +1 -0
  25. package/dist/__tests__/parity/fixtures/usd-cents/expected-ir.d.ts +6 -0
  26. package/dist/__tests__/parity/fixtures/usd-cents/expected-ir.d.ts.map +1 -0
  27. package/dist/__tests__/parity/fixtures/usd-cents/tsdoc.d.ts +19 -0
  28. package/dist/__tests__/parity/fixtures/usd-cents/tsdoc.d.ts.map +1 -0
  29. package/dist/__tests__/parity/utils.d.ts +6 -1
  30. package/dist/__tests__/parity/utils.d.ts.map +1 -1
  31. package/dist/__tests__/path-target-parser.test.d.ts +9 -0
  32. package/dist/__tests__/path-target-parser.test.d.ts.map +1 -0
  33. package/dist/analyzer/class-analyzer.d.ts +1 -1
  34. package/dist/analyzer/class-analyzer.d.ts.map +1 -1
  35. package/dist/analyzer/jsdoc-constraints.d.ts +8 -52
  36. package/dist/analyzer/jsdoc-constraints.d.ts.map +1 -1
  37. package/dist/analyzer/json-utils.d.ts +22 -0
  38. package/dist/analyzer/json-utils.d.ts.map +1 -0
  39. package/dist/analyzer/tsdoc-parser.d.ts +24 -12
  40. package/dist/analyzer/tsdoc-parser.d.ts.map +1 -1
  41. package/dist/browser.cjs +452 -94
  42. package/dist/browser.cjs.map +1 -1
  43. package/dist/browser.d.ts +15 -2
  44. package/dist/browser.d.ts.map +1 -1
  45. package/dist/browser.js +450 -94
  46. package/dist/browser.js.map +1 -1
  47. package/dist/build.d.ts +132 -5
  48. package/dist/canonicalize/tsdoc-canonicalizer.d.ts +3 -3
  49. package/dist/cli.cjs +406 -104
  50. package/dist/cli.cjs.map +1 -1
  51. package/dist/cli.js +407 -104
  52. package/dist/cli.js.map +1 -1
  53. package/dist/index.cjs +386 -102
  54. package/dist/index.cjs.map +1 -1
  55. package/dist/index.d.ts +20 -3
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +386 -104
  58. package/dist/index.js.map +1 -1
  59. package/dist/internals.cjs +597 -172
  60. package/dist/internals.cjs.map +1 -1
  61. package/dist/internals.js +597 -172
  62. package/dist/internals.js.map +1 -1
  63. package/dist/json-schema/generator.d.ts +8 -2
  64. package/dist/json-schema/generator.d.ts.map +1 -1
  65. package/dist/json-schema/ir-generator.d.ts +25 -2
  66. package/dist/json-schema/ir-generator.d.ts.map +1 -1
  67. package/dist/json-schema/types.d.ts +1 -1
  68. package/dist/json-schema/types.d.ts.map +1 -1
  69. package/dist/validate/constraint-validator.d.ts +3 -7
  70. package/dist/validate/constraint-validator.d.ts.map +1 -1
  71. package/package.json +3 -3
@@ -228,7 +228,7 @@ function canonicalizeArrayField(field) {
228
228
  const itemsType = {
229
229
  kind: "object",
230
230
  properties: itemProperties,
231
- additionalProperties: false
231
+ additionalProperties: true
232
232
  };
233
233
  const type = { kind: "array", items: itemsType };
234
234
  const constraints = [];
@@ -263,7 +263,7 @@ function canonicalizeObjectField(field) {
263
263
  const type = {
264
264
  kind: "object",
265
265
  properties,
266
- additionalProperties: false
266
+ additionalProperties: true
267
267
  };
268
268
  return buildFieldNode(field.name, type, field.required, buildAnnotations(field.label));
269
269
  }
@@ -511,26 +511,36 @@ var ts4 = __toESM(require("typescript"), 1);
511
511
 
512
512
  // src/analyzer/jsdoc-constraints.ts
513
513
  var ts3 = __toESM(require("typescript"), 1);
514
- var import_core4 = require("@formspec/core");
515
514
 
516
515
  // src/analyzer/tsdoc-parser.ts
517
516
  var ts2 = __toESM(require("typescript"), 1);
518
517
  var import_tsdoc = require("@microsoft/tsdoc");
519
518
  var import_core3 = require("@formspec/core");
519
+
520
+ // src/analyzer/json-utils.ts
521
+ function tryParseJson(text) {
522
+ try {
523
+ return JSON.parse(text);
524
+ } catch {
525
+ return null;
526
+ }
527
+ }
528
+
529
+ // src/analyzer/tsdoc-parser.ts
520
530
  var NUMERIC_CONSTRAINT_MAP = {
521
- Minimum: "minimum",
522
- Maximum: "maximum",
523
- ExclusiveMinimum: "exclusiveMinimum",
524
- ExclusiveMaximum: "exclusiveMaximum"
531
+ minimum: "minimum",
532
+ maximum: "maximum",
533
+ exclusiveMinimum: "exclusiveMinimum",
534
+ exclusiveMaximum: "exclusiveMaximum",
535
+ multipleOf: "multipleOf"
525
536
  };
526
537
  var LENGTH_CONSTRAINT_MAP = {
527
- MinLength: "minLength",
528
- MaxLength: "maxLength"
538
+ minLength: "minLength",
539
+ maxLength: "maxLength",
540
+ minItems: "minItems",
541
+ maxItems: "maxItems"
529
542
  };
530
- var TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["Pattern", "EnumOptions"]);
531
- function isBuiltinConstraintName(tagName) {
532
- return tagName in import_core3.BUILTIN_CONSTRAINT_DEFINITIONS;
533
- }
543
+ var TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["pattern", "enumOptions"]);
534
544
  function createFormSpecTSDocConfig() {
535
545
  const config = new import_tsdoc.TSDocConfiguration();
536
546
  for (const tagName of Object.keys(import_core3.BUILTIN_CONSTRAINT_DEFINITIONS)) {
@@ -542,6 +552,15 @@ function createFormSpecTSDocConfig() {
542
552
  })
543
553
  );
544
554
  }
555
+ for (const tagName of ["displayName", "description"]) {
556
+ config.addTagDefinition(
557
+ new import_tsdoc.TSDocTagDefinition({
558
+ tagName: "@" + tagName,
559
+ syntaxKind: import_tsdoc.TSDocTagSyntaxKind.BlockTag,
560
+ allowMultiple: true
561
+ })
562
+ );
563
+ }
545
564
  return config;
546
565
  }
547
566
  var sharedParser;
@@ -570,7 +589,28 @@ function parseTSDocTags(node, file = "") {
570
589
  );
571
590
  const docComment = parserContext.docComment;
572
591
  for (const block of docComment.customBlocks) {
573
- const tagName = block.blockTag.tagName.substring(1);
592
+ const tagName = (0, import_core3.normalizeConstraintTagName)(block.blockTag.tagName.substring(1));
593
+ if (tagName === "displayName" || tagName === "description") {
594
+ const text2 = extractBlockText(block).trim();
595
+ if (text2 === "") continue;
596
+ const provenance2 = provenanceForComment(range, sourceFile, file, tagName);
597
+ if (tagName === "displayName") {
598
+ annotations.push({
599
+ kind: "annotation",
600
+ annotationKind: "displayName",
601
+ value: text2,
602
+ provenance: provenance2
603
+ });
604
+ } else {
605
+ annotations.push({
606
+ kind: "annotation",
607
+ annotationKind: "description",
608
+ value: text2,
609
+ provenance: provenance2
610
+ });
611
+ }
612
+ continue;
613
+ }
574
614
  if (TAGS_REQUIRING_RAW_TEXT.has(tagName)) continue;
575
615
  const text = extractBlockText(block).trim();
576
616
  if (text === "") continue;
@@ -591,7 +631,7 @@ function parseTSDocTags(node, file = "") {
591
631
  }
592
632
  const jsDocTagsAll = ts2.getJSDocTags(node);
593
633
  for (const tag of jsDocTagsAll) {
594
- const tagName = tag.tagName.text;
634
+ const tagName = (0, import_core3.normalizeConstraintTagName)(tag.tagName.text);
595
635
  if (!TAGS_REQUIRING_RAW_TEXT.has(tagName)) continue;
596
636
  const commentText = getTagCommentText(tag);
597
637
  if (commentText === void 0 || commentText.trim() === "") continue;
@@ -602,43 +642,17 @@ function parseTSDocTags(node, file = "") {
602
642
  constraints.push(constraintNode);
603
643
  }
604
644
  }
605
- let displayName;
606
- let description;
607
- let displayNameTag;
608
- let descriptionTag;
609
- for (const tag of jsDocTagsAll) {
610
- const tagName = tag.tagName.text;
611
- const commentText = getTagCommentText(tag);
612
- if (commentText === void 0 || commentText.trim() === "") {
613
- continue;
614
- }
615
- const trimmed = commentText.trim();
616
- if (tagName === "Field_displayName") {
617
- displayName = trimmed;
618
- displayNameTag = tag;
619
- } else if (tagName === "Field_description") {
620
- description = trimmed;
621
- descriptionTag = tag;
622
- }
623
- }
624
- if (displayName !== void 0 && displayNameTag) {
625
- annotations.push({
626
- kind: "annotation",
627
- annotationKind: "displayName",
628
- value: displayName,
629
- provenance: provenanceForJSDocTag(displayNameTag, file)
630
- });
631
- }
632
- if (description !== void 0 && descriptionTag) {
633
- annotations.push({
634
- kind: "annotation",
635
- annotationKind: "description",
636
- value: description,
637
- provenance: provenanceForJSDocTag(descriptionTag, file)
638
- });
639
- }
640
645
  return { constraints, annotations };
641
646
  }
647
+ function extractPathTarget(text) {
648
+ const trimmed = text.trimStart();
649
+ const match = /^:([a-zA-Z_]\w*)\s+([\s\S]*)$/.exec(trimmed);
650
+ if (!match?.[1] || !match[2]) return null;
651
+ return {
652
+ path: { segments: [match[1]] },
653
+ remainingText: match[2]
654
+ };
655
+ }
642
656
  function extractBlockText(block) {
643
657
  return extractPlainText(block.content);
644
658
  }
@@ -658,12 +672,15 @@ function extractPlainText(node) {
658
672
  return result;
659
673
  }
660
674
  function parseConstraintValue(tagName, text, provenance) {
661
- if (!isBuiltinConstraintName(tagName)) {
675
+ if (!(0, import_core3.isBuiltinConstraintName)(tagName)) {
662
676
  return null;
663
677
  }
678
+ const pathResult = extractPathTarget(text);
679
+ const effectiveText = pathResult ? pathResult.remainingText : text;
680
+ const path2 = pathResult?.path;
664
681
  const expectedType = import_core3.BUILTIN_CONSTRAINT_DEFINITIONS[tagName];
665
682
  if (expectedType === "number") {
666
- const value = Number(text);
683
+ const value = Number(effectiveText);
667
684
  if (Number.isNaN(value)) {
668
685
  return null;
669
686
  }
@@ -673,6 +690,7 @@ function parseConstraintValue(tagName, text, provenance) {
673
690
  kind: "constraint",
674
691
  constraintKind: numericKind,
675
692
  value,
693
+ ...path2 && { path: path2 },
676
694
  provenance
677
695
  };
678
696
  }
@@ -682,42 +700,41 @@ function parseConstraintValue(tagName, text, provenance) {
682
700
  kind: "constraint",
683
701
  constraintKind: lengthKind,
684
702
  value,
703
+ ...path2 && { path: path2 },
685
704
  provenance
686
705
  };
687
706
  }
688
707
  return null;
689
708
  }
690
709
  if (expectedType === "json") {
691
- try {
692
- const parsed = JSON.parse(text);
693
- if (!Array.isArray(parsed)) {
694
- return null;
695
- }
696
- const members = [];
697
- for (const item of parsed) {
698
- if (typeof item === "string" || typeof item === "number") {
699
- members.push(item);
700
- } else if (typeof item === "object" && item !== null && "id" in item) {
701
- const id = item["id"];
702
- if (typeof id === "string" || typeof id === "number") {
703
- members.push(id);
704
- }
710
+ const parsed = tryParseJson(effectiveText);
711
+ if (!Array.isArray(parsed)) {
712
+ return null;
713
+ }
714
+ const members = [];
715
+ for (const item of parsed) {
716
+ if (typeof item === "string" || typeof item === "number") {
717
+ members.push(item);
718
+ } else if (typeof item === "object" && item !== null && "id" in item) {
719
+ const id = item["id"];
720
+ if (typeof id === "string" || typeof id === "number") {
721
+ members.push(id);
705
722
  }
706
723
  }
707
- return {
708
- kind: "constraint",
709
- constraintKind: "allowedMembers",
710
- members,
711
- provenance
712
- };
713
- } catch {
714
- return null;
715
724
  }
725
+ return {
726
+ kind: "constraint",
727
+ constraintKind: "allowedMembers",
728
+ members,
729
+ ...path2 && { path: path2 },
730
+ provenance
731
+ };
716
732
  }
717
733
  return {
718
734
  kind: "constraint",
719
735
  constraintKind: "pattern",
720
- pattern: text,
736
+ pattern: effectiveText,
737
+ ...path2 && { path: path2 },
721
738
  provenance
722
739
  };
723
740
  }
@@ -889,18 +906,19 @@ function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting) {
889
906
  const tsType = checker.getTypeAtLocation(prop);
890
907
  const optional = prop.questionToken !== void 0;
891
908
  const provenance = provenanceForNode(prop, file);
892
- const type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting);
909
+ let type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting);
893
910
  const constraints = [];
894
911
  if (prop.type) {
895
912
  constraints.push(...extractTypeAliasConstraintNodes(prop.type, checker, file));
896
913
  }
897
914
  constraints.push(...extractJSDocConstraintNodes(prop, file));
898
- const annotations = [];
915
+ let annotations = [];
899
916
  annotations.push(...extractJSDocAnnotationNodes(prop, file));
900
917
  const defaultAnnotation = extractDefaultValueAnnotation(prop.initializer, file);
901
918
  if (defaultAnnotation) {
902
919
  annotations.push(defaultAnnotation);
903
920
  }
921
+ ({ type, annotations } = applyEnumMemberDisplayNames(type, annotations));
904
922
  return {
905
923
  kind: "field",
906
924
  name,
@@ -919,14 +937,15 @@ function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visitin
919
937
  const tsType = checker.getTypeAtLocation(prop);
920
938
  const optional = prop.questionToken !== void 0;
921
939
  const provenance = provenanceForNode(prop, file);
922
- const type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting);
940
+ let type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting);
923
941
  const constraints = [];
924
942
  if (prop.type) {
925
943
  constraints.push(...extractTypeAliasConstraintNodes(prop.type, checker, file));
926
944
  }
927
945
  constraints.push(...extractJSDocConstraintNodes(prop, file));
928
- const annotations = [];
946
+ let annotations = [];
929
947
  annotations.push(...extractJSDocAnnotationNodes(prop, file));
948
+ ({ type, annotations } = applyEnumMemberDisplayNames(type, annotations));
930
949
  return {
931
950
  kind: "field",
932
951
  name,
@@ -937,6 +956,68 @@ function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visitin
937
956
  provenance
938
957
  };
939
958
  }
959
+ function applyEnumMemberDisplayNames(type, annotations) {
960
+ if (!annotations.some(
961
+ (annotation) => annotation.annotationKind === "displayName" && annotation.value.trim().startsWith(":")
962
+ )) {
963
+ return { type, annotations: [...annotations] };
964
+ }
965
+ const consumed = /* @__PURE__ */ new Set();
966
+ const nextType = rewriteEnumDisplayNames(type, annotations, consumed);
967
+ if (consumed.size === 0) {
968
+ return { type, annotations: [...annotations] };
969
+ }
970
+ return {
971
+ type: nextType,
972
+ annotations: annotations.filter((annotation) => !consumed.has(annotation))
973
+ };
974
+ }
975
+ function rewriteEnumDisplayNames(type, annotations, consumed) {
976
+ switch (type.kind) {
977
+ case "enum":
978
+ return applyEnumMemberDisplayNamesToEnum(type, annotations, consumed);
979
+ case "union": {
980
+ return {
981
+ ...type,
982
+ members: type.members.map(
983
+ (member) => rewriteEnumDisplayNames(member, annotations, consumed)
984
+ )
985
+ };
986
+ }
987
+ default:
988
+ return type;
989
+ }
990
+ }
991
+ function applyEnumMemberDisplayNamesToEnum(type, annotations, consumed) {
992
+ const displayNames = /* @__PURE__ */ new Map();
993
+ for (const annotation of annotations) {
994
+ if (annotation.annotationKind !== "displayName") continue;
995
+ const parsed = parseEnumMemberDisplayName(annotation.value);
996
+ if (!parsed) continue;
997
+ consumed.add(annotation);
998
+ const member = type.members.find((m) => String(m.value) === parsed.value);
999
+ if (!member) continue;
1000
+ displayNames.set(String(member.value), parsed.label);
1001
+ }
1002
+ if (displayNames.size === 0) {
1003
+ return type;
1004
+ }
1005
+ return {
1006
+ ...type,
1007
+ members: type.members.map((member) => {
1008
+ const displayName = displayNames.get(String(member.value));
1009
+ return displayName !== void 0 ? { ...member, displayName } : member;
1010
+ })
1011
+ };
1012
+ }
1013
+ function parseEnumMemberDisplayName(value) {
1014
+ const trimmed = value.trim();
1015
+ const match = /^:([^\s]+)\s+([\s\S]+)$/.exec(trimmed);
1016
+ if (!match?.[1] || !match[2]) return null;
1017
+ const label = match[2].trim();
1018
+ if (label === "") return null;
1019
+ return { value: match[1], label };
1020
+ }
940
1021
  function resolveTypeNode(type, checker, file, typeRegistry, visiting) {
941
1022
  if (type.flags & ts4.TypeFlags.String) {
942
1023
  return { kind: "primitive", primitiveKind: "string" };
@@ -1047,7 +1128,30 @@ function resolveArrayType(type, checker, file, typeRegistry, visiting) {
1047
1128
  const items = elementType ? resolveTypeNode(elementType, checker, file, typeRegistry, visiting) : { kind: "primitive", primitiveKind: "string" };
1048
1129
  return { kind: "array", items };
1049
1130
  }
1131
+ function tryResolveRecordType(type, checker, file, typeRegistry, visiting) {
1132
+ if (type.getProperties().length > 0) {
1133
+ return null;
1134
+ }
1135
+ const indexInfo = checker.getIndexInfoOfType(type, ts4.IndexKind.String);
1136
+ if (!indexInfo) {
1137
+ return null;
1138
+ }
1139
+ if (visiting.has(type)) {
1140
+ return null;
1141
+ }
1142
+ visiting.add(type);
1143
+ try {
1144
+ const valueType = resolveTypeNode(indexInfo.type, checker, file, typeRegistry, visiting);
1145
+ return { kind: "record", valueType };
1146
+ } finally {
1147
+ visiting.delete(type);
1148
+ }
1149
+ }
1050
1150
  function resolveObjectType(type, checker, file, typeRegistry, visiting) {
1151
+ const recordNode = tryResolveRecordType(type, checker, file, typeRegistry, visiting);
1152
+ if (recordNode) {
1153
+ return recordNode;
1154
+ }
1051
1155
  if (visiting.has(type)) {
1052
1156
  return { kind: "object", properties: [], additionalProperties: false };
1053
1157
  }
@@ -1079,7 +1183,7 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting) {
1079
1183
  const objectNode = {
1080
1184
  kind: "object",
1081
1185
  properties,
1082
- additionalProperties: false
1186
+ additionalProperties: true
1083
1187
  };
1084
1188
  if (typeName) {
1085
1189
  typeRegistry[typeName] = {
@@ -1148,14 +1252,23 @@ function buildFieldNodeInfoMap(members, checker, file, typeRegistry, visiting) {
1148
1252
  }
1149
1253
  return map;
1150
1254
  }
1151
- function extractTypeAliasConstraintNodes(typeNode, checker, file) {
1255
+ var MAX_ALIAS_CHAIN_DEPTH = 8;
1256
+ function extractTypeAliasConstraintNodes(typeNode, checker, file, depth = 0) {
1152
1257
  if (!ts4.isTypeReferenceNode(typeNode)) return [];
1258
+ if (depth >= MAX_ALIAS_CHAIN_DEPTH) {
1259
+ const aliasName = typeNode.typeName.getText();
1260
+ throw new Error(
1261
+ `Type alias chain exceeds maximum depth of ${String(MAX_ALIAS_CHAIN_DEPTH)} at alias "${aliasName}" in ${file}. Simplify the alias chain or check for circular references.`
1262
+ );
1263
+ }
1153
1264
  const symbol = checker.getSymbolAtLocation(typeNode.typeName);
1154
1265
  if (!symbol?.declarations) return [];
1155
1266
  const aliasDecl = symbol.declarations.find(ts4.isTypeAliasDeclaration);
1156
1267
  if (!aliasDecl) return [];
1157
1268
  if (ts4.isTypeLiteralNode(aliasDecl.type)) return [];
1158
- return extractJSDocConstraintNodes(aliasDecl, file);
1269
+ const constraints = extractJSDocConstraintNodes(aliasDecl, file);
1270
+ constraints.push(...extractTypeAliasConstraintNodes(aliasDecl.type, checker, file, depth + 1));
1271
+ return constraints;
1159
1272
  }
1160
1273
  function provenanceForNode(node, file) {
1161
1274
  const sourceFile = node.getSourceFile();
@@ -1230,11 +1343,21 @@ function detectFormSpecReference(typeNode) {
1230
1343
  }
1231
1344
 
1232
1345
  // src/json-schema/ir-generator.ts
1233
- function makeContext() {
1234
- return { defs: {} };
1346
+ function makeContext(options) {
1347
+ const vendorPrefix = options?.vendorPrefix ?? "x-formspec";
1348
+ if (!vendorPrefix.startsWith("x-")) {
1349
+ throw new Error(
1350
+ `Invalid vendorPrefix "${vendorPrefix}". Extension JSON Schema keywords must start with "x-".`
1351
+ );
1352
+ }
1353
+ return {
1354
+ defs: {},
1355
+ extensionRegistry: options?.extensionRegistry,
1356
+ vendorPrefix
1357
+ };
1235
1358
  }
1236
- function generateJsonSchemaFromIR(ir) {
1237
- const ctx = makeContext();
1359
+ function generateJsonSchemaFromIR(ir, options) {
1360
+ const ctx = makeContext(options);
1238
1361
  for (const [name, typeDef] of Object.entries(ir.typeRegistry)) {
1239
1362
  ctx.defs[name] = generateTypeNode(typeDef.type, ctx);
1240
1363
  }
@@ -1277,8 +1400,70 @@ function collectFields(elements, properties, required, ctx) {
1277
1400
  }
1278
1401
  function generateFieldSchema(field, ctx) {
1279
1402
  const schema = generateTypeNode(field.type, ctx);
1280
- applyConstraints(schema, field.constraints);
1281
- applyAnnotations(schema, field.annotations);
1403
+ const directConstraints = [];
1404
+ const pathConstraints = [];
1405
+ for (const c of field.constraints) {
1406
+ if (c.path) {
1407
+ pathConstraints.push(c);
1408
+ } else {
1409
+ directConstraints.push(c);
1410
+ }
1411
+ }
1412
+ applyConstraints(schema, directConstraints, ctx);
1413
+ applyAnnotations(schema, field.annotations, ctx);
1414
+ if (pathConstraints.length === 0) {
1415
+ return schema;
1416
+ }
1417
+ return applyPathTargetedConstraints(schema, pathConstraints, ctx);
1418
+ }
1419
+ function applyPathTargetedConstraints(schema, pathConstraints, ctx) {
1420
+ if (schema.type === "array" && schema.items) {
1421
+ schema.items = applyPathTargetedConstraints(schema.items, pathConstraints, ctx);
1422
+ return schema;
1423
+ }
1424
+ const byTarget = /* @__PURE__ */ new Map();
1425
+ for (const c of pathConstraints) {
1426
+ const target = c.path?.segments[0];
1427
+ if (!target) continue;
1428
+ const group = byTarget.get(target) ?? [];
1429
+ group.push(c);
1430
+ byTarget.set(target, group);
1431
+ }
1432
+ const propertyOverrides = {};
1433
+ for (const [target, constraints] of byTarget) {
1434
+ const subSchema = {};
1435
+ applyConstraints(subSchema, constraints, ctx);
1436
+ propertyOverrides[target] = subSchema;
1437
+ }
1438
+ if (schema.$ref) {
1439
+ const { $ref, ...rest } = schema;
1440
+ const refPart = { $ref };
1441
+ const overridePart = {
1442
+ properties: propertyOverrides,
1443
+ ...rest
1444
+ };
1445
+ return { allOf: [refPart, overridePart] };
1446
+ }
1447
+ if (schema.type === "object" && schema.properties) {
1448
+ const missingOverrides = {};
1449
+ for (const [target, overrideSchema] of Object.entries(propertyOverrides)) {
1450
+ if (schema.properties[target]) {
1451
+ Object.assign(schema.properties[target], overrideSchema);
1452
+ } else {
1453
+ missingOverrides[target] = overrideSchema;
1454
+ }
1455
+ }
1456
+ if (Object.keys(missingOverrides).length === 0) {
1457
+ return schema;
1458
+ }
1459
+ return {
1460
+ allOf: [schema, { properties: missingOverrides }]
1461
+ };
1462
+ }
1463
+ if (schema.allOf) {
1464
+ schema.allOf = [...schema.allOf, { properties: propertyOverrides }];
1465
+ return schema;
1466
+ }
1282
1467
  return schema;
1283
1468
  }
1284
1469
  function generateTypeNode(type, ctx) {
@@ -1291,6 +1476,8 @@ function generateTypeNode(type, ctx) {
1291
1476
  return generateArrayType(type, ctx);
1292
1477
  case "object":
1293
1478
  return generateObjectType(type, ctx);
1479
+ case "record":
1480
+ return generateRecordType(type, ctx);
1294
1481
  case "union":
1295
1482
  return generateUnionType(type, ctx);
1296
1483
  case "reference":
@@ -1298,7 +1485,7 @@ function generateTypeNode(type, ctx) {
1298
1485
  case "dynamic":
1299
1486
  return generateDynamicType(type);
1300
1487
  case "custom":
1301
- return generateCustomType(type);
1488
+ return generateCustomType(type, ctx);
1302
1489
  default: {
1303
1490
  const _exhaustive = type;
1304
1491
  return _exhaustive;
@@ -1347,16 +1534,27 @@ function generateObjectType(type, ctx) {
1347
1534
  }
1348
1535
  return schema;
1349
1536
  }
1537
+ function generateRecordType(type, ctx) {
1538
+ return {
1539
+ type: "object",
1540
+ additionalProperties: generateTypeNode(type.valueType, ctx)
1541
+ };
1542
+ }
1350
1543
  function generatePropertySchema(prop, ctx) {
1351
1544
  const schema = generateTypeNode(prop.type, ctx);
1352
- applyConstraints(schema, prop.constraints);
1353
- applyAnnotations(schema, prop.annotations);
1545
+ applyConstraints(schema, prop.constraints, ctx);
1546
+ applyAnnotations(schema, prop.annotations, ctx);
1354
1547
  return schema;
1355
1548
  }
1356
1549
  function generateUnionType(type, ctx) {
1357
1550
  if (isBooleanUnion(type)) {
1358
1551
  return { type: "boolean" };
1359
1552
  }
1553
+ if (isNullableUnion(type)) {
1554
+ return {
1555
+ oneOf: type.members.map((m) => generateTypeNode(m, ctx))
1556
+ };
1557
+ }
1360
1558
  return {
1361
1559
  anyOf: type.members.map((m) => generateTypeNode(m, ctx))
1362
1560
  };
@@ -1366,6 +1564,13 @@ function isBooleanUnion(type) {
1366
1564
  const kinds = type.members.map((m) => m.kind);
1367
1565
  return kinds.every((k) => k === "primitive") && type.members.every((m) => m.kind === "primitive" && m.primitiveKind === "boolean");
1368
1566
  }
1567
+ function isNullableUnion(type) {
1568
+ if (type.members.length !== 2) return false;
1569
+ const nullCount = type.members.filter(
1570
+ (m) => m.kind === "primitive" && m.primitiveKind === "null"
1571
+ ).length;
1572
+ return nullCount === 1;
1573
+ }
1369
1574
  function generateReferenceType(type) {
1370
1575
  return { $ref: `#/$defs/${type.name}` };
1371
1576
  }
@@ -1386,10 +1591,7 @@ function generateDynamicType(type) {
1386
1591
  "x-formspec-schemaSource": type.sourceKey
1387
1592
  };
1388
1593
  }
1389
- function generateCustomType(_type) {
1390
- return { type: "object" };
1391
- }
1392
- function applyConstraints(schema, constraints) {
1594
+ function applyConstraints(schema, constraints, ctx) {
1393
1595
  for (const constraint of constraints) {
1394
1596
  switch (constraint.constraintKind) {
1395
1597
  case "minimum":
@@ -1434,6 +1636,7 @@ function applyConstraints(schema, constraints) {
1434
1636
  case "allowedMembers":
1435
1637
  break;
1436
1638
  case "custom":
1639
+ applyCustomConstraint(schema, constraint, ctx);
1437
1640
  break;
1438
1641
  default: {
1439
1642
  const _exhaustive = constraint;
@@ -1442,7 +1645,7 @@ function applyConstraints(schema, constraints) {
1442
1645
  }
1443
1646
  }
1444
1647
  }
1445
- function applyAnnotations(schema, annotations) {
1648
+ function applyAnnotations(schema, annotations, ctx) {
1446
1649
  for (const annotation of annotations) {
1447
1650
  switch (annotation.annotationKind) {
1448
1651
  case "displayName":
@@ -1462,6 +1665,7 @@ function applyAnnotations(schema, annotations) {
1462
1665
  case "formatHint":
1463
1666
  break;
1464
1667
  case "custom":
1668
+ applyCustomAnnotation(schema, annotation, ctx);
1465
1669
  break;
1466
1670
  default: {
1467
1671
  const _exhaustive = annotation;
@@ -1470,6 +1674,36 @@ function applyAnnotations(schema, annotations) {
1470
1674
  }
1471
1675
  }
1472
1676
  }
1677
+ function generateCustomType(type, ctx) {
1678
+ const registration = ctx.extensionRegistry?.findType(type.typeId);
1679
+ if (registration === void 0) {
1680
+ throw new Error(
1681
+ `Cannot generate JSON Schema for custom type "${type.typeId}" without a matching extension registration`
1682
+ );
1683
+ }
1684
+ return registration.toJsonSchema(type.payload, ctx.vendorPrefix);
1685
+ }
1686
+ function applyCustomConstraint(schema, constraint, ctx) {
1687
+ const registration = ctx.extensionRegistry?.findConstraint(constraint.constraintId);
1688
+ if (registration === void 0) {
1689
+ throw new Error(
1690
+ `Cannot generate JSON Schema for custom constraint "${constraint.constraintId}" without a matching extension registration`
1691
+ );
1692
+ }
1693
+ Object.assign(schema, registration.toJsonSchema(constraint.payload, ctx.vendorPrefix));
1694
+ }
1695
+ function applyCustomAnnotation(schema, annotation, ctx) {
1696
+ const registration = ctx.extensionRegistry?.findAnnotation(annotation.annotationId);
1697
+ if (registration === void 0) {
1698
+ throw new Error(
1699
+ `Cannot generate JSON Schema for custom annotation "${annotation.annotationId}" without a matching extension registration`
1700
+ );
1701
+ }
1702
+ if (registration.toJsonSchema === void 0) {
1703
+ return;
1704
+ }
1705
+ Object.assign(schema, registration.toJsonSchema(annotation.value, ctx.vendorPrefix));
1706
+ }
1473
1707
 
1474
1708
  // src/ui-schema/schema.ts
1475
1709
  var import_zod = require("zod");
@@ -1694,12 +1928,9 @@ function generateClassSchemas(analysis, source) {
1694
1928
  }
1695
1929
 
1696
1930
  // src/validate/constraint-validator.ts
1697
- function makeCode(ctx, category, number) {
1698
- return `${ctx.vendorPrefix}-${category}-${String(number).padStart(3, "0")}`;
1699
- }
1700
1931
  function addContradiction(ctx, message, primary, related) {
1701
1932
  ctx.diagnostics.push({
1702
- code: makeCode(ctx, "CONTRADICTION", 1),
1933
+ code: "CONTRADICTING_CONSTRAINTS",
1703
1934
  message,
1704
1935
  severity: "error",
1705
1936
  primaryLocation: primary,
@@ -1708,7 +1939,7 @@ function addContradiction(ctx, message, primary, related) {
1708
1939
  }
1709
1940
  function addTypeMismatch(ctx, message, primary) {
1710
1941
  ctx.diagnostics.push({
1711
- code: makeCode(ctx, "TYPE_MISMATCH", 1),
1942
+ code: "TYPE_MISMATCH",
1712
1943
  message,
1713
1944
  severity: "error",
1714
1945
  primaryLocation: primary,
@@ -1717,28 +1948,153 @@ function addTypeMismatch(ctx, message, primary) {
1717
1948
  }
1718
1949
  function addUnknownExtension(ctx, message, primary) {
1719
1950
  ctx.diagnostics.push({
1720
- code: makeCode(ctx, "UNKNOWN_EXTENSION", 1),
1951
+ code: "UNKNOWN_EXTENSION",
1721
1952
  message,
1722
1953
  severity: "warning",
1723
1954
  primaryLocation: primary,
1724
1955
  relatedLocations: []
1725
1956
  });
1726
1957
  }
1958
+ function addConstraintBroadening(ctx, message, primary, related) {
1959
+ ctx.diagnostics.push({
1960
+ code: "CONSTRAINT_BROADENING",
1961
+ message,
1962
+ severity: "error",
1963
+ primaryLocation: primary,
1964
+ relatedLocations: [related]
1965
+ });
1966
+ }
1727
1967
  function findNumeric(constraints, constraintKind) {
1728
- return constraints.find(
1729
- (c) => c.constraintKind === constraintKind
1730
- );
1968
+ return constraints.find((c) => c.constraintKind === constraintKind);
1731
1969
  }
1732
1970
  function findLength(constraints, constraintKind) {
1733
- return constraints.find(
1734
- (c) => c.constraintKind === constraintKind
1735
- );
1971
+ return constraints.find((c) => c.constraintKind === constraintKind);
1736
1972
  }
1737
1973
  function findAllowedMembers(constraints) {
1738
1974
  return constraints.filter(
1739
1975
  (c) => c.constraintKind === "allowedMembers"
1740
1976
  );
1741
1977
  }
1978
+ function isOrderedBoundConstraint(constraint) {
1979
+ 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";
1980
+ }
1981
+ function pathKey(constraint) {
1982
+ return constraint.path?.segments.join(".") ?? "";
1983
+ }
1984
+ function orderedBoundFamily(kind) {
1985
+ switch (kind) {
1986
+ case "minimum":
1987
+ case "exclusiveMinimum":
1988
+ return "numeric-lower";
1989
+ case "maximum":
1990
+ case "exclusiveMaximum":
1991
+ return "numeric-upper";
1992
+ case "minLength":
1993
+ return "minLength";
1994
+ case "minItems":
1995
+ return "minItems";
1996
+ case "maxLength":
1997
+ return "maxLength";
1998
+ case "maxItems":
1999
+ return "maxItems";
2000
+ default: {
2001
+ const _exhaustive = kind;
2002
+ return _exhaustive;
2003
+ }
2004
+ }
2005
+ }
2006
+ function isNumericLowerKind(kind) {
2007
+ return kind === "minimum" || kind === "exclusiveMinimum";
2008
+ }
2009
+ function isNumericUpperKind(kind) {
2010
+ return kind === "maximum" || kind === "exclusiveMaximum";
2011
+ }
2012
+ function describeConstraintTag(constraint) {
2013
+ return `@${constraint.constraintKind}`;
2014
+ }
2015
+ function compareConstraintStrength(current, previous) {
2016
+ const family = orderedBoundFamily(current.constraintKind);
2017
+ if (family === "numeric-lower") {
2018
+ if (!isNumericLowerKind(current.constraintKind) || !isNumericLowerKind(previous.constraintKind)) {
2019
+ throw new Error("numeric-lower family received non-numeric lower-bound constraint");
2020
+ }
2021
+ if (current.value !== previous.value) {
2022
+ return current.value > previous.value ? 1 : -1;
2023
+ }
2024
+ if (current.constraintKind === "exclusiveMinimum" && previous.constraintKind === "minimum") {
2025
+ return 1;
2026
+ }
2027
+ if (current.constraintKind === "minimum" && previous.constraintKind === "exclusiveMinimum") {
2028
+ return -1;
2029
+ }
2030
+ return 0;
2031
+ }
2032
+ if (family === "numeric-upper") {
2033
+ if (!isNumericUpperKind(current.constraintKind) || !isNumericUpperKind(previous.constraintKind)) {
2034
+ throw new Error("numeric-upper family received non-numeric upper-bound constraint");
2035
+ }
2036
+ if (current.value !== previous.value) {
2037
+ return current.value < previous.value ? 1 : -1;
2038
+ }
2039
+ if (current.constraintKind === "exclusiveMaximum" && previous.constraintKind === "maximum") {
2040
+ return 1;
2041
+ }
2042
+ if (current.constraintKind === "maximum" && previous.constraintKind === "exclusiveMaximum") {
2043
+ return -1;
2044
+ }
2045
+ return 0;
2046
+ }
2047
+ switch (family) {
2048
+ case "minLength":
2049
+ case "minItems":
2050
+ if (current.value === previous.value) {
2051
+ return 0;
2052
+ }
2053
+ return current.value > previous.value ? 1 : -1;
2054
+ case "maxLength":
2055
+ case "maxItems":
2056
+ if (current.value === previous.value) {
2057
+ return 0;
2058
+ }
2059
+ return current.value < previous.value ? 1 : -1;
2060
+ default: {
2061
+ const _exhaustive = family;
2062
+ return _exhaustive;
2063
+ }
2064
+ }
2065
+ }
2066
+ function checkConstraintBroadening(ctx, fieldName, constraints) {
2067
+ const strongestByKey = /* @__PURE__ */ new Map();
2068
+ for (const constraint of constraints) {
2069
+ if (!isOrderedBoundConstraint(constraint)) {
2070
+ continue;
2071
+ }
2072
+ const key = `${orderedBoundFamily(constraint.constraintKind)}:${pathKey(constraint)}`;
2073
+ const previous = strongestByKey.get(key);
2074
+ if (previous === void 0) {
2075
+ strongestByKey.set(key, constraint);
2076
+ continue;
2077
+ }
2078
+ const strength = compareConstraintStrength(constraint, previous);
2079
+ if (strength < 0) {
2080
+ const displayFieldName = formatPathTargetFieldName(
2081
+ fieldName,
2082
+ constraint.path?.segments ?? []
2083
+ );
2084
+ addConstraintBroadening(
2085
+ ctx,
2086
+ `Field "${displayFieldName}": ${describeConstraintTag(constraint)} (${String(constraint.value)}) is broader than earlier ${describeConstraintTag(previous)} (${String(previous.value)}). Constraints can only narrow.`,
2087
+ constraint.provenance,
2088
+ previous.provenance
2089
+ );
2090
+ continue;
2091
+ }
2092
+ if (strength <= 0) {
2093
+ continue;
2094
+ }
2095
+ strongestByKey.set(key, constraint);
2096
+ }
2097
+ }
1742
2098
  function checkNumericContradictions(ctx, fieldName, constraints) {
1743
2099
  const min = findNumeric(constraints, "minimum");
1744
2100
  const max = findNumeric(constraints, "maximum");
@@ -1835,6 +2191,8 @@ function typeLabel(type) {
1835
2191
  return "array";
1836
2192
  case "object":
1837
2193
  return "object";
2194
+ case "record":
2195
+ return "record";
1838
2196
  case "union":
1839
2197
  return "union";
1840
2198
  case "reference":
@@ -1849,74 +2207,140 @@ function typeLabel(type) {
1849
2207
  }
1850
2208
  }
1851
2209
  }
1852
- function checkTypeApplicability(ctx, fieldName, type, constraints) {
1853
- const isNumber = type.kind === "primitive" && type.primitiveKind === "number";
1854
- const isString = type.kind === "primitive" && type.primitiveKind === "string";
1855
- const isArray = type.kind === "array";
1856
- const isEnum = type.kind === "enum";
1857
- const label = typeLabel(type);
1858
- for (const constraint of constraints) {
1859
- const ck = constraint.constraintKind;
1860
- switch (ck) {
1861
- case "minimum":
1862
- case "maximum":
1863
- case "exclusiveMinimum":
1864
- case "exclusiveMaximum":
1865
- case "multipleOf": {
1866
- if (!isNumber) {
1867
- addTypeMismatch(
1868
- ctx,
1869
- `Field "${fieldName}": constraint "${ck}" is only valid on number fields, but field type is "${label}"`,
1870
- constraint.provenance
1871
- );
1872
- }
1873
- break;
2210
+ function dereferenceType(ctx, type) {
2211
+ let current = type;
2212
+ const seen = /* @__PURE__ */ new Set();
2213
+ while (current.kind === "reference") {
2214
+ if (seen.has(current.name)) {
2215
+ return current;
2216
+ }
2217
+ seen.add(current.name);
2218
+ const definition = ctx.typeRegistry[current.name];
2219
+ if (definition === void 0) {
2220
+ return current;
2221
+ }
2222
+ current = definition.type;
2223
+ }
2224
+ return current;
2225
+ }
2226
+ function resolvePathTargetType(ctx, type, segments) {
2227
+ const effectiveType = dereferenceType(ctx, type);
2228
+ if (segments.length === 0) {
2229
+ return { kind: "resolved", type: effectiveType };
2230
+ }
2231
+ if (effectiveType.kind === "array") {
2232
+ return resolvePathTargetType(ctx, effectiveType.items, segments);
2233
+ }
2234
+ if (effectiveType.kind === "object") {
2235
+ const [segment, ...rest] = segments;
2236
+ if (segment === void 0) {
2237
+ throw new Error("Invariant violation: object path traversal requires a segment");
2238
+ }
2239
+ const property = effectiveType.properties.find((prop) => prop.name === segment);
2240
+ if (property === void 0) {
2241
+ return { kind: "missing-property", segment };
2242
+ }
2243
+ return resolvePathTargetType(ctx, property.type, rest);
2244
+ }
2245
+ return { kind: "unresolvable", type: effectiveType };
2246
+ }
2247
+ function formatPathTargetFieldName(fieldName, path2) {
2248
+ return path2.length === 0 ? fieldName : `${fieldName}.${path2.join(".")}`;
2249
+ }
2250
+ function checkConstraintOnType(ctx, fieldName, type, constraint) {
2251
+ const effectiveType = dereferenceType(ctx, type);
2252
+ const isNumber = effectiveType.kind === "primitive" && effectiveType.primitiveKind === "number";
2253
+ const isString = effectiveType.kind === "primitive" && effectiveType.primitiveKind === "string";
2254
+ const isArray = effectiveType.kind === "array";
2255
+ const isEnum = effectiveType.kind === "enum";
2256
+ const label = typeLabel(effectiveType);
2257
+ const ck = constraint.constraintKind;
2258
+ switch (ck) {
2259
+ case "minimum":
2260
+ case "maximum":
2261
+ case "exclusiveMinimum":
2262
+ case "exclusiveMaximum":
2263
+ case "multipleOf": {
2264
+ if (!isNumber) {
2265
+ addTypeMismatch(
2266
+ ctx,
2267
+ `Field "${fieldName}": constraint "${ck}" is only valid on number fields, but field type is "${label}"`,
2268
+ constraint.provenance
2269
+ );
1874
2270
  }
1875
- case "minLength":
1876
- case "maxLength":
1877
- case "pattern": {
1878
- if (!isString) {
1879
- addTypeMismatch(
1880
- ctx,
1881
- `Field "${fieldName}": constraint "${ck}" is only valid on string fields, but field type is "${label}"`,
1882
- constraint.provenance
1883
- );
1884
- }
1885
- break;
2271
+ break;
2272
+ }
2273
+ case "minLength":
2274
+ case "maxLength":
2275
+ case "pattern": {
2276
+ if (!isString) {
2277
+ addTypeMismatch(
2278
+ ctx,
2279
+ `Field "${fieldName}": constraint "${ck}" is only valid on string fields, but field type is "${label}"`,
2280
+ constraint.provenance
2281
+ );
1886
2282
  }
1887
- case "minItems":
1888
- case "maxItems":
1889
- case "uniqueItems": {
1890
- if (!isArray) {
1891
- addTypeMismatch(
1892
- ctx,
1893
- `Field "${fieldName}": constraint "${ck}" is only valid on array fields, but field type is "${label}"`,
1894
- constraint.provenance
1895
- );
1896
- }
1897
- break;
2283
+ break;
2284
+ }
2285
+ case "minItems":
2286
+ case "maxItems":
2287
+ case "uniqueItems": {
2288
+ if (!isArray) {
2289
+ addTypeMismatch(
2290
+ ctx,
2291
+ `Field "${fieldName}": constraint "${ck}" is only valid on array fields, but field type is "${label}"`,
2292
+ constraint.provenance
2293
+ );
1898
2294
  }
1899
- case "allowedMembers": {
1900
- if (!isEnum) {
1901
- addTypeMismatch(
1902
- ctx,
1903
- `Field "${fieldName}": constraint "allowedMembers" is only valid on enum fields, but field type is "${label}"`,
1904
- constraint.provenance
1905
- );
1906
- }
1907
- break;
2295
+ break;
2296
+ }
2297
+ case "allowedMembers": {
2298
+ if (!isEnum) {
2299
+ addTypeMismatch(
2300
+ ctx,
2301
+ `Field "${fieldName}": constraint "allowedMembers" is only valid on enum fields, but field type is "${label}"`,
2302
+ constraint.provenance
2303
+ );
1908
2304
  }
1909
- case "custom": {
1910
- checkCustomConstraint(ctx, fieldName, type, constraint);
1911
- break;
2305
+ break;
2306
+ }
2307
+ case "custom": {
2308
+ checkCustomConstraint(ctx, fieldName, effectiveType, constraint);
2309
+ break;
2310
+ }
2311
+ default: {
2312
+ const _exhaustive = constraint;
2313
+ throw new Error(
2314
+ `Unhandled constraint kind: ${_exhaustive.constraintKind}`
2315
+ );
2316
+ }
2317
+ }
2318
+ }
2319
+ function checkTypeApplicability(ctx, fieldName, type, constraints) {
2320
+ for (const constraint of constraints) {
2321
+ if (constraint.path) {
2322
+ const resolution = resolvePathTargetType(ctx, type, constraint.path.segments);
2323
+ const targetFieldName = formatPathTargetFieldName(fieldName, constraint.path.segments);
2324
+ if (resolution.kind === "missing-property") {
2325
+ addTypeMismatch(
2326
+ ctx,
2327
+ `Field "${fieldName}": path-targeted constraint "${constraint.constraintKind}" references unknown path segment "${resolution.segment}"`,
2328
+ constraint.provenance
2329
+ );
2330
+ continue;
1912
2331
  }
1913
- default: {
1914
- const _exhaustive = constraint;
1915
- throw new Error(
1916
- `Unhandled constraint kind: ${_exhaustive.constraintKind}`
2332
+ if (resolution.kind === "unresolvable") {
2333
+ addTypeMismatch(
2334
+ ctx,
2335
+ `Field "${targetFieldName}": path-targeted constraint "${constraint.constraintKind}" is invalid because type "${typeLabel(resolution.type)}" cannot be traversed`,
2336
+ constraint.provenance
1917
2337
  );
2338
+ continue;
1918
2339
  }
2340
+ checkConstraintOnType(ctx, targetFieldName, resolution.type, constraint);
2341
+ continue;
1919
2342
  }
2343
+ checkConstraintOnType(ctx, fieldName, type, constraint);
1920
2344
  }
1921
2345
  }
1922
2346
  function checkCustomConstraint(ctx, fieldName, type, constraint) {
@@ -1960,6 +2384,7 @@ function validateConstraints(ctx, name, type, constraints) {
1960
2384
  checkNumericContradictions(ctx, name, constraints);
1961
2385
  checkLengthContradictions(ctx, name, constraints);
1962
2386
  checkAllowedMembersContradiction(ctx, name, constraints);
2387
+ checkConstraintBroadening(ctx, name, constraints);
1963
2388
  checkTypeApplicability(ctx, name, type, constraints);
1964
2389
  }
1965
2390
  function validateElement(ctx, element) {
@@ -1986,8 +2411,8 @@ function validateElement(ctx, element) {
1986
2411
  function validateIR(ir, options) {
1987
2412
  const ctx = {
1988
2413
  diagnostics: [],
1989
- vendorPrefix: options?.vendorPrefix ?? "FORMSPEC",
1990
- extensionRegistry: options?.extensionRegistry
2414
+ extensionRegistry: options?.extensionRegistry,
2415
+ typeRegistry: ir.typeRegistry
1991
2416
  };
1992
2417
  for (const element of ir.elements) {
1993
2418
  validateElement(ctx, element);
@@ -2041,7 +2466,7 @@ function createExtensionRegistry(extensions) {
2041
2466
  }
2042
2467
 
2043
2468
  // src/generators/method-schema.ts
2044
- var import_core5 = require("@formspec/core");
2469
+ var import_core4 = require("@formspec/core");
2045
2470
  function typeToJsonSchema(type, checker) {
2046
2471
  const typeRegistry = {};
2047
2472
  const visiting = /* @__PURE__ */ new Set();
@@ -2049,7 +2474,7 @@ function typeToJsonSchema(type, checker) {
2049
2474
  const fieldProvenance = { surface: "tsdoc", file: "", line: 0, column: 0 };
2050
2475
  const ir = {
2051
2476
  kind: "form-ir",
2052
- irVersion: import_core5.IR_VERSION,
2477
+ irVersion: import_core4.IR_VERSION,
2053
2478
  elements: [
2054
2479
  {
2055
2480
  kind: "field",