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

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 (68) hide show
  1. package/dist/__tests__/extension-runtime.integration.test.d.ts +2 -0
  2. package/dist/__tests__/extension-runtime.integration.test.d.ts.map +1 -0
  3. package/dist/__tests__/fixtures/edge-cases.d.ts +22 -0
  4. package/dist/__tests__/fixtures/edge-cases.d.ts.map +1 -1
  5. package/dist/__tests__/fixtures/example-a-builtins.d.ts +6 -6
  6. package/dist/__tests__/fixtures/example-interface-types.d.ts +26 -26
  7. package/dist/__tests__/fixtures/example-interface-types.d.ts.map +1 -1
  8. package/dist/__tests__/fixtures/mixed-authoring-shipping-address.d.ts +30 -0
  9. package/dist/__tests__/fixtures/mixed-authoring-shipping-address.d.ts.map +1 -0
  10. package/dist/__tests__/mixed-authoring.test.d.ts +2 -0
  11. package/dist/__tests__/mixed-authoring.test.d.ts.map +1 -0
  12. package/dist/__tests__/parity/fixtures/plan-status/chain-dsl.d.ts +19 -0
  13. package/dist/__tests__/parity/fixtures/plan-status/chain-dsl.d.ts.map +1 -0
  14. package/dist/__tests__/parity/fixtures/plan-status/expected-ir.d.ts +6 -0
  15. package/dist/__tests__/parity/fixtures/plan-status/expected-ir.d.ts.map +1 -0
  16. package/dist/__tests__/parity/fixtures/plan-status/tsdoc.d.ts +17 -0
  17. package/dist/__tests__/parity/fixtures/plan-status/tsdoc.d.ts.map +1 -0
  18. package/dist/__tests__/parity/fixtures/usd-cents/chain-dsl.d.ts +9 -0
  19. package/dist/__tests__/parity/fixtures/usd-cents/chain-dsl.d.ts.map +1 -0
  20. package/dist/__tests__/parity/fixtures/usd-cents/expected-ir.d.ts +6 -0
  21. package/dist/__tests__/parity/fixtures/usd-cents/expected-ir.d.ts.map +1 -0
  22. package/dist/__tests__/parity/fixtures/usd-cents/tsdoc.d.ts +19 -0
  23. package/dist/__tests__/parity/fixtures/usd-cents/tsdoc.d.ts.map +1 -0
  24. package/dist/__tests__/parity/utils.d.ts +11 -4
  25. package/dist/__tests__/parity/utils.d.ts.map +1 -1
  26. package/dist/analyzer/class-analyzer.d.ts +5 -3
  27. package/dist/analyzer/class-analyzer.d.ts.map +1 -1
  28. package/dist/analyzer/jsdoc-constraints.d.ts +7 -51
  29. package/dist/analyzer/jsdoc-constraints.d.ts.map +1 -1
  30. package/dist/analyzer/tsdoc-parser.d.ts +25 -9
  31. package/dist/analyzer/tsdoc-parser.d.ts.map +1 -1
  32. package/dist/browser.cjs +546 -102
  33. package/dist/browser.cjs.map +1 -1
  34. package/dist/browser.d.ts +15 -2
  35. package/dist/browser.d.ts.map +1 -1
  36. package/dist/browser.js +544 -102
  37. package/dist/browser.js.map +1 -1
  38. package/dist/build.d.ts +170 -6
  39. package/dist/canonicalize/tsdoc-canonicalizer.d.ts +3 -3
  40. package/dist/canonicalize/tsdoc-canonicalizer.d.ts.map +1 -1
  41. package/dist/cli.cjs +877 -128
  42. package/dist/cli.cjs.map +1 -1
  43. package/dist/cli.js +876 -131
  44. package/dist/cli.js.map +1 -1
  45. package/dist/generators/mixed-authoring.d.ts +45 -0
  46. package/dist/generators/mixed-authoring.d.ts.map +1 -0
  47. package/dist/index.cjs +850 -125
  48. package/dist/index.cjs.map +1 -1
  49. package/dist/index.d.ts +22 -3
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +847 -129
  52. package/dist/index.js.map +1 -1
  53. package/dist/internals.cjs +946 -187
  54. package/dist/internals.cjs.map +1 -1
  55. package/dist/internals.js +944 -189
  56. package/dist/internals.js.map +1 -1
  57. package/dist/json-schema/generator.d.ts +8 -2
  58. package/dist/json-schema/generator.d.ts.map +1 -1
  59. package/dist/json-schema/ir-generator.d.ts +27 -4
  60. package/dist/json-schema/ir-generator.d.ts.map +1 -1
  61. package/dist/json-schema/types.d.ts +1 -1
  62. package/dist/json-schema/types.d.ts.map +1 -1
  63. package/dist/ui-schema/ir-generator.d.ts.map +1 -1
  64. package/dist/validate/constraint-validator.d.ts +3 -7
  65. package/dist/validate/constraint-validator.d.ts.map +1 -1
  66. package/package.json +3 -3
  67. package/dist/__tests__/jsdoc-constraints.test.d.ts +0 -10
  68. package/dist/__tests__/jsdoc-constraints.test.d.ts.map +0 -1
@@ -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
  }
@@ -376,6 +376,7 @@ function canonicalizeTSDoc(analysis, source) {
376
376
  irVersion: import_core2.IR_VERSION,
377
377
  elements,
378
378
  typeRegistry: analysis.typeRegistry,
379
+ ...analysis.annotations !== void 0 && analysis.annotations.length > 0 && { annotations: analysis.annotations },
379
380
  provenance
380
381
  };
381
382
  }
@@ -511,7 +512,6 @@ var ts4 = __toESM(require("typescript"), 1);
511
512
 
512
513
  // src/analyzer/jsdoc-constraints.ts
513
514
  var ts3 = __toESM(require("typescript"), 1);
514
- var import_core4 = require("@formspec/core");
515
515
 
516
516
  // src/analyzer/tsdoc-parser.ts
517
517
  var ts2 = __toESM(require("typescript"), 1);
@@ -541,7 +541,7 @@ var LENGTH_CONSTRAINT_MAP = {
541
541
  minItems: "minItems",
542
542
  maxItems: "maxItems"
543
543
  };
544
- var TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["pattern", "enumOptions"]);
544
+ var TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["pattern", "enumOptions", "defaultValue"]);
545
545
  function createFormSpecTSDocConfig() {
546
546
  const config = new import_tsdoc.TSDocConfiguration();
547
547
  for (const tagName of Object.keys(import_core3.BUILTIN_CONSTRAINT_DEFINITIONS)) {
@@ -553,6 +553,15 @@ function createFormSpecTSDocConfig() {
553
553
  })
554
554
  );
555
555
  }
556
+ for (const tagName of ["displayName", "description", "format", "placeholder"]) {
557
+ config.addTagDefinition(
558
+ new import_tsdoc.TSDocTagDefinition({
559
+ tagName: "@" + tagName,
560
+ syntaxKind: import_tsdoc.TSDocTagSyntaxKind.BlockTag,
561
+ allowMultiple: true
562
+ })
563
+ );
564
+ }
556
565
  return config;
557
566
  }
558
567
  var sharedParser;
@@ -563,6 +572,12 @@ function getParser() {
563
572
  function parseTSDocTags(node, file = "") {
564
573
  const constraints = [];
565
574
  const annotations = [];
575
+ let displayName;
576
+ let description;
577
+ let placeholder;
578
+ let displayNameProvenance;
579
+ let descriptionProvenance;
580
+ let placeholderProvenance;
566
581
  const sourceFile = node.getSourceFile();
567
582
  const sourceText = sourceFile.getFullText();
568
583
  const commentRanges = ts2.getLeadingCommentRanges(sourceText, node.getFullStart());
@@ -582,9 +597,37 @@ function parseTSDocTags(node, file = "") {
582
597
  const docComment = parserContext.docComment;
583
598
  for (const block of docComment.customBlocks) {
584
599
  const tagName = (0, import_core3.normalizeConstraintTagName)(block.blockTag.tagName.substring(1));
600
+ if (tagName === "displayName" || tagName === "description" || tagName === "format" || tagName === "placeholder") {
601
+ const text2 = extractBlockText(block).trim();
602
+ if (text2 === "") continue;
603
+ const provenance2 = provenanceForComment(range, sourceFile, file, tagName);
604
+ if (tagName === "displayName") {
605
+ if (!isMemberTargetDisplayName(text2) && displayName === void 0) {
606
+ displayName = text2;
607
+ displayNameProvenance = provenance2;
608
+ }
609
+ } else if (tagName === "format") {
610
+ annotations.push({
611
+ kind: "annotation",
612
+ annotationKind: "format",
613
+ value: text2,
614
+ provenance: provenance2
615
+ });
616
+ } else {
617
+ if (tagName === "description" && description === void 0) {
618
+ description = text2;
619
+ descriptionProvenance = provenance2;
620
+ } else if (tagName === "placeholder" && placeholder === void 0) {
621
+ placeholder = text2;
622
+ placeholderProvenance = provenance2;
623
+ }
624
+ }
625
+ continue;
626
+ }
585
627
  if (TAGS_REQUIRING_RAW_TEXT.has(tagName)) continue;
586
628
  const text = extractBlockText(block).trim();
587
- if (text === "") continue;
629
+ const expectedType = (0, import_core3.isBuiltinConstraintName)(tagName) ? import_core3.BUILTIN_CONSTRAINT_DEFINITIONS[tagName] : void 0;
630
+ if (text === "" && expectedType !== "boolean") continue;
588
631
  const provenance = provenanceForComment(range, sourceFile, file, tagName);
589
632
  const constraintNode = parseConstraintValue(tagName, text, provenance);
590
633
  if (constraintNode) {
@@ -592,14 +635,47 @@ function parseTSDocTags(node, file = "") {
592
635
  }
593
636
  }
594
637
  if (docComment.deprecatedBlock !== void 0) {
638
+ const message = extractBlockText(docComment.deprecatedBlock).trim();
595
639
  annotations.push({
596
640
  kind: "annotation",
597
641
  annotationKind: "deprecated",
642
+ ...message !== "" && { message },
598
643
  provenance: provenanceForComment(range, sourceFile, file, "deprecated")
599
644
  });
600
645
  }
646
+ if (description === void 0 && docComment.remarksBlock !== void 0) {
647
+ const remarks = extractBlockText(docComment.remarksBlock).trim();
648
+ if (remarks !== "") {
649
+ description = remarks;
650
+ descriptionProvenance = provenanceForComment(range, sourceFile, file, "remarks");
651
+ }
652
+ }
601
653
  }
602
654
  }
655
+ if (displayName !== void 0 && displayNameProvenance !== void 0) {
656
+ annotations.push({
657
+ kind: "annotation",
658
+ annotationKind: "displayName",
659
+ value: displayName,
660
+ provenance: displayNameProvenance
661
+ });
662
+ }
663
+ if (description !== void 0 && descriptionProvenance !== void 0) {
664
+ annotations.push({
665
+ kind: "annotation",
666
+ annotationKind: "description",
667
+ value: description,
668
+ provenance: descriptionProvenance
669
+ });
670
+ }
671
+ if (placeholder !== void 0 && placeholderProvenance !== void 0) {
672
+ annotations.push({
673
+ kind: "annotation",
674
+ annotationKind: "placeholder",
675
+ value: placeholder,
676
+ provenance: placeholderProvenance
677
+ });
678
+ }
603
679
  const jsDocTagsAll = ts2.getJSDocTags(node);
604
680
  for (const tag of jsDocTagsAll) {
605
681
  const tagName = (0, import_core3.normalizeConstraintTagName)(tag.tagName.text);
@@ -608,47 +684,39 @@ function parseTSDocTags(node, file = "") {
608
684
  if (commentText === void 0 || commentText.trim() === "") continue;
609
685
  const text = commentText.trim();
610
686
  const provenance = provenanceForJSDocTag(tag, file);
687
+ if (tagName === "defaultValue") {
688
+ const defaultValueNode = parseDefaultValueValue(text, provenance);
689
+ annotations.push(defaultValueNode);
690
+ continue;
691
+ }
611
692
  const constraintNode = parseConstraintValue(tagName, text, provenance);
612
693
  if (constraintNode) {
613
694
  constraints.push(constraintNode);
614
695
  }
615
696
  }
697
+ return { constraints, annotations };
698
+ }
699
+ function extractDisplayNameMetadata(node) {
616
700
  let displayName;
617
- let description;
618
- let displayNameTag;
619
- let descriptionTag;
620
- for (const tag of jsDocTagsAll) {
621
- const tagName = tag.tagName.text;
701
+ const memberDisplayNames = /* @__PURE__ */ new Map();
702
+ for (const tag of ts2.getJSDocTags(node)) {
703
+ const tagName = (0, import_core3.normalizeConstraintTagName)(tag.tagName.text);
704
+ if (tagName !== "displayName") continue;
622
705
  const commentText = getTagCommentText(tag);
623
- if (commentText === void 0 || commentText.trim() === "") {
706
+ if (commentText === void 0) continue;
707
+ const text = commentText.trim();
708
+ if (text === "") continue;
709
+ const memberTarget = parseMemberTargetDisplayName(text);
710
+ if (memberTarget) {
711
+ memberDisplayNames.set(memberTarget.target, memberTarget.label);
624
712
  continue;
625
713
  }
626
- const trimmed = commentText.trim();
627
- if (tagName === "Field_displayName") {
628
- displayName = trimmed;
629
- displayNameTag = tag;
630
- } else if (tagName === "Field_description") {
631
- description = trimmed;
632
- descriptionTag = tag;
633
- }
634
- }
635
- if (displayName !== void 0 && displayNameTag) {
636
- annotations.push({
637
- kind: "annotation",
638
- annotationKind: "displayName",
639
- value: displayName,
640
- provenance: provenanceForJSDocTag(displayNameTag, file)
641
- });
642
- }
643
- if (description !== void 0 && descriptionTag) {
644
- annotations.push({
645
- kind: "annotation",
646
- annotationKind: "description",
647
- value: description,
648
- provenance: provenanceForJSDocTag(descriptionTag, file)
649
- });
714
+ displayName ??= text;
650
715
  }
651
- return { constraints, annotations };
716
+ return {
717
+ ...displayName !== void 0 && { displayName },
718
+ memberDisplayNames
719
+ };
652
720
  }
653
721
  function extractPathTarget(text) {
654
722
  const trimmed = text.trimStart();
@@ -712,7 +780,45 @@ function parseConstraintValue(tagName, text, provenance) {
712
780
  }
713
781
  return null;
714
782
  }
783
+ if (expectedType === "boolean") {
784
+ const trimmed = effectiveText.trim();
785
+ if (trimmed !== "" && trimmed !== "true") {
786
+ return null;
787
+ }
788
+ if (tagName === "uniqueItems") {
789
+ return {
790
+ kind: "constraint",
791
+ constraintKind: "uniqueItems",
792
+ value: true,
793
+ ...path2 && { path: path2 },
794
+ provenance
795
+ };
796
+ }
797
+ return null;
798
+ }
715
799
  if (expectedType === "json") {
800
+ if (tagName === "const") {
801
+ const trimmedText = effectiveText.trim();
802
+ if (trimmedText === "") return null;
803
+ try {
804
+ const parsed2 = JSON.parse(trimmedText);
805
+ return {
806
+ kind: "constraint",
807
+ constraintKind: "const",
808
+ value: parsed2,
809
+ ...path2 && { path: path2 },
810
+ provenance
811
+ };
812
+ } catch {
813
+ return {
814
+ kind: "constraint",
815
+ constraintKind: "const",
816
+ value: trimmedText,
817
+ ...path2 && { path: path2 },
818
+ provenance
819
+ };
820
+ }
821
+ }
716
822
  const parsed = tryParseJson(effectiveText);
717
823
  if (!Array.isArray(parsed)) {
718
824
  return null;
@@ -744,6 +850,34 @@ function parseConstraintValue(tagName, text, provenance) {
744
850
  provenance
745
851
  };
746
852
  }
853
+ function parseDefaultValueValue(text, provenance) {
854
+ const trimmed = text.trim();
855
+ let value;
856
+ if (trimmed === "null") {
857
+ value = null;
858
+ } else if (trimmed === "true") {
859
+ value = true;
860
+ } else if (trimmed === "false") {
861
+ value = false;
862
+ } else {
863
+ const parsed = tryParseJson(trimmed);
864
+ value = parsed !== null ? parsed : trimmed;
865
+ }
866
+ return {
867
+ kind: "annotation",
868
+ annotationKind: "defaultValue",
869
+ value,
870
+ provenance
871
+ };
872
+ }
873
+ function isMemberTargetDisplayName(text) {
874
+ return parseMemberTargetDisplayName(text) !== null;
875
+ }
876
+ function parseMemberTargetDisplayName(text) {
877
+ const match = /^:([^\s]+)\s+([\s\S]+)$/.exec(text);
878
+ if (!match?.[1] || !match[2]) return null;
879
+ return { target: match[1], label: match[2].trim() };
880
+ }
747
881
  function provenanceForComment(range, sourceFile, file, tagName) {
748
882
  const { line, character } = sourceFile.getLineAndCharacterOfPosition(range.pos);
749
883
  return {
@@ -825,11 +959,17 @@ function isObjectType(type) {
825
959
  function isTypeReference(type) {
826
960
  return !!(type.flags & ts4.TypeFlags.Object) && !!(type.objectFlags & ts4.ObjectFlags.Reference);
827
961
  }
962
+ var RESOLVING_TYPE_PLACEHOLDER = {
963
+ kind: "object",
964
+ properties: [],
965
+ additionalProperties: true
966
+ };
828
967
  function analyzeClassToIR(classDecl, checker, file = "") {
829
968
  const name = classDecl.name?.text ?? "AnonymousClass";
830
969
  const fields = [];
831
970
  const fieldLayouts = [];
832
971
  const typeRegistry = {};
972
+ const annotations = extractJSDocAnnotationNodes(classDecl, file);
833
973
  const visiting = /* @__PURE__ */ new Set();
834
974
  const instanceMethods = [];
835
975
  const staticMethods = [];
@@ -852,12 +992,21 @@ function analyzeClassToIR(classDecl, checker, file = "") {
852
992
  }
853
993
  }
854
994
  }
855
- return { name, fields, fieldLayouts, typeRegistry, instanceMethods, staticMethods };
995
+ return {
996
+ name,
997
+ fields,
998
+ fieldLayouts,
999
+ typeRegistry,
1000
+ ...annotations.length > 0 && { annotations },
1001
+ instanceMethods,
1002
+ staticMethods
1003
+ };
856
1004
  }
857
1005
  function analyzeInterfaceToIR(interfaceDecl, checker, file = "") {
858
1006
  const name = interfaceDecl.name.text;
859
1007
  const fields = [];
860
1008
  const typeRegistry = {};
1009
+ const annotations = extractJSDocAnnotationNodes(interfaceDecl, file);
861
1010
  const visiting = /* @__PURE__ */ new Set();
862
1011
  for (const member of interfaceDecl.members) {
863
1012
  if (ts4.isPropertySignature(member)) {
@@ -868,7 +1017,15 @@ function analyzeInterfaceToIR(interfaceDecl, checker, file = "") {
868
1017
  }
869
1018
  }
870
1019
  const fieldLayouts = fields.map(() => ({}));
871
- return { name, fields, fieldLayouts, typeRegistry, instanceMethods: [], staticMethods: [] };
1020
+ return {
1021
+ name,
1022
+ fields,
1023
+ fieldLayouts,
1024
+ typeRegistry,
1025
+ ...annotations.length > 0 && { annotations },
1026
+ instanceMethods: [],
1027
+ staticMethods: []
1028
+ };
872
1029
  }
873
1030
  function analyzeTypeAliasToIR(typeAlias, checker, file = "") {
874
1031
  if (!ts4.isTypeLiteralNode(typeAlias.type)) {
@@ -883,6 +1040,7 @@ function analyzeTypeAliasToIR(typeAlias, checker, file = "") {
883
1040
  const name = typeAlias.name.text;
884
1041
  const fields = [];
885
1042
  const typeRegistry = {};
1043
+ const annotations = extractJSDocAnnotationNodes(typeAlias, file);
886
1044
  const visiting = /* @__PURE__ */ new Set();
887
1045
  for (const member of typeAlias.type.members) {
888
1046
  if (ts4.isPropertySignature(member)) {
@@ -899,6 +1057,7 @@ function analyzeTypeAliasToIR(typeAlias, checker, file = "") {
899
1057
  fields,
900
1058
  fieldLayouts: fields.map(() => ({})),
901
1059
  typeRegistry,
1060
+ ...annotations.length > 0 && { annotations },
902
1061
  instanceMethods: [],
903
1062
  staticMethods: []
904
1063
  }
@@ -912,18 +1071,19 @@ function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting) {
912
1071
  const tsType = checker.getTypeAtLocation(prop);
913
1072
  const optional = prop.questionToken !== void 0;
914
1073
  const provenance = provenanceForNode(prop, file);
915
- const type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting);
1074
+ let type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting, prop);
916
1075
  const constraints = [];
917
1076
  if (prop.type) {
918
1077
  constraints.push(...extractTypeAliasConstraintNodes(prop.type, checker, file));
919
1078
  }
920
1079
  constraints.push(...extractJSDocConstraintNodes(prop, file));
921
- const annotations = [];
1080
+ let annotations = [];
922
1081
  annotations.push(...extractJSDocAnnotationNodes(prop, file));
923
1082
  const defaultAnnotation = extractDefaultValueAnnotation(prop.initializer, file);
924
- if (defaultAnnotation) {
1083
+ if (defaultAnnotation && !annotations.some((a) => a.annotationKind === "defaultValue")) {
925
1084
  annotations.push(defaultAnnotation);
926
1085
  }
1086
+ ({ type, annotations } = applyEnumMemberDisplayNames(type, annotations));
927
1087
  return {
928
1088
  kind: "field",
929
1089
  name,
@@ -942,14 +1102,15 @@ function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visitin
942
1102
  const tsType = checker.getTypeAtLocation(prop);
943
1103
  const optional = prop.questionToken !== void 0;
944
1104
  const provenance = provenanceForNode(prop, file);
945
- const type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting);
1105
+ let type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting, prop);
946
1106
  const constraints = [];
947
1107
  if (prop.type) {
948
1108
  constraints.push(...extractTypeAliasConstraintNodes(prop.type, checker, file));
949
1109
  }
950
1110
  constraints.push(...extractJSDocConstraintNodes(prop, file));
951
- const annotations = [];
1111
+ let annotations = [];
952
1112
  annotations.push(...extractJSDocAnnotationNodes(prop, file));
1113
+ ({ type, annotations } = applyEnumMemberDisplayNames(type, annotations));
953
1114
  return {
954
1115
  kind: "field",
955
1116
  name,
@@ -960,7 +1121,69 @@ function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visitin
960
1121
  provenance
961
1122
  };
962
1123
  }
963
- function resolveTypeNode(type, checker, file, typeRegistry, visiting) {
1124
+ function applyEnumMemberDisplayNames(type, annotations) {
1125
+ if (!annotations.some(
1126
+ (annotation) => annotation.annotationKind === "displayName" && annotation.value.trim().startsWith(":")
1127
+ )) {
1128
+ return { type, annotations: [...annotations] };
1129
+ }
1130
+ const consumed = /* @__PURE__ */ new Set();
1131
+ const nextType = rewriteEnumDisplayNames(type, annotations, consumed);
1132
+ if (consumed.size === 0) {
1133
+ return { type, annotations: [...annotations] };
1134
+ }
1135
+ return {
1136
+ type: nextType,
1137
+ annotations: annotations.filter((annotation) => !consumed.has(annotation))
1138
+ };
1139
+ }
1140
+ function rewriteEnumDisplayNames(type, annotations, consumed) {
1141
+ switch (type.kind) {
1142
+ case "enum":
1143
+ return applyEnumMemberDisplayNamesToEnum(type, annotations, consumed);
1144
+ case "union": {
1145
+ return {
1146
+ ...type,
1147
+ members: type.members.map(
1148
+ (member) => rewriteEnumDisplayNames(member, annotations, consumed)
1149
+ )
1150
+ };
1151
+ }
1152
+ default:
1153
+ return type;
1154
+ }
1155
+ }
1156
+ function applyEnumMemberDisplayNamesToEnum(type, annotations, consumed) {
1157
+ const displayNames = /* @__PURE__ */ new Map();
1158
+ for (const annotation of annotations) {
1159
+ if (annotation.annotationKind !== "displayName") continue;
1160
+ const parsed = parseEnumMemberDisplayName(annotation.value);
1161
+ if (!parsed) continue;
1162
+ consumed.add(annotation);
1163
+ const member = type.members.find((m) => String(m.value) === parsed.value);
1164
+ if (!member) continue;
1165
+ displayNames.set(String(member.value), parsed.label);
1166
+ }
1167
+ if (displayNames.size === 0) {
1168
+ return type;
1169
+ }
1170
+ return {
1171
+ ...type,
1172
+ members: type.members.map((member) => {
1173
+ const displayName = displayNames.get(String(member.value));
1174
+ return displayName !== void 0 ? { ...member, displayName } : member;
1175
+ })
1176
+ };
1177
+ }
1178
+ function parseEnumMemberDisplayName(value) {
1179
+ const trimmed = value.trim();
1180
+ const match = /^:([^\s]+)\s+([\s\S]+)$/.exec(trimmed);
1181
+ if (!match?.[1] || !match[2]) return null;
1182
+ const label = match[2].trim();
1183
+ if (label === "") return null;
1184
+ return { value: match[1], label };
1185
+ }
1186
+ function resolveTypeNode(type, checker, file, typeRegistry, visiting, sourceNode) {
964
1187
  if (type.flags & ts4.TypeFlags.String) {
965
1188
  return { kind: "primitive", primitiveKind: "string" };
966
1189
  }
@@ -989,7 +1212,7 @@ function resolveTypeNode(type, checker, file, typeRegistry, visiting) {
989
1212
  };
990
1213
  }
991
1214
  if (type.isUnion()) {
992
- return resolveUnionType(type, checker, file, typeRegistry, visiting);
1215
+ return resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNode);
993
1216
  }
994
1217
  if (checker.isArrayType(type)) {
995
1218
  return resolveArrayType(type, checker, file, typeRegistry, visiting);
@@ -999,70 +1222,102 @@ function resolveTypeNode(type, checker, file, typeRegistry, visiting) {
999
1222
  }
1000
1223
  return { kind: "primitive", primitiveKind: "string" };
1001
1224
  }
1002
- function resolveUnionType(type, checker, file, typeRegistry, visiting) {
1225
+ function resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNode) {
1226
+ const typeName = getNamedTypeName(type);
1227
+ const namedDecl = getNamedTypeDeclaration(type);
1228
+ if (typeName && typeName in typeRegistry) {
1229
+ return { kind: "reference", name: typeName, typeArguments: [] };
1230
+ }
1003
1231
  const allTypes = type.types;
1004
1232
  const nonNullTypes = allTypes.filter(
1005
1233
  (t) => !(t.flags & (ts4.TypeFlags.Null | ts4.TypeFlags.Undefined))
1006
1234
  );
1007
1235
  const hasNull = allTypes.some((t) => t.flags & ts4.TypeFlags.Null);
1236
+ const memberDisplayNames = /* @__PURE__ */ new Map();
1237
+ if (namedDecl) {
1238
+ for (const [value, label] of extractDisplayNameMetadata(namedDecl).memberDisplayNames) {
1239
+ memberDisplayNames.set(value, label);
1240
+ }
1241
+ }
1242
+ if (sourceNode) {
1243
+ for (const [value, label] of extractDisplayNameMetadata(sourceNode).memberDisplayNames) {
1244
+ memberDisplayNames.set(value, label);
1245
+ }
1246
+ }
1247
+ const registerNamed = (result) => {
1248
+ if (!typeName) {
1249
+ return result;
1250
+ }
1251
+ const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file) : void 0;
1252
+ typeRegistry[typeName] = {
1253
+ name: typeName,
1254
+ type: result,
1255
+ ...annotations !== void 0 && annotations.length > 0 && { annotations },
1256
+ provenance: provenanceForDeclaration(namedDecl ?? sourceNode, file)
1257
+ };
1258
+ return { kind: "reference", name: typeName, typeArguments: [] };
1259
+ };
1260
+ const applyMemberLabels = (members2) => members2.map((value) => {
1261
+ const displayName = memberDisplayNames.get(String(value));
1262
+ return displayName !== void 0 ? { value, displayName } : { value };
1263
+ });
1008
1264
  const isBooleanUnion2 = nonNullTypes.length === 2 && nonNullTypes.every((t) => t.flags & ts4.TypeFlags.BooleanLiteral);
1009
1265
  if (isBooleanUnion2) {
1010
1266
  const boolNode = { kind: "primitive", primitiveKind: "boolean" };
1011
- if (hasNull) {
1012
- return {
1013
- kind: "union",
1014
- members: [boolNode, { kind: "primitive", primitiveKind: "null" }]
1015
- };
1016
- }
1017
- return boolNode;
1267
+ const result = hasNull ? {
1268
+ kind: "union",
1269
+ members: [boolNode, { kind: "primitive", primitiveKind: "null" }]
1270
+ } : boolNode;
1271
+ return registerNamed(result);
1018
1272
  }
1019
1273
  const allStringLiterals = nonNullTypes.every((t) => t.isStringLiteral());
1020
1274
  if (allStringLiterals && nonNullTypes.length > 0) {
1021
1275
  const stringTypes = nonNullTypes.filter((t) => t.isStringLiteral());
1022
1276
  const enumNode = {
1023
1277
  kind: "enum",
1024
- members: stringTypes.map((t) => ({ value: t.value }))
1278
+ members: applyMemberLabels(stringTypes.map((t) => t.value))
1025
1279
  };
1026
- if (hasNull) {
1027
- return {
1028
- kind: "union",
1029
- members: [enumNode, { kind: "primitive", primitiveKind: "null" }]
1030
- };
1031
- }
1032
- return enumNode;
1280
+ const result = hasNull ? {
1281
+ kind: "union",
1282
+ members: [enumNode, { kind: "primitive", primitiveKind: "null" }]
1283
+ } : enumNode;
1284
+ return registerNamed(result);
1033
1285
  }
1034
1286
  const allNumberLiterals = nonNullTypes.every((t) => t.isNumberLiteral());
1035
1287
  if (allNumberLiterals && nonNullTypes.length > 0) {
1036
1288
  const numberTypes = nonNullTypes.filter((t) => t.isNumberLiteral());
1037
1289
  const enumNode = {
1038
1290
  kind: "enum",
1039
- members: numberTypes.map((t) => ({ value: t.value }))
1291
+ members: applyMemberLabels(numberTypes.map((t) => t.value))
1040
1292
  };
1041
- if (hasNull) {
1042
- return {
1043
- kind: "union",
1044
- members: [enumNode, { kind: "primitive", primitiveKind: "null" }]
1045
- };
1046
- }
1047
- return enumNode;
1293
+ const result = hasNull ? {
1294
+ kind: "union",
1295
+ members: [enumNode, { kind: "primitive", primitiveKind: "null" }]
1296
+ } : enumNode;
1297
+ return registerNamed(result);
1048
1298
  }
1049
1299
  if (nonNullTypes.length === 1 && nonNullTypes[0]) {
1050
- const inner = resolveTypeNode(nonNullTypes[0], checker, file, typeRegistry, visiting);
1051
- if (hasNull) {
1052
- return {
1053
- kind: "union",
1054
- members: [inner, { kind: "primitive", primitiveKind: "null" }]
1055
- };
1056
- }
1057
- return inner;
1300
+ const inner = resolveTypeNode(
1301
+ nonNullTypes[0],
1302
+ checker,
1303
+ file,
1304
+ typeRegistry,
1305
+ visiting,
1306
+ sourceNode
1307
+ );
1308
+ const result = hasNull ? {
1309
+ kind: "union",
1310
+ members: [inner, { kind: "primitive", primitiveKind: "null" }]
1311
+ } : inner;
1312
+ return registerNamed(result);
1058
1313
  }
1059
1314
  const members = nonNullTypes.map(
1060
- (t) => resolveTypeNode(t, checker, file, typeRegistry, visiting)
1315
+ (t) => resolveTypeNode(t, checker, file, typeRegistry, visiting, sourceNode)
1061
1316
  );
1062
1317
  if (hasNull) {
1063
1318
  members.push({ kind: "primitive", primitiveKind: "null" });
1064
1319
  }
1065
- return { kind: "union", members };
1320
+ return registerNamed({ kind: "union", members });
1066
1321
  }
1067
1322
  function resolveArrayType(type, checker, file, typeRegistry, visiting) {
1068
1323
  const typeArgs = isTypeReference(type) ? type.typeArguments : void 0;
@@ -1070,15 +1325,92 @@ function resolveArrayType(type, checker, file, typeRegistry, visiting) {
1070
1325
  const items = elementType ? resolveTypeNode(elementType, checker, file, typeRegistry, visiting) : { kind: "primitive", primitiveKind: "string" };
1071
1326
  return { kind: "array", items };
1072
1327
  }
1328
+ function tryResolveRecordType(type, checker, file, typeRegistry, visiting) {
1329
+ if (type.getProperties().length > 0) {
1330
+ return null;
1331
+ }
1332
+ const indexInfo = checker.getIndexInfoOfType(type, ts4.IndexKind.String);
1333
+ if (!indexInfo) {
1334
+ return null;
1335
+ }
1336
+ const valueType = resolveTypeNode(indexInfo.type, checker, file, typeRegistry, visiting);
1337
+ return { kind: "record", valueType };
1338
+ }
1339
+ function typeNodeContainsReference(type, targetName) {
1340
+ switch (type.kind) {
1341
+ case "reference":
1342
+ return type.name === targetName;
1343
+ case "array":
1344
+ return typeNodeContainsReference(type.items, targetName);
1345
+ case "record":
1346
+ return typeNodeContainsReference(type.valueType, targetName);
1347
+ case "union":
1348
+ return type.members.some((member) => typeNodeContainsReference(member, targetName));
1349
+ case "object":
1350
+ return type.properties.some(
1351
+ (property) => typeNodeContainsReference(property.type, targetName)
1352
+ );
1353
+ case "primitive":
1354
+ case "enum":
1355
+ case "dynamic":
1356
+ case "custom":
1357
+ return false;
1358
+ default: {
1359
+ const _exhaustive = type;
1360
+ return _exhaustive;
1361
+ }
1362
+ }
1363
+ }
1073
1364
  function resolveObjectType(type, checker, file, typeRegistry, visiting) {
1365
+ const typeName = getNamedTypeName(type);
1366
+ const namedTypeName = typeName ?? void 0;
1367
+ const namedDecl = getNamedTypeDeclaration(type);
1368
+ const shouldRegisterNamedType = namedTypeName !== void 0 && !(namedTypeName === "Record" && namedDecl?.getSourceFile().fileName !== file);
1369
+ const clearNamedTypeRegistration = () => {
1370
+ if (namedTypeName === void 0 || !shouldRegisterNamedType) {
1371
+ return;
1372
+ }
1373
+ Reflect.deleteProperty(typeRegistry, namedTypeName);
1374
+ };
1074
1375
  if (visiting.has(type)) {
1376
+ if (namedTypeName !== void 0 && shouldRegisterNamedType) {
1377
+ return { kind: "reference", name: namedTypeName, typeArguments: [] };
1378
+ }
1075
1379
  return { kind: "object", properties: [], additionalProperties: false };
1076
1380
  }
1381
+ if (namedTypeName !== void 0 && shouldRegisterNamedType && !typeRegistry[namedTypeName]) {
1382
+ typeRegistry[namedTypeName] = {
1383
+ name: namedTypeName,
1384
+ type: RESOLVING_TYPE_PLACEHOLDER,
1385
+ provenance: provenanceForDeclaration(namedDecl, file)
1386
+ };
1387
+ }
1077
1388
  visiting.add(type);
1078
- const typeName = getNamedTypeName(type);
1079
- if (typeName && typeName in typeRegistry) {
1389
+ if (namedTypeName !== void 0 && shouldRegisterNamedType && typeRegistry[namedTypeName]?.type !== void 0) {
1390
+ if (typeRegistry[namedTypeName].type !== RESOLVING_TYPE_PLACEHOLDER) {
1391
+ visiting.delete(type);
1392
+ return { kind: "reference", name: namedTypeName, typeArguments: [] };
1393
+ }
1394
+ }
1395
+ const recordNode = tryResolveRecordType(type, checker, file, typeRegistry, visiting);
1396
+ if (recordNode) {
1080
1397
  visiting.delete(type);
1081
- return { kind: "reference", name: typeName, typeArguments: [] };
1398
+ if (namedTypeName !== void 0 && shouldRegisterNamedType) {
1399
+ const isRecursiveRecord = typeNodeContainsReference(recordNode.valueType, namedTypeName);
1400
+ if (!isRecursiveRecord) {
1401
+ clearNamedTypeRegistration();
1402
+ return recordNode;
1403
+ }
1404
+ const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file) : void 0;
1405
+ typeRegistry[namedTypeName] = {
1406
+ name: namedTypeName,
1407
+ type: recordNode,
1408
+ ...annotations !== void 0 && annotations.length > 0 && { annotations },
1409
+ provenance: provenanceForDeclaration(namedDecl, file)
1410
+ };
1411
+ return { kind: "reference", name: namedTypeName, typeArguments: [] };
1412
+ }
1413
+ return recordNode;
1082
1414
  }
1083
1415
  const properties = [];
1084
1416
  const fieldInfoMap = getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visiting);
@@ -1087,7 +1419,14 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting) {
1087
1419
  if (!declaration) continue;
1088
1420
  const propType = checker.getTypeOfSymbolAtLocation(prop, declaration);
1089
1421
  const optional = !!(prop.flags & ts4.SymbolFlags.Optional);
1090
- const propTypeNode = resolveTypeNode(propType, checker, file, typeRegistry, visiting);
1422
+ const propTypeNode = resolveTypeNode(
1423
+ propType,
1424
+ checker,
1425
+ file,
1426
+ typeRegistry,
1427
+ visiting,
1428
+ declaration
1429
+ );
1091
1430
  const fieldNodeInfo = fieldInfoMap?.get(prop.name);
1092
1431
  properties.push({
1093
1432
  name: prop.name,
@@ -1102,15 +1441,17 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting) {
1102
1441
  const objectNode = {
1103
1442
  kind: "object",
1104
1443
  properties,
1105
- additionalProperties: false
1444
+ additionalProperties: true
1106
1445
  };
1107
- if (typeName) {
1108
- typeRegistry[typeName] = {
1109
- name: typeName,
1446
+ if (namedTypeName !== void 0 && shouldRegisterNamedType) {
1447
+ const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file) : void 0;
1448
+ typeRegistry[namedTypeName] = {
1449
+ name: namedTypeName,
1110
1450
  type: objectNode,
1111
- provenance: provenanceForFile(file)
1451
+ ...annotations !== void 0 && annotations.length > 0 && { annotations },
1452
+ provenance: provenanceForDeclaration(namedDecl, file)
1112
1453
  };
1113
- return { kind: "reference", name: typeName, typeArguments: [] };
1454
+ return { kind: "reference", name: namedTypeName, typeArguments: [] };
1114
1455
  }
1115
1456
  return objectNode;
1116
1457
  }
@@ -1202,6 +1543,12 @@ function provenanceForNode(node, file) {
1202
1543
  function provenanceForFile(file) {
1203
1544
  return { surface: "tsdoc", file, line: 0, column: 0 };
1204
1545
  }
1546
+ function provenanceForDeclaration(node, file) {
1547
+ if (!node) {
1548
+ return provenanceForFile(file);
1549
+ }
1550
+ return provenanceForNode(node, file);
1551
+ }
1205
1552
  function getNamedTypeName(type) {
1206
1553
  const symbol = type.getSymbol();
1207
1554
  if (symbol?.declarations) {
@@ -1220,6 +1567,20 @@ function getNamedTypeName(type) {
1220
1567
  }
1221
1568
  return null;
1222
1569
  }
1570
+ function getNamedTypeDeclaration(type) {
1571
+ const symbol = type.getSymbol();
1572
+ if (symbol?.declarations) {
1573
+ const decl = symbol.declarations[0];
1574
+ if (decl && (ts4.isClassDeclaration(decl) || ts4.isInterfaceDeclaration(decl) || ts4.isTypeAliasDeclaration(decl))) {
1575
+ return decl;
1576
+ }
1577
+ }
1578
+ const aliasSymbol = type.aliasSymbol;
1579
+ if (aliasSymbol?.declarations) {
1580
+ return aliasSymbol.declarations.find(ts4.isTypeAliasDeclaration);
1581
+ }
1582
+ return void 0;
1583
+ }
1223
1584
  function analyzeMethod(method, checker) {
1224
1585
  if (!ts4.isIdentifier(method.name)) {
1225
1586
  return null;
@@ -1262,13 +1623,26 @@ function detectFormSpecReference(typeNode) {
1262
1623
  }
1263
1624
 
1264
1625
  // src/json-schema/ir-generator.ts
1265
- function makeContext() {
1266
- return { defs: {} };
1626
+ function makeContext(options) {
1627
+ const vendorPrefix = options?.vendorPrefix ?? "x-formspec";
1628
+ if (!vendorPrefix.startsWith("x-")) {
1629
+ throw new Error(
1630
+ `Invalid vendorPrefix "${vendorPrefix}". Extension JSON Schema keywords must start with "x-".`
1631
+ );
1632
+ }
1633
+ return {
1634
+ defs: {},
1635
+ extensionRegistry: options?.extensionRegistry,
1636
+ vendorPrefix
1637
+ };
1267
1638
  }
1268
- function generateJsonSchemaFromIR(ir) {
1269
- const ctx = makeContext();
1639
+ function generateJsonSchemaFromIR(ir, options) {
1640
+ const ctx = makeContext(options);
1270
1641
  for (const [name, typeDef] of Object.entries(ir.typeRegistry)) {
1271
1642
  ctx.defs[name] = generateTypeNode(typeDef.type, ctx);
1643
+ if (typeDef.annotations && typeDef.annotations.length > 0) {
1644
+ applyAnnotations(ctx.defs[name], typeDef.annotations, ctx);
1645
+ }
1272
1646
  }
1273
1647
  const properties = {};
1274
1648
  const required = [];
@@ -1280,6 +1654,9 @@ function generateJsonSchemaFromIR(ir) {
1280
1654
  properties,
1281
1655
  ...uniqueRequired.length > 0 && { required: uniqueRequired }
1282
1656
  };
1657
+ if (ir.annotations && ir.annotations.length > 0) {
1658
+ applyAnnotations(result, ir.annotations, ctx);
1659
+ }
1283
1660
  if (Object.keys(ctx.defs).length > 0) {
1284
1661
  result.$defs = ctx.defs;
1285
1662
  }
@@ -1309,25 +1686,54 @@ function collectFields(elements, properties, required, ctx) {
1309
1686
  }
1310
1687
  function generateFieldSchema(field, ctx) {
1311
1688
  const schema = generateTypeNode(field.type, ctx);
1689
+ const itemStringSchema = schema.type === "array" && schema.items?.type === "string" ? schema.items : void 0;
1312
1690
  const directConstraints = [];
1691
+ const itemConstraints = [];
1313
1692
  const pathConstraints = [];
1314
1693
  for (const c of field.constraints) {
1315
1694
  if (c.path) {
1316
1695
  pathConstraints.push(c);
1696
+ } else if (itemStringSchema !== void 0 && isStringItemConstraint(c)) {
1697
+ itemConstraints.push(c);
1317
1698
  } else {
1318
1699
  directConstraints.push(c);
1319
1700
  }
1320
1701
  }
1321
- applyConstraints(schema, directConstraints);
1322
- applyAnnotations(schema, field.annotations);
1702
+ applyConstraints(schema, directConstraints, ctx);
1703
+ if (itemStringSchema !== void 0) {
1704
+ applyConstraints(itemStringSchema, itemConstraints, ctx);
1705
+ }
1706
+ const rootAnnotations = [];
1707
+ const itemAnnotations = [];
1708
+ for (const annotation of field.annotations) {
1709
+ if (itemStringSchema !== void 0 && annotation.annotationKind === "format") {
1710
+ itemAnnotations.push(annotation);
1711
+ } else {
1712
+ rootAnnotations.push(annotation);
1713
+ }
1714
+ }
1715
+ applyAnnotations(schema, rootAnnotations, ctx);
1716
+ if (itemStringSchema !== void 0) {
1717
+ applyAnnotations(itemStringSchema, itemAnnotations, ctx);
1718
+ }
1323
1719
  if (pathConstraints.length === 0) {
1324
1720
  return schema;
1325
1721
  }
1326
- return applyPathTargetedConstraints(schema, pathConstraints);
1722
+ return applyPathTargetedConstraints(schema, pathConstraints, ctx);
1327
1723
  }
1328
- function applyPathTargetedConstraints(schema, pathConstraints) {
1724
+ function isStringItemConstraint(constraint) {
1725
+ switch (constraint.constraintKind) {
1726
+ case "minLength":
1727
+ case "maxLength":
1728
+ case "pattern":
1729
+ return true;
1730
+ default:
1731
+ return false;
1732
+ }
1733
+ }
1734
+ function applyPathTargetedConstraints(schema, pathConstraints, ctx) {
1329
1735
  if (schema.type === "array" && schema.items) {
1330
- schema.items = applyPathTargetedConstraints(schema.items, pathConstraints);
1736
+ schema.items = applyPathTargetedConstraints(schema.items, pathConstraints, ctx);
1331
1737
  return schema;
1332
1738
  }
1333
1739
  const byTarget = /* @__PURE__ */ new Map();
@@ -1341,7 +1747,7 @@ function applyPathTargetedConstraints(schema, pathConstraints) {
1341
1747
  const propertyOverrides = {};
1342
1748
  for (const [target, constraints] of byTarget) {
1343
1749
  const subSchema = {};
1344
- applyConstraints(subSchema, constraints);
1750
+ applyConstraints(subSchema, constraints, ctx);
1345
1751
  propertyOverrides[target] = subSchema;
1346
1752
  }
1347
1753
  if (schema.$ref) {
@@ -1385,6 +1791,8 @@ function generateTypeNode(type, ctx) {
1385
1791
  return generateArrayType(type, ctx);
1386
1792
  case "object":
1387
1793
  return generateObjectType(type, ctx);
1794
+ case "record":
1795
+ return generateRecordType(type, ctx);
1388
1796
  case "union":
1389
1797
  return generateUnionType(type, ctx);
1390
1798
  case "reference":
@@ -1392,7 +1800,7 @@ function generateTypeNode(type, ctx) {
1392
1800
  case "dynamic":
1393
1801
  return generateDynamicType(type);
1394
1802
  case "custom":
1395
- return generateCustomType(type);
1803
+ return generateCustomType(type, ctx);
1396
1804
  default: {
1397
1805
  const _exhaustive = type;
1398
1806
  return _exhaustive;
@@ -1441,16 +1849,27 @@ function generateObjectType(type, ctx) {
1441
1849
  }
1442
1850
  return schema;
1443
1851
  }
1852
+ function generateRecordType(type, ctx) {
1853
+ return {
1854
+ type: "object",
1855
+ additionalProperties: generateTypeNode(type.valueType, ctx)
1856
+ };
1857
+ }
1444
1858
  function generatePropertySchema(prop, ctx) {
1445
1859
  const schema = generateTypeNode(prop.type, ctx);
1446
- applyConstraints(schema, prop.constraints);
1447
- applyAnnotations(schema, prop.annotations);
1860
+ applyConstraints(schema, prop.constraints, ctx);
1861
+ applyAnnotations(schema, prop.annotations, ctx);
1448
1862
  return schema;
1449
1863
  }
1450
1864
  function generateUnionType(type, ctx) {
1451
1865
  if (isBooleanUnion(type)) {
1452
1866
  return { type: "boolean" };
1453
1867
  }
1868
+ if (isNullableUnion(type)) {
1869
+ return {
1870
+ oneOf: type.members.map((m) => generateTypeNode(m, ctx))
1871
+ };
1872
+ }
1454
1873
  return {
1455
1874
  anyOf: type.members.map((m) => generateTypeNode(m, ctx))
1456
1875
  };
@@ -1460,6 +1879,13 @@ function isBooleanUnion(type) {
1460
1879
  const kinds = type.members.map((m) => m.kind);
1461
1880
  return kinds.every((k) => k === "primitive") && type.members.every((m) => m.kind === "primitive" && m.primitiveKind === "boolean");
1462
1881
  }
1882
+ function isNullableUnion(type) {
1883
+ if (type.members.length !== 2) return false;
1884
+ const nullCount = type.members.filter(
1885
+ (m) => m.kind === "primitive" && m.primitiveKind === "null"
1886
+ ).length;
1887
+ return nullCount === 1;
1888
+ }
1463
1889
  function generateReferenceType(type) {
1464
1890
  return { $ref: `#/$defs/${type.name}` };
1465
1891
  }
@@ -1480,10 +1906,7 @@ function generateDynamicType(type) {
1480
1906
  "x-formspec-schemaSource": type.sourceKey
1481
1907
  };
1482
1908
  }
1483
- function generateCustomType(_type) {
1484
- return { type: "object" };
1485
- }
1486
- function applyConstraints(schema, constraints) {
1909
+ function applyConstraints(schema, constraints, ctx) {
1487
1910
  for (const constraint of constraints) {
1488
1911
  switch (constraint.constraintKind) {
1489
1912
  case "minimum":
@@ -1525,9 +1948,13 @@ function applyConstraints(schema, constraints) {
1525
1948
  case "uniqueItems":
1526
1949
  schema.uniqueItems = constraint.value;
1527
1950
  break;
1951
+ case "const":
1952
+ schema.const = constraint.value;
1953
+ break;
1528
1954
  case "allowedMembers":
1529
1955
  break;
1530
1956
  case "custom":
1957
+ applyCustomConstraint(schema, constraint, ctx);
1531
1958
  break;
1532
1959
  default: {
1533
1960
  const _exhaustive = constraint;
@@ -1536,7 +1963,7 @@ function applyConstraints(schema, constraints) {
1536
1963
  }
1537
1964
  }
1538
1965
  }
1539
- function applyAnnotations(schema, annotations) {
1966
+ function applyAnnotations(schema, annotations, ctx) {
1540
1967
  for (const annotation of annotations) {
1541
1968
  switch (annotation.annotationKind) {
1542
1969
  case "displayName":
@@ -1548,14 +1975,21 @@ function applyAnnotations(schema, annotations) {
1548
1975
  case "defaultValue":
1549
1976
  schema.default = annotation.value;
1550
1977
  break;
1978
+ case "format":
1979
+ schema.format = annotation.value;
1980
+ break;
1551
1981
  case "deprecated":
1552
1982
  schema.deprecated = true;
1983
+ if (annotation.message !== void 0 && annotation.message !== "") {
1984
+ schema["x-formspec-deprecation-description"] = annotation.message;
1985
+ }
1553
1986
  break;
1554
1987
  case "placeholder":
1555
1988
  break;
1556
1989
  case "formatHint":
1557
1990
  break;
1558
1991
  case "custom":
1992
+ applyCustomAnnotation(schema, annotation, ctx);
1559
1993
  break;
1560
1994
  default: {
1561
1995
  const _exhaustive = annotation;
@@ -1564,6 +1998,36 @@ function applyAnnotations(schema, annotations) {
1564
1998
  }
1565
1999
  }
1566
2000
  }
2001
+ function generateCustomType(type, ctx) {
2002
+ const registration = ctx.extensionRegistry?.findType(type.typeId);
2003
+ if (registration === void 0) {
2004
+ throw new Error(
2005
+ `Cannot generate JSON Schema for custom type "${type.typeId}" without a matching extension registration`
2006
+ );
2007
+ }
2008
+ return registration.toJsonSchema(type.payload, ctx.vendorPrefix);
2009
+ }
2010
+ function applyCustomConstraint(schema, constraint, ctx) {
2011
+ const registration = ctx.extensionRegistry?.findConstraint(constraint.constraintId);
2012
+ if (registration === void 0) {
2013
+ throw new Error(
2014
+ `Cannot generate JSON Schema for custom constraint "${constraint.constraintId}" without a matching extension registration`
2015
+ );
2016
+ }
2017
+ Object.assign(schema, registration.toJsonSchema(constraint.payload, ctx.vendorPrefix));
2018
+ }
2019
+ function applyCustomAnnotation(schema, annotation, ctx) {
2020
+ const registration = ctx.extensionRegistry?.findAnnotation(annotation.annotationId);
2021
+ if (registration === void 0) {
2022
+ throw new Error(
2023
+ `Cannot generate JSON Schema for custom annotation "${annotation.annotationId}" without a matching extension registration`
2024
+ );
2025
+ }
2026
+ if (registration.toJsonSchema === void 0) {
2027
+ return;
2028
+ }
2029
+ Object.assign(schema, registration.toJsonSchema(annotation.value, ctx.vendorPrefix));
2030
+ }
1567
2031
 
1568
2032
  // src/ui-schema/schema.ts
1569
2033
  var import_zod = require("zod");
@@ -1700,25 +2164,31 @@ function createShowRule(fieldName, value) {
1700
2164
  }
1701
2165
  };
1702
2166
  }
2167
+ function flattenConditionSchema(scope, schema) {
2168
+ if (schema.allOf === void 0) {
2169
+ if (scope === "#") {
2170
+ return [schema];
2171
+ }
2172
+ const fieldName = scope.replace("#/properties/", "");
2173
+ return [
2174
+ {
2175
+ properties: {
2176
+ [fieldName]: schema
2177
+ }
2178
+ }
2179
+ ];
2180
+ }
2181
+ return schema.allOf.flatMap((member) => flattenConditionSchema(scope, member));
2182
+ }
1703
2183
  function combineRules(parentRule, childRule) {
1704
- const parentCondition = parentRule.condition;
1705
- const childCondition = childRule.condition;
1706
2184
  return {
1707
2185
  effect: "SHOW",
1708
2186
  condition: {
1709
2187
  scope: "#",
1710
2188
  schema: {
1711
2189
  allOf: [
1712
- {
1713
- properties: {
1714
- [parentCondition.scope.replace("#/properties/", "")]: parentCondition.schema
1715
- }
1716
- },
1717
- {
1718
- properties: {
1719
- [childCondition.scope.replace("#/properties/", "")]: childCondition.schema
1720
- }
1721
- }
2190
+ ...flattenConditionSchema(parentRule.condition.scope, parentRule.condition.schema),
2191
+ ...flattenConditionSchema(childRule.condition.scope, childRule.condition.schema)
1722
2192
  ]
1723
2193
  }
1724
2194
  }
@@ -1726,10 +2196,14 @@ function combineRules(parentRule, childRule) {
1726
2196
  }
1727
2197
  function fieldNodeToControl(field, parentRule) {
1728
2198
  const displayNameAnnotation = field.annotations.find((a) => a.annotationKind === "displayName");
2199
+ const placeholderAnnotation = field.annotations.find((a) => a.annotationKind === "placeholder");
1729
2200
  const control = {
1730
2201
  type: "Control",
1731
2202
  scope: fieldToScope(field.name),
1732
2203
  ...displayNameAnnotation !== void 0 && { label: displayNameAnnotation.value },
2204
+ ...placeholderAnnotation !== void 0 && {
2205
+ options: { placeholder: placeholderAnnotation.value }
2206
+ },
1733
2207
  ...parentRule !== void 0 && { rule: parentRule }
1734
2208
  };
1735
2209
  return control;
@@ -1788,12 +2262,9 @@ function generateClassSchemas(analysis, source) {
1788
2262
  }
1789
2263
 
1790
2264
  // src/validate/constraint-validator.ts
1791
- function makeCode(ctx, category, number) {
1792
- return `${ctx.vendorPrefix}-${category}-${String(number).padStart(3, "0")}`;
1793
- }
1794
2265
  function addContradiction(ctx, message, primary, related) {
1795
2266
  ctx.diagnostics.push({
1796
- code: makeCode(ctx, "CONTRADICTION", 1),
2267
+ code: "CONTRADICTING_CONSTRAINTS",
1797
2268
  message,
1798
2269
  severity: "error",
1799
2270
  primaryLocation: primary,
@@ -1802,7 +2273,7 @@ function addContradiction(ctx, message, primary, related) {
1802
2273
  }
1803
2274
  function addTypeMismatch(ctx, message, primary) {
1804
2275
  ctx.diagnostics.push({
1805
- code: makeCode(ctx, "TYPE_MISMATCH", 1),
2276
+ code: "TYPE_MISMATCH",
1806
2277
  message,
1807
2278
  severity: "error",
1808
2279
  primaryLocation: primary,
@@ -1811,13 +2282,31 @@ function addTypeMismatch(ctx, message, primary) {
1811
2282
  }
1812
2283
  function addUnknownExtension(ctx, message, primary) {
1813
2284
  ctx.diagnostics.push({
1814
- code: makeCode(ctx, "UNKNOWN_EXTENSION", 1),
2285
+ code: "UNKNOWN_EXTENSION",
1815
2286
  message,
1816
2287
  severity: "warning",
1817
2288
  primaryLocation: primary,
1818
2289
  relatedLocations: []
1819
2290
  });
1820
2291
  }
2292
+ function addUnknownPathTarget(ctx, message, primary) {
2293
+ ctx.diagnostics.push({
2294
+ code: "UNKNOWN_PATH_TARGET",
2295
+ message,
2296
+ severity: "error",
2297
+ primaryLocation: primary,
2298
+ relatedLocations: []
2299
+ });
2300
+ }
2301
+ function addConstraintBroadening(ctx, message, primary, related) {
2302
+ ctx.diagnostics.push({
2303
+ code: "CONSTRAINT_BROADENING",
2304
+ message,
2305
+ severity: "error",
2306
+ primaryLocation: primary,
2307
+ relatedLocations: [related]
2308
+ });
2309
+ }
1821
2310
  function findNumeric(constraints, constraintKind) {
1822
2311
  return constraints.find((c) => c.constraintKind === constraintKind);
1823
2312
  }
@@ -1829,6 +2318,165 @@ function findAllowedMembers(constraints) {
1829
2318
  (c) => c.constraintKind === "allowedMembers"
1830
2319
  );
1831
2320
  }
2321
+ function findConstConstraints(constraints) {
2322
+ return constraints.filter(
2323
+ (c) => c.constraintKind === "const"
2324
+ );
2325
+ }
2326
+ function jsonValueEquals(left, right) {
2327
+ if (left === right) {
2328
+ return true;
2329
+ }
2330
+ if (Array.isArray(left) || Array.isArray(right)) {
2331
+ if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) {
2332
+ return false;
2333
+ }
2334
+ return left.every((item, index) => jsonValueEquals(item, right[index]));
2335
+ }
2336
+ if (isJsonObject(left) || isJsonObject(right)) {
2337
+ if (!isJsonObject(left) || !isJsonObject(right)) {
2338
+ return false;
2339
+ }
2340
+ const leftKeys = Object.keys(left).sort();
2341
+ const rightKeys = Object.keys(right).sort();
2342
+ if (leftKeys.length !== rightKeys.length) {
2343
+ return false;
2344
+ }
2345
+ return leftKeys.every((key, index) => {
2346
+ const rightKey = rightKeys[index];
2347
+ if (rightKey !== key) {
2348
+ return false;
2349
+ }
2350
+ const leftValue = left[key];
2351
+ const rightValue = right[rightKey];
2352
+ return leftValue !== void 0 && rightValue !== void 0 && jsonValueEquals(leftValue, rightValue);
2353
+ });
2354
+ }
2355
+ return false;
2356
+ }
2357
+ function isJsonObject(value) {
2358
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2359
+ }
2360
+ function isOrderedBoundConstraint(constraint) {
2361
+ 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";
2362
+ }
2363
+ function pathKey(constraint) {
2364
+ return constraint.path?.segments.join(".") ?? "";
2365
+ }
2366
+ function orderedBoundFamily(kind) {
2367
+ switch (kind) {
2368
+ case "minimum":
2369
+ case "exclusiveMinimum":
2370
+ return "numeric-lower";
2371
+ case "maximum":
2372
+ case "exclusiveMaximum":
2373
+ return "numeric-upper";
2374
+ case "minLength":
2375
+ return "minLength";
2376
+ case "minItems":
2377
+ return "minItems";
2378
+ case "maxLength":
2379
+ return "maxLength";
2380
+ case "maxItems":
2381
+ return "maxItems";
2382
+ default: {
2383
+ const _exhaustive = kind;
2384
+ return _exhaustive;
2385
+ }
2386
+ }
2387
+ }
2388
+ function isNumericLowerKind(kind) {
2389
+ return kind === "minimum" || kind === "exclusiveMinimum";
2390
+ }
2391
+ function isNumericUpperKind(kind) {
2392
+ return kind === "maximum" || kind === "exclusiveMaximum";
2393
+ }
2394
+ function describeConstraintTag(constraint) {
2395
+ return `@${constraint.constraintKind}`;
2396
+ }
2397
+ function compareConstraintStrength(current, previous) {
2398
+ const family = orderedBoundFamily(current.constraintKind);
2399
+ if (family === "numeric-lower") {
2400
+ if (!isNumericLowerKind(current.constraintKind) || !isNumericLowerKind(previous.constraintKind)) {
2401
+ throw new Error("numeric-lower family received non-numeric lower-bound constraint");
2402
+ }
2403
+ if (current.value !== previous.value) {
2404
+ return current.value > previous.value ? 1 : -1;
2405
+ }
2406
+ if (current.constraintKind === "exclusiveMinimum" && previous.constraintKind === "minimum") {
2407
+ return 1;
2408
+ }
2409
+ if (current.constraintKind === "minimum" && previous.constraintKind === "exclusiveMinimum") {
2410
+ return -1;
2411
+ }
2412
+ return 0;
2413
+ }
2414
+ if (family === "numeric-upper") {
2415
+ if (!isNumericUpperKind(current.constraintKind) || !isNumericUpperKind(previous.constraintKind)) {
2416
+ throw new Error("numeric-upper family received non-numeric upper-bound constraint");
2417
+ }
2418
+ if (current.value !== previous.value) {
2419
+ return current.value < previous.value ? 1 : -1;
2420
+ }
2421
+ if (current.constraintKind === "exclusiveMaximum" && previous.constraintKind === "maximum") {
2422
+ return 1;
2423
+ }
2424
+ if (current.constraintKind === "maximum" && previous.constraintKind === "exclusiveMaximum") {
2425
+ return -1;
2426
+ }
2427
+ return 0;
2428
+ }
2429
+ switch (family) {
2430
+ case "minLength":
2431
+ case "minItems":
2432
+ if (current.value === previous.value) {
2433
+ return 0;
2434
+ }
2435
+ return current.value > previous.value ? 1 : -1;
2436
+ case "maxLength":
2437
+ case "maxItems":
2438
+ if (current.value === previous.value) {
2439
+ return 0;
2440
+ }
2441
+ return current.value < previous.value ? 1 : -1;
2442
+ default: {
2443
+ const _exhaustive = family;
2444
+ return _exhaustive;
2445
+ }
2446
+ }
2447
+ }
2448
+ function checkConstraintBroadening(ctx, fieldName, constraints) {
2449
+ const strongestByKey = /* @__PURE__ */ new Map();
2450
+ for (const constraint of constraints) {
2451
+ if (!isOrderedBoundConstraint(constraint)) {
2452
+ continue;
2453
+ }
2454
+ const key = `${orderedBoundFamily(constraint.constraintKind)}:${pathKey(constraint)}`;
2455
+ const previous = strongestByKey.get(key);
2456
+ if (previous === void 0) {
2457
+ strongestByKey.set(key, constraint);
2458
+ continue;
2459
+ }
2460
+ const strength = compareConstraintStrength(constraint, previous);
2461
+ if (strength < 0) {
2462
+ const displayFieldName = formatPathTargetFieldName(
2463
+ fieldName,
2464
+ constraint.path?.segments ?? []
2465
+ );
2466
+ addConstraintBroadening(
2467
+ ctx,
2468
+ `Field "${displayFieldName}": ${describeConstraintTag(constraint)} (${String(constraint.value)}) is broader than earlier ${describeConstraintTag(previous)} (${String(previous.value)}). Constraints can only narrow.`,
2469
+ constraint.provenance,
2470
+ previous.provenance
2471
+ );
2472
+ continue;
2473
+ }
2474
+ if (strength <= 0) {
2475
+ continue;
2476
+ }
2477
+ strongestByKey.set(key, constraint);
2478
+ }
2479
+ }
1832
2480
  function checkNumericContradictions(ctx, fieldName, constraints) {
1833
2481
  const min = findNumeric(constraints, "minimum");
1834
2482
  const max = findNumeric(constraints, "maximum");
@@ -1915,6 +2563,25 @@ function checkAllowedMembersContradiction(ctx, fieldName, constraints) {
1915
2563
  }
1916
2564
  }
1917
2565
  }
2566
+ function checkConstContradictions(ctx, fieldName, constraints) {
2567
+ const constConstraints = findConstConstraints(constraints);
2568
+ if (constConstraints.length < 2) return;
2569
+ const first = constConstraints[0];
2570
+ if (first === void 0) return;
2571
+ for (let i = 1; i < constConstraints.length; i++) {
2572
+ const current = constConstraints[i];
2573
+ if (current === void 0) continue;
2574
+ if (jsonValueEquals(first.value, current.value)) {
2575
+ continue;
2576
+ }
2577
+ addContradiction(
2578
+ ctx,
2579
+ `Field "${fieldName}": conflicting @const constraints require both ${JSON.stringify(first.value)} and ${JSON.stringify(current.value)}`,
2580
+ first.provenance,
2581
+ current.provenance
2582
+ );
2583
+ }
2584
+ }
1918
2585
  function typeLabel(type) {
1919
2586
  switch (type.kind) {
1920
2587
  case "primitive":
@@ -1925,6 +2592,8 @@ function typeLabel(type) {
1925
2592
  return "array";
1926
2593
  case "object":
1927
2594
  return "object";
2595
+ case "record":
2596
+ return "record";
1928
2597
  case "union":
1929
2598
  return "union";
1930
2599
  case "reference":
@@ -1939,85 +2608,173 @@ function typeLabel(type) {
1939
2608
  }
1940
2609
  }
1941
2610
  }
1942
- function checkTypeApplicability(ctx, fieldName, type, constraints) {
1943
- const isNumber = type.kind === "primitive" && type.primitiveKind === "number";
1944
- const isString = type.kind === "primitive" && type.primitiveKind === "string";
1945
- const isArray = type.kind === "array";
1946
- const isEnum = type.kind === "enum";
1947
- const label = typeLabel(type);
1948
- for (const constraint of constraints) {
1949
- if (constraint.path) {
1950
- const isTraversable = type.kind === "object" || type.kind === "array" || type.kind === "reference";
1951
- if (!isTraversable) {
2611
+ function dereferenceType(ctx, type) {
2612
+ let current = type;
2613
+ const seen = /* @__PURE__ */ new Set();
2614
+ while (current.kind === "reference") {
2615
+ if (seen.has(current.name)) {
2616
+ return current;
2617
+ }
2618
+ seen.add(current.name);
2619
+ const definition = ctx.typeRegistry[current.name];
2620
+ if (definition === void 0) {
2621
+ return current;
2622
+ }
2623
+ current = definition.type;
2624
+ }
2625
+ return current;
2626
+ }
2627
+ function resolvePathTargetType(ctx, type, segments) {
2628
+ const effectiveType = dereferenceType(ctx, type);
2629
+ if (segments.length === 0) {
2630
+ return { kind: "resolved", type: effectiveType };
2631
+ }
2632
+ if (effectiveType.kind === "array") {
2633
+ return resolvePathTargetType(ctx, effectiveType.items, segments);
2634
+ }
2635
+ if (effectiveType.kind === "object") {
2636
+ const [segment, ...rest] = segments;
2637
+ if (segment === void 0) {
2638
+ throw new Error("Invariant violation: object path traversal requires a segment");
2639
+ }
2640
+ const property = effectiveType.properties.find((prop) => prop.name === segment);
2641
+ if (property === void 0) {
2642
+ return { kind: "missing-property", segment };
2643
+ }
2644
+ return resolvePathTargetType(ctx, property.type, rest);
2645
+ }
2646
+ return { kind: "unresolvable", type: effectiveType };
2647
+ }
2648
+ function formatPathTargetFieldName(fieldName, path2) {
2649
+ return path2.length === 0 ? fieldName : `${fieldName}.${path2.join(".")}`;
2650
+ }
2651
+ function checkConstraintOnType(ctx, fieldName, type, constraint) {
2652
+ const effectiveType = dereferenceType(ctx, type);
2653
+ const isNumber = effectiveType.kind === "primitive" && effectiveType.primitiveKind === "number";
2654
+ const isString = effectiveType.kind === "primitive" && effectiveType.primitiveKind === "string";
2655
+ const isArray = effectiveType.kind === "array";
2656
+ const isEnum = effectiveType.kind === "enum";
2657
+ const arrayItemType = effectiveType.kind === "array" ? dereferenceType(ctx, effectiveType.items) : void 0;
2658
+ const isStringArray = arrayItemType?.kind === "primitive" && arrayItemType.primitiveKind === "string";
2659
+ const label = typeLabel(effectiveType);
2660
+ const ck = constraint.constraintKind;
2661
+ switch (ck) {
2662
+ case "minimum":
2663
+ case "maximum":
2664
+ case "exclusiveMinimum":
2665
+ case "exclusiveMaximum":
2666
+ case "multipleOf": {
2667
+ if (!isNumber) {
1952
2668
  addTypeMismatch(
1953
2669
  ctx,
1954
- `Field "${fieldName}": path-targeted constraint "${constraint.constraintKind}" is invalid because type "${label}" cannot be traversed`,
2670
+ `Field "${fieldName}": constraint "${ck}" is only valid on number fields, but field type is "${label}"`,
1955
2671
  constraint.provenance
1956
2672
  );
1957
2673
  }
1958
- continue;
2674
+ break;
1959
2675
  }
1960
- const ck = constraint.constraintKind;
1961
- switch (ck) {
1962
- case "minimum":
1963
- case "maximum":
1964
- case "exclusiveMinimum":
1965
- case "exclusiveMaximum":
1966
- case "multipleOf": {
1967
- if (!isNumber) {
1968
- addTypeMismatch(
1969
- ctx,
1970
- `Field "${fieldName}": constraint "${ck}" is only valid on number fields, but field type is "${label}"`,
1971
- constraint.provenance
1972
- );
1973
- }
1974
- break;
2676
+ case "minLength":
2677
+ case "maxLength":
2678
+ case "pattern": {
2679
+ if (!isString && !isStringArray) {
2680
+ addTypeMismatch(
2681
+ ctx,
2682
+ `Field "${fieldName}": constraint "${ck}" is only valid on string fields or string array items, but field type is "${label}"`,
2683
+ constraint.provenance
2684
+ );
1975
2685
  }
1976
- case "minLength":
1977
- case "maxLength":
1978
- case "pattern": {
1979
- if (!isString) {
1980
- addTypeMismatch(
1981
- ctx,
1982
- `Field "${fieldName}": constraint "${ck}" is only valid on string fields, but field type is "${label}"`,
1983
- constraint.provenance
1984
- );
1985
- }
1986
- break;
2686
+ break;
2687
+ }
2688
+ case "minItems":
2689
+ case "maxItems":
2690
+ case "uniqueItems": {
2691
+ if (!isArray) {
2692
+ addTypeMismatch(
2693
+ ctx,
2694
+ `Field "${fieldName}": constraint "${ck}" is only valid on array fields, but field type is "${label}"`,
2695
+ constraint.provenance
2696
+ );
1987
2697
  }
1988
- case "minItems":
1989
- case "maxItems":
1990
- case "uniqueItems": {
1991
- if (!isArray) {
1992
- addTypeMismatch(
1993
- ctx,
1994
- `Field "${fieldName}": constraint "${ck}" is only valid on array fields, but field type is "${label}"`,
1995
- constraint.provenance
1996
- );
1997
- }
2698
+ break;
2699
+ }
2700
+ case "allowedMembers": {
2701
+ if (!isEnum) {
2702
+ addTypeMismatch(
2703
+ ctx,
2704
+ `Field "${fieldName}": constraint "allowedMembers" is only valid on enum fields, but field type is "${label}"`,
2705
+ constraint.provenance
2706
+ );
2707
+ }
2708
+ break;
2709
+ }
2710
+ case "const": {
2711
+ const isPrimitiveConstType = effectiveType.kind === "primitive" && ["string", "number", "boolean", "null"].includes(effectiveType.primitiveKind) || effectiveType.kind === "enum";
2712
+ if (!isPrimitiveConstType) {
2713
+ addTypeMismatch(
2714
+ ctx,
2715
+ `Field "${fieldName}": constraint "const" is only valid on primitive or enum fields, but field type is "${label}"`,
2716
+ constraint.provenance
2717
+ );
1998
2718
  break;
1999
2719
  }
2000
- case "allowedMembers": {
2001
- if (!isEnum) {
2720
+ if (effectiveType.kind === "primitive") {
2721
+ const valueType = constraint.value === null ? "null" : Array.isArray(constraint.value) ? "array" : typeof constraint.value;
2722
+ if (valueType !== effectiveType.primitiveKind) {
2002
2723
  addTypeMismatch(
2003
2724
  ctx,
2004
- `Field "${fieldName}": constraint "allowedMembers" is only valid on enum fields, but field type is "${label}"`,
2725
+ `Field "${fieldName}": @const value type "${valueType}" is incompatible with field type "${effectiveType.primitiveKind}"`,
2005
2726
  constraint.provenance
2006
2727
  );
2007
2728
  }
2008
2729
  break;
2009
2730
  }
2010
- case "custom": {
2011
- checkCustomConstraint(ctx, fieldName, type, constraint);
2012
- break;
2731
+ const memberValues = effectiveType.members.map((member) => member.value);
2732
+ if (!memberValues.some((member) => jsonValueEquals(member, constraint.value))) {
2733
+ addTypeMismatch(
2734
+ ctx,
2735
+ `Field "${fieldName}": @const value ${JSON.stringify(constraint.value)} is not one of the enum members`,
2736
+ constraint.provenance
2737
+ );
2013
2738
  }
2014
- default: {
2015
- const _exhaustive = constraint;
2016
- throw new Error(
2017
- `Unhandled constraint kind: ${_exhaustive.constraintKind}`
2739
+ break;
2740
+ }
2741
+ case "custom": {
2742
+ checkCustomConstraint(ctx, fieldName, effectiveType, constraint);
2743
+ break;
2744
+ }
2745
+ default: {
2746
+ const _exhaustive = constraint;
2747
+ throw new Error(
2748
+ `Unhandled constraint kind: ${_exhaustive.constraintKind}`
2749
+ );
2750
+ }
2751
+ }
2752
+ }
2753
+ function checkTypeApplicability(ctx, fieldName, type, constraints) {
2754
+ for (const constraint of constraints) {
2755
+ if (constraint.path) {
2756
+ const resolution = resolvePathTargetType(ctx, type, constraint.path.segments);
2757
+ const targetFieldName = formatPathTargetFieldName(fieldName, constraint.path.segments);
2758
+ if (resolution.kind === "missing-property") {
2759
+ addUnknownPathTarget(
2760
+ ctx,
2761
+ `Field "${targetFieldName}": path-targeted constraint "${constraint.constraintKind}" references unknown path segment "${resolution.segment}"`,
2762
+ constraint.provenance
2018
2763
  );
2764
+ continue;
2019
2765
  }
2766
+ if (resolution.kind === "unresolvable") {
2767
+ addTypeMismatch(
2768
+ ctx,
2769
+ `Field "${targetFieldName}": path-targeted constraint "${constraint.constraintKind}" is invalid because type "${typeLabel(resolution.type)}" cannot be traversed`,
2770
+ constraint.provenance
2771
+ );
2772
+ continue;
2773
+ }
2774
+ checkConstraintOnType(ctx, targetFieldName, resolution.type, constraint);
2775
+ continue;
2020
2776
  }
2777
+ checkConstraintOnType(ctx, fieldName, type, constraint);
2021
2778
  }
2022
2779
  }
2023
2780
  function checkCustomConstraint(ctx, fieldName, type, constraint) {
@@ -2061,6 +2818,8 @@ function validateConstraints(ctx, name, type, constraints) {
2061
2818
  checkNumericContradictions(ctx, name, constraints);
2062
2819
  checkLengthContradictions(ctx, name, constraints);
2063
2820
  checkAllowedMembersContradiction(ctx, name, constraints);
2821
+ checkConstContradictions(ctx, name, constraints);
2822
+ checkConstraintBroadening(ctx, name, constraints);
2064
2823
  checkTypeApplicability(ctx, name, type, constraints);
2065
2824
  }
2066
2825
  function validateElement(ctx, element) {
@@ -2087,8 +2846,8 @@ function validateElement(ctx, element) {
2087
2846
  function validateIR(ir, options) {
2088
2847
  const ctx = {
2089
2848
  diagnostics: [],
2090
- vendorPrefix: options?.vendorPrefix ?? "FORMSPEC",
2091
- extensionRegistry: options?.extensionRegistry
2849
+ extensionRegistry: options?.extensionRegistry,
2850
+ typeRegistry: ir.typeRegistry
2092
2851
  };
2093
2852
  for (const element of ir.elements) {
2094
2853
  validateElement(ctx, element);
@@ -2142,7 +2901,7 @@ function createExtensionRegistry(extensions) {
2142
2901
  }
2143
2902
 
2144
2903
  // src/generators/method-schema.ts
2145
- var import_core5 = require("@formspec/core");
2904
+ var import_core4 = require("@formspec/core");
2146
2905
  function typeToJsonSchema(type, checker) {
2147
2906
  const typeRegistry = {};
2148
2907
  const visiting = /* @__PURE__ */ new Set();
@@ -2150,7 +2909,7 @@ function typeToJsonSchema(type, checker) {
2150
2909
  const fieldProvenance = { surface: "tsdoc", file: "", line: 0, column: 0 };
2151
2910
  const ir = {
2152
2911
  kind: "form-ir",
2153
- irVersion: import_core5.IR_VERSION,
2912
+ irVersion: import_core4.IR_VERSION,
2154
2913
  elements: [
2155
2914
  {
2156
2915
  kind: "field",