@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
package/dist/internals.js CHANGED
@@ -177,7 +177,7 @@ function canonicalizeArrayField(field) {
177
177
  const itemsType = {
178
178
  kind: "object",
179
179
  properties: itemProperties,
180
- additionalProperties: false
180
+ additionalProperties: true
181
181
  };
182
182
  const type = { kind: "array", items: itemsType };
183
183
  const constraints = [];
@@ -212,7 +212,7 @@ function canonicalizeObjectField(field) {
212
212
  const type = {
213
213
  kind: "object",
214
214
  properties,
215
- additionalProperties: false
215
+ additionalProperties: true
216
216
  };
217
217
  return buildFieldNode(field.name, type, field.required, buildAnnotations(field.label));
218
218
  }
@@ -460,9 +460,6 @@ import * as ts4 from "typescript";
460
460
 
461
461
  // src/analyzer/jsdoc-constraints.ts
462
462
  import * as ts3 from "typescript";
463
- import {
464
- BUILTIN_CONSTRAINT_DEFINITIONS as BUILTIN_CONSTRAINT_DEFINITIONS2
465
- } from "@formspec/core";
466
463
 
467
464
  // src/analyzer/tsdoc-parser.ts
468
465
  import * as ts2 from "typescript";
@@ -476,22 +473,35 @@ import {
476
473
  TextRange
477
474
  } from "@microsoft/tsdoc";
478
475
  import {
479
- BUILTIN_CONSTRAINT_DEFINITIONS
476
+ BUILTIN_CONSTRAINT_DEFINITIONS,
477
+ normalizeConstraintTagName,
478
+ isBuiltinConstraintName
480
479
  } from "@formspec/core";
480
+
481
+ // src/analyzer/json-utils.ts
482
+ function tryParseJson(text) {
483
+ try {
484
+ return JSON.parse(text);
485
+ } catch {
486
+ return null;
487
+ }
488
+ }
489
+
490
+ // src/analyzer/tsdoc-parser.ts
481
491
  var NUMERIC_CONSTRAINT_MAP = {
482
- Minimum: "minimum",
483
- Maximum: "maximum",
484
- ExclusiveMinimum: "exclusiveMinimum",
485
- ExclusiveMaximum: "exclusiveMaximum"
492
+ minimum: "minimum",
493
+ maximum: "maximum",
494
+ exclusiveMinimum: "exclusiveMinimum",
495
+ exclusiveMaximum: "exclusiveMaximum",
496
+ multipleOf: "multipleOf"
486
497
  };
487
498
  var LENGTH_CONSTRAINT_MAP = {
488
- MinLength: "minLength",
489
- MaxLength: "maxLength"
499
+ minLength: "minLength",
500
+ maxLength: "maxLength",
501
+ minItems: "minItems",
502
+ maxItems: "maxItems"
490
503
  };
491
- var TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["Pattern", "EnumOptions"]);
492
- function isBuiltinConstraintName(tagName) {
493
- return tagName in BUILTIN_CONSTRAINT_DEFINITIONS;
494
- }
504
+ var TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["pattern", "enumOptions"]);
495
505
  function createFormSpecTSDocConfig() {
496
506
  const config = new TSDocConfiguration();
497
507
  for (const tagName of Object.keys(BUILTIN_CONSTRAINT_DEFINITIONS)) {
@@ -503,6 +513,15 @@ function createFormSpecTSDocConfig() {
503
513
  })
504
514
  );
505
515
  }
516
+ for (const tagName of ["displayName", "description"]) {
517
+ config.addTagDefinition(
518
+ new TSDocTagDefinition({
519
+ tagName: "@" + tagName,
520
+ syntaxKind: TSDocTagSyntaxKind.BlockTag,
521
+ allowMultiple: true
522
+ })
523
+ );
524
+ }
506
525
  return config;
507
526
  }
508
527
  var sharedParser;
@@ -531,7 +550,28 @@ function parseTSDocTags(node, file = "") {
531
550
  );
532
551
  const docComment = parserContext.docComment;
533
552
  for (const block of docComment.customBlocks) {
534
- const tagName = block.blockTag.tagName.substring(1);
553
+ const tagName = normalizeConstraintTagName(block.blockTag.tagName.substring(1));
554
+ if (tagName === "displayName" || tagName === "description") {
555
+ const text2 = extractBlockText(block).trim();
556
+ if (text2 === "") continue;
557
+ const provenance2 = provenanceForComment(range, sourceFile, file, tagName);
558
+ if (tagName === "displayName") {
559
+ annotations.push({
560
+ kind: "annotation",
561
+ annotationKind: "displayName",
562
+ value: text2,
563
+ provenance: provenance2
564
+ });
565
+ } else {
566
+ annotations.push({
567
+ kind: "annotation",
568
+ annotationKind: "description",
569
+ value: text2,
570
+ provenance: provenance2
571
+ });
572
+ }
573
+ continue;
574
+ }
535
575
  if (TAGS_REQUIRING_RAW_TEXT.has(tagName)) continue;
536
576
  const text = extractBlockText(block).trim();
537
577
  if (text === "") continue;
@@ -552,7 +592,7 @@ function parseTSDocTags(node, file = "") {
552
592
  }
553
593
  const jsDocTagsAll = ts2.getJSDocTags(node);
554
594
  for (const tag of jsDocTagsAll) {
555
- const tagName = tag.tagName.text;
595
+ const tagName = normalizeConstraintTagName(tag.tagName.text);
556
596
  if (!TAGS_REQUIRING_RAW_TEXT.has(tagName)) continue;
557
597
  const commentText = getTagCommentText(tag);
558
598
  if (commentText === void 0 || commentText.trim() === "") continue;
@@ -563,43 +603,17 @@ function parseTSDocTags(node, file = "") {
563
603
  constraints.push(constraintNode);
564
604
  }
565
605
  }
566
- let displayName;
567
- let description;
568
- let displayNameTag;
569
- let descriptionTag;
570
- for (const tag of jsDocTagsAll) {
571
- const tagName = tag.tagName.text;
572
- const commentText = getTagCommentText(tag);
573
- if (commentText === void 0 || commentText.trim() === "") {
574
- continue;
575
- }
576
- const trimmed = commentText.trim();
577
- if (tagName === "Field_displayName") {
578
- displayName = trimmed;
579
- displayNameTag = tag;
580
- } else if (tagName === "Field_description") {
581
- description = trimmed;
582
- descriptionTag = tag;
583
- }
584
- }
585
- if (displayName !== void 0 && displayNameTag) {
586
- annotations.push({
587
- kind: "annotation",
588
- annotationKind: "displayName",
589
- value: displayName,
590
- provenance: provenanceForJSDocTag(displayNameTag, file)
591
- });
592
- }
593
- if (description !== void 0 && descriptionTag) {
594
- annotations.push({
595
- kind: "annotation",
596
- annotationKind: "description",
597
- value: description,
598
- provenance: provenanceForJSDocTag(descriptionTag, file)
599
- });
600
- }
601
606
  return { constraints, annotations };
602
607
  }
608
+ function extractPathTarget(text) {
609
+ const trimmed = text.trimStart();
610
+ const match = /^:([a-zA-Z_]\w*)\s+([\s\S]*)$/.exec(trimmed);
611
+ if (!match?.[1] || !match[2]) return null;
612
+ return {
613
+ path: { segments: [match[1]] },
614
+ remainingText: match[2]
615
+ };
616
+ }
603
617
  function extractBlockText(block) {
604
618
  return extractPlainText(block.content);
605
619
  }
@@ -622,9 +636,12 @@ function parseConstraintValue(tagName, text, provenance) {
622
636
  if (!isBuiltinConstraintName(tagName)) {
623
637
  return null;
624
638
  }
639
+ const pathResult = extractPathTarget(text);
640
+ const effectiveText = pathResult ? pathResult.remainingText : text;
641
+ const path2 = pathResult?.path;
625
642
  const expectedType = BUILTIN_CONSTRAINT_DEFINITIONS[tagName];
626
643
  if (expectedType === "number") {
627
- const value = Number(text);
644
+ const value = Number(effectiveText);
628
645
  if (Number.isNaN(value)) {
629
646
  return null;
630
647
  }
@@ -634,6 +651,7 @@ function parseConstraintValue(tagName, text, provenance) {
634
651
  kind: "constraint",
635
652
  constraintKind: numericKind,
636
653
  value,
654
+ ...path2 && { path: path2 },
637
655
  provenance
638
656
  };
639
657
  }
@@ -643,42 +661,41 @@ function parseConstraintValue(tagName, text, provenance) {
643
661
  kind: "constraint",
644
662
  constraintKind: lengthKind,
645
663
  value,
664
+ ...path2 && { path: path2 },
646
665
  provenance
647
666
  };
648
667
  }
649
668
  return null;
650
669
  }
651
670
  if (expectedType === "json") {
652
- try {
653
- const parsed = JSON.parse(text);
654
- if (!Array.isArray(parsed)) {
655
- return null;
656
- }
657
- const members = [];
658
- for (const item of parsed) {
659
- if (typeof item === "string" || typeof item === "number") {
660
- members.push(item);
661
- } else if (typeof item === "object" && item !== null && "id" in item) {
662
- const id = item["id"];
663
- if (typeof id === "string" || typeof id === "number") {
664
- members.push(id);
665
- }
671
+ const parsed = tryParseJson(effectiveText);
672
+ if (!Array.isArray(parsed)) {
673
+ return null;
674
+ }
675
+ const members = [];
676
+ for (const item of parsed) {
677
+ if (typeof item === "string" || typeof item === "number") {
678
+ members.push(item);
679
+ } else if (typeof item === "object" && item !== null && "id" in item) {
680
+ const id = item["id"];
681
+ if (typeof id === "string" || typeof id === "number") {
682
+ members.push(id);
666
683
  }
667
684
  }
668
- return {
669
- kind: "constraint",
670
- constraintKind: "allowedMembers",
671
- members,
672
- provenance
673
- };
674
- } catch {
675
- return null;
676
685
  }
686
+ return {
687
+ kind: "constraint",
688
+ constraintKind: "allowedMembers",
689
+ members,
690
+ ...path2 && { path: path2 },
691
+ provenance
692
+ };
677
693
  }
678
694
  return {
679
695
  kind: "constraint",
680
696
  constraintKind: "pattern",
681
- pattern: text,
697
+ pattern: effectiveText,
698
+ ...path2 && { path: path2 },
682
699
  provenance
683
700
  };
684
701
  }
@@ -850,18 +867,19 @@ function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting) {
850
867
  const tsType = checker.getTypeAtLocation(prop);
851
868
  const optional = prop.questionToken !== void 0;
852
869
  const provenance = provenanceForNode(prop, file);
853
- const type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting);
870
+ let type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting);
854
871
  const constraints = [];
855
872
  if (prop.type) {
856
873
  constraints.push(...extractTypeAliasConstraintNodes(prop.type, checker, file));
857
874
  }
858
875
  constraints.push(...extractJSDocConstraintNodes(prop, file));
859
- const annotations = [];
876
+ let annotations = [];
860
877
  annotations.push(...extractJSDocAnnotationNodes(prop, file));
861
878
  const defaultAnnotation = extractDefaultValueAnnotation(prop.initializer, file);
862
879
  if (defaultAnnotation) {
863
880
  annotations.push(defaultAnnotation);
864
881
  }
882
+ ({ type, annotations } = applyEnumMemberDisplayNames(type, annotations));
865
883
  return {
866
884
  kind: "field",
867
885
  name,
@@ -880,14 +898,15 @@ function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visitin
880
898
  const tsType = checker.getTypeAtLocation(prop);
881
899
  const optional = prop.questionToken !== void 0;
882
900
  const provenance = provenanceForNode(prop, file);
883
- const type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting);
901
+ let type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting);
884
902
  const constraints = [];
885
903
  if (prop.type) {
886
904
  constraints.push(...extractTypeAliasConstraintNodes(prop.type, checker, file));
887
905
  }
888
906
  constraints.push(...extractJSDocConstraintNodes(prop, file));
889
- const annotations = [];
907
+ let annotations = [];
890
908
  annotations.push(...extractJSDocAnnotationNodes(prop, file));
909
+ ({ type, annotations } = applyEnumMemberDisplayNames(type, annotations));
891
910
  return {
892
911
  kind: "field",
893
912
  name,
@@ -898,6 +917,68 @@ function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visitin
898
917
  provenance
899
918
  };
900
919
  }
920
+ function applyEnumMemberDisplayNames(type, annotations) {
921
+ if (!annotations.some(
922
+ (annotation) => annotation.annotationKind === "displayName" && annotation.value.trim().startsWith(":")
923
+ )) {
924
+ return { type, annotations: [...annotations] };
925
+ }
926
+ const consumed = /* @__PURE__ */ new Set();
927
+ const nextType = rewriteEnumDisplayNames(type, annotations, consumed);
928
+ if (consumed.size === 0) {
929
+ return { type, annotations: [...annotations] };
930
+ }
931
+ return {
932
+ type: nextType,
933
+ annotations: annotations.filter((annotation) => !consumed.has(annotation))
934
+ };
935
+ }
936
+ function rewriteEnumDisplayNames(type, annotations, consumed) {
937
+ switch (type.kind) {
938
+ case "enum":
939
+ return applyEnumMemberDisplayNamesToEnum(type, annotations, consumed);
940
+ case "union": {
941
+ return {
942
+ ...type,
943
+ members: type.members.map(
944
+ (member) => rewriteEnumDisplayNames(member, annotations, consumed)
945
+ )
946
+ };
947
+ }
948
+ default:
949
+ return type;
950
+ }
951
+ }
952
+ function applyEnumMemberDisplayNamesToEnum(type, annotations, consumed) {
953
+ const displayNames = /* @__PURE__ */ new Map();
954
+ for (const annotation of annotations) {
955
+ if (annotation.annotationKind !== "displayName") continue;
956
+ const parsed = parseEnumMemberDisplayName(annotation.value);
957
+ if (!parsed) continue;
958
+ consumed.add(annotation);
959
+ const member = type.members.find((m) => String(m.value) === parsed.value);
960
+ if (!member) continue;
961
+ displayNames.set(String(member.value), parsed.label);
962
+ }
963
+ if (displayNames.size === 0) {
964
+ return type;
965
+ }
966
+ return {
967
+ ...type,
968
+ members: type.members.map((member) => {
969
+ const displayName = displayNames.get(String(member.value));
970
+ return displayName !== void 0 ? { ...member, displayName } : member;
971
+ })
972
+ };
973
+ }
974
+ function parseEnumMemberDisplayName(value) {
975
+ const trimmed = value.trim();
976
+ const match = /^:([^\s]+)\s+([\s\S]+)$/.exec(trimmed);
977
+ if (!match?.[1] || !match[2]) return null;
978
+ const label = match[2].trim();
979
+ if (label === "") return null;
980
+ return { value: match[1], label };
981
+ }
901
982
  function resolveTypeNode(type, checker, file, typeRegistry, visiting) {
902
983
  if (type.flags & ts4.TypeFlags.String) {
903
984
  return { kind: "primitive", primitiveKind: "string" };
@@ -1008,7 +1089,30 @@ function resolveArrayType(type, checker, file, typeRegistry, visiting) {
1008
1089
  const items = elementType ? resolveTypeNode(elementType, checker, file, typeRegistry, visiting) : { kind: "primitive", primitiveKind: "string" };
1009
1090
  return { kind: "array", items };
1010
1091
  }
1092
+ function tryResolveRecordType(type, checker, file, typeRegistry, visiting) {
1093
+ if (type.getProperties().length > 0) {
1094
+ return null;
1095
+ }
1096
+ const indexInfo = checker.getIndexInfoOfType(type, ts4.IndexKind.String);
1097
+ if (!indexInfo) {
1098
+ return null;
1099
+ }
1100
+ if (visiting.has(type)) {
1101
+ return null;
1102
+ }
1103
+ visiting.add(type);
1104
+ try {
1105
+ const valueType = resolveTypeNode(indexInfo.type, checker, file, typeRegistry, visiting);
1106
+ return { kind: "record", valueType };
1107
+ } finally {
1108
+ visiting.delete(type);
1109
+ }
1110
+ }
1011
1111
  function resolveObjectType(type, checker, file, typeRegistry, visiting) {
1112
+ const recordNode = tryResolveRecordType(type, checker, file, typeRegistry, visiting);
1113
+ if (recordNode) {
1114
+ return recordNode;
1115
+ }
1012
1116
  if (visiting.has(type)) {
1013
1117
  return { kind: "object", properties: [], additionalProperties: false };
1014
1118
  }
@@ -1040,7 +1144,7 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting) {
1040
1144
  const objectNode = {
1041
1145
  kind: "object",
1042
1146
  properties,
1043
- additionalProperties: false
1147
+ additionalProperties: true
1044
1148
  };
1045
1149
  if (typeName) {
1046
1150
  typeRegistry[typeName] = {
@@ -1109,14 +1213,23 @@ function buildFieldNodeInfoMap(members, checker, file, typeRegistry, visiting) {
1109
1213
  }
1110
1214
  return map;
1111
1215
  }
1112
- function extractTypeAliasConstraintNodes(typeNode, checker, file) {
1216
+ var MAX_ALIAS_CHAIN_DEPTH = 8;
1217
+ function extractTypeAliasConstraintNodes(typeNode, checker, file, depth = 0) {
1113
1218
  if (!ts4.isTypeReferenceNode(typeNode)) return [];
1219
+ if (depth >= MAX_ALIAS_CHAIN_DEPTH) {
1220
+ const aliasName = typeNode.typeName.getText();
1221
+ throw new Error(
1222
+ `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.`
1223
+ );
1224
+ }
1114
1225
  const symbol = checker.getSymbolAtLocation(typeNode.typeName);
1115
1226
  if (!symbol?.declarations) return [];
1116
1227
  const aliasDecl = symbol.declarations.find(ts4.isTypeAliasDeclaration);
1117
1228
  if (!aliasDecl) return [];
1118
1229
  if (ts4.isTypeLiteralNode(aliasDecl.type)) return [];
1119
- return extractJSDocConstraintNodes(aliasDecl, file);
1230
+ const constraints = extractJSDocConstraintNodes(aliasDecl, file);
1231
+ constraints.push(...extractTypeAliasConstraintNodes(aliasDecl.type, checker, file, depth + 1));
1232
+ return constraints;
1120
1233
  }
1121
1234
  function provenanceForNode(node, file) {
1122
1235
  const sourceFile = node.getSourceFile();
@@ -1191,11 +1304,21 @@ function detectFormSpecReference(typeNode) {
1191
1304
  }
1192
1305
 
1193
1306
  // src/json-schema/ir-generator.ts
1194
- function makeContext() {
1195
- return { defs: {} };
1307
+ function makeContext(options) {
1308
+ const vendorPrefix = options?.vendorPrefix ?? "x-formspec";
1309
+ if (!vendorPrefix.startsWith("x-")) {
1310
+ throw new Error(
1311
+ `Invalid vendorPrefix "${vendorPrefix}". Extension JSON Schema keywords must start with "x-".`
1312
+ );
1313
+ }
1314
+ return {
1315
+ defs: {},
1316
+ extensionRegistry: options?.extensionRegistry,
1317
+ vendorPrefix
1318
+ };
1196
1319
  }
1197
- function generateJsonSchemaFromIR(ir) {
1198
- const ctx = makeContext();
1320
+ function generateJsonSchemaFromIR(ir, options) {
1321
+ const ctx = makeContext(options);
1199
1322
  for (const [name, typeDef] of Object.entries(ir.typeRegistry)) {
1200
1323
  ctx.defs[name] = generateTypeNode(typeDef.type, ctx);
1201
1324
  }
@@ -1238,8 +1361,70 @@ function collectFields(elements, properties, required, ctx) {
1238
1361
  }
1239
1362
  function generateFieldSchema(field, ctx) {
1240
1363
  const schema = generateTypeNode(field.type, ctx);
1241
- applyConstraints(schema, field.constraints);
1242
- applyAnnotations(schema, field.annotations);
1364
+ const directConstraints = [];
1365
+ const pathConstraints = [];
1366
+ for (const c of field.constraints) {
1367
+ if (c.path) {
1368
+ pathConstraints.push(c);
1369
+ } else {
1370
+ directConstraints.push(c);
1371
+ }
1372
+ }
1373
+ applyConstraints(schema, directConstraints, ctx);
1374
+ applyAnnotations(schema, field.annotations, ctx);
1375
+ if (pathConstraints.length === 0) {
1376
+ return schema;
1377
+ }
1378
+ return applyPathTargetedConstraints(schema, pathConstraints, ctx);
1379
+ }
1380
+ function applyPathTargetedConstraints(schema, pathConstraints, ctx) {
1381
+ if (schema.type === "array" && schema.items) {
1382
+ schema.items = applyPathTargetedConstraints(schema.items, pathConstraints, ctx);
1383
+ return schema;
1384
+ }
1385
+ const byTarget = /* @__PURE__ */ new Map();
1386
+ for (const c of pathConstraints) {
1387
+ const target = c.path?.segments[0];
1388
+ if (!target) continue;
1389
+ const group = byTarget.get(target) ?? [];
1390
+ group.push(c);
1391
+ byTarget.set(target, group);
1392
+ }
1393
+ const propertyOverrides = {};
1394
+ for (const [target, constraints] of byTarget) {
1395
+ const subSchema = {};
1396
+ applyConstraints(subSchema, constraints, ctx);
1397
+ propertyOverrides[target] = subSchema;
1398
+ }
1399
+ if (schema.$ref) {
1400
+ const { $ref, ...rest } = schema;
1401
+ const refPart = { $ref };
1402
+ const overridePart = {
1403
+ properties: propertyOverrides,
1404
+ ...rest
1405
+ };
1406
+ return { allOf: [refPart, overridePart] };
1407
+ }
1408
+ if (schema.type === "object" && schema.properties) {
1409
+ const missingOverrides = {};
1410
+ for (const [target, overrideSchema] of Object.entries(propertyOverrides)) {
1411
+ if (schema.properties[target]) {
1412
+ Object.assign(schema.properties[target], overrideSchema);
1413
+ } else {
1414
+ missingOverrides[target] = overrideSchema;
1415
+ }
1416
+ }
1417
+ if (Object.keys(missingOverrides).length === 0) {
1418
+ return schema;
1419
+ }
1420
+ return {
1421
+ allOf: [schema, { properties: missingOverrides }]
1422
+ };
1423
+ }
1424
+ if (schema.allOf) {
1425
+ schema.allOf = [...schema.allOf, { properties: propertyOverrides }];
1426
+ return schema;
1427
+ }
1243
1428
  return schema;
1244
1429
  }
1245
1430
  function generateTypeNode(type, ctx) {
@@ -1252,6 +1437,8 @@ function generateTypeNode(type, ctx) {
1252
1437
  return generateArrayType(type, ctx);
1253
1438
  case "object":
1254
1439
  return generateObjectType(type, ctx);
1440
+ case "record":
1441
+ return generateRecordType(type, ctx);
1255
1442
  case "union":
1256
1443
  return generateUnionType(type, ctx);
1257
1444
  case "reference":
@@ -1259,7 +1446,7 @@ function generateTypeNode(type, ctx) {
1259
1446
  case "dynamic":
1260
1447
  return generateDynamicType(type);
1261
1448
  case "custom":
1262
- return generateCustomType(type);
1449
+ return generateCustomType(type, ctx);
1263
1450
  default: {
1264
1451
  const _exhaustive = type;
1265
1452
  return _exhaustive;
@@ -1308,16 +1495,27 @@ function generateObjectType(type, ctx) {
1308
1495
  }
1309
1496
  return schema;
1310
1497
  }
1498
+ function generateRecordType(type, ctx) {
1499
+ return {
1500
+ type: "object",
1501
+ additionalProperties: generateTypeNode(type.valueType, ctx)
1502
+ };
1503
+ }
1311
1504
  function generatePropertySchema(prop, ctx) {
1312
1505
  const schema = generateTypeNode(prop.type, ctx);
1313
- applyConstraints(schema, prop.constraints);
1314
- applyAnnotations(schema, prop.annotations);
1506
+ applyConstraints(schema, prop.constraints, ctx);
1507
+ applyAnnotations(schema, prop.annotations, ctx);
1315
1508
  return schema;
1316
1509
  }
1317
1510
  function generateUnionType(type, ctx) {
1318
1511
  if (isBooleanUnion(type)) {
1319
1512
  return { type: "boolean" };
1320
1513
  }
1514
+ if (isNullableUnion(type)) {
1515
+ return {
1516
+ oneOf: type.members.map((m) => generateTypeNode(m, ctx))
1517
+ };
1518
+ }
1321
1519
  return {
1322
1520
  anyOf: type.members.map((m) => generateTypeNode(m, ctx))
1323
1521
  };
@@ -1327,6 +1525,13 @@ function isBooleanUnion(type) {
1327
1525
  const kinds = type.members.map((m) => m.kind);
1328
1526
  return kinds.every((k) => k === "primitive") && type.members.every((m) => m.kind === "primitive" && m.primitiveKind === "boolean");
1329
1527
  }
1528
+ function isNullableUnion(type) {
1529
+ if (type.members.length !== 2) return false;
1530
+ const nullCount = type.members.filter(
1531
+ (m) => m.kind === "primitive" && m.primitiveKind === "null"
1532
+ ).length;
1533
+ return nullCount === 1;
1534
+ }
1330
1535
  function generateReferenceType(type) {
1331
1536
  return { $ref: `#/$defs/${type.name}` };
1332
1537
  }
@@ -1347,10 +1552,7 @@ function generateDynamicType(type) {
1347
1552
  "x-formspec-schemaSource": type.sourceKey
1348
1553
  };
1349
1554
  }
1350
- function generateCustomType(_type) {
1351
- return { type: "object" };
1352
- }
1353
- function applyConstraints(schema, constraints) {
1555
+ function applyConstraints(schema, constraints, ctx) {
1354
1556
  for (const constraint of constraints) {
1355
1557
  switch (constraint.constraintKind) {
1356
1558
  case "minimum":
@@ -1395,6 +1597,7 @@ function applyConstraints(schema, constraints) {
1395
1597
  case "allowedMembers":
1396
1598
  break;
1397
1599
  case "custom":
1600
+ applyCustomConstraint(schema, constraint, ctx);
1398
1601
  break;
1399
1602
  default: {
1400
1603
  const _exhaustive = constraint;
@@ -1403,7 +1606,7 @@ function applyConstraints(schema, constraints) {
1403
1606
  }
1404
1607
  }
1405
1608
  }
1406
- function applyAnnotations(schema, annotations) {
1609
+ function applyAnnotations(schema, annotations, ctx) {
1407
1610
  for (const annotation of annotations) {
1408
1611
  switch (annotation.annotationKind) {
1409
1612
  case "displayName":
@@ -1423,6 +1626,7 @@ function applyAnnotations(schema, annotations) {
1423
1626
  case "formatHint":
1424
1627
  break;
1425
1628
  case "custom":
1629
+ applyCustomAnnotation(schema, annotation, ctx);
1426
1630
  break;
1427
1631
  default: {
1428
1632
  const _exhaustive = annotation;
@@ -1431,6 +1635,36 @@ function applyAnnotations(schema, annotations) {
1431
1635
  }
1432
1636
  }
1433
1637
  }
1638
+ function generateCustomType(type, ctx) {
1639
+ const registration = ctx.extensionRegistry?.findType(type.typeId);
1640
+ if (registration === void 0) {
1641
+ throw new Error(
1642
+ `Cannot generate JSON Schema for custom type "${type.typeId}" without a matching extension registration`
1643
+ );
1644
+ }
1645
+ return registration.toJsonSchema(type.payload, ctx.vendorPrefix);
1646
+ }
1647
+ function applyCustomConstraint(schema, constraint, ctx) {
1648
+ const registration = ctx.extensionRegistry?.findConstraint(constraint.constraintId);
1649
+ if (registration === void 0) {
1650
+ throw new Error(
1651
+ `Cannot generate JSON Schema for custom constraint "${constraint.constraintId}" without a matching extension registration`
1652
+ );
1653
+ }
1654
+ Object.assign(schema, registration.toJsonSchema(constraint.payload, ctx.vendorPrefix));
1655
+ }
1656
+ function applyCustomAnnotation(schema, annotation, ctx) {
1657
+ const registration = ctx.extensionRegistry?.findAnnotation(annotation.annotationId);
1658
+ if (registration === void 0) {
1659
+ throw new Error(
1660
+ `Cannot generate JSON Schema for custom annotation "${annotation.annotationId}" without a matching extension registration`
1661
+ );
1662
+ }
1663
+ if (registration.toJsonSchema === void 0) {
1664
+ return;
1665
+ }
1666
+ Object.assign(schema, registration.toJsonSchema(annotation.value, ctx.vendorPrefix));
1667
+ }
1434
1668
 
1435
1669
  // src/ui-schema/schema.ts
1436
1670
  import { z } from "zod";
@@ -1655,12 +1889,9 @@ function generateClassSchemas(analysis, source) {
1655
1889
  }
1656
1890
 
1657
1891
  // src/validate/constraint-validator.ts
1658
- function makeCode(ctx, category, number) {
1659
- return `${ctx.vendorPrefix}-${category}-${String(number).padStart(3, "0")}`;
1660
- }
1661
1892
  function addContradiction(ctx, message, primary, related) {
1662
1893
  ctx.diagnostics.push({
1663
- code: makeCode(ctx, "CONTRADICTION", 1),
1894
+ code: "CONTRADICTING_CONSTRAINTS",
1664
1895
  message,
1665
1896
  severity: "error",
1666
1897
  primaryLocation: primary,
@@ -1669,7 +1900,7 @@ function addContradiction(ctx, message, primary, related) {
1669
1900
  }
1670
1901
  function addTypeMismatch(ctx, message, primary) {
1671
1902
  ctx.diagnostics.push({
1672
- code: makeCode(ctx, "TYPE_MISMATCH", 1),
1903
+ code: "TYPE_MISMATCH",
1673
1904
  message,
1674
1905
  severity: "error",
1675
1906
  primaryLocation: primary,
@@ -1678,28 +1909,153 @@ function addTypeMismatch(ctx, message, primary) {
1678
1909
  }
1679
1910
  function addUnknownExtension(ctx, message, primary) {
1680
1911
  ctx.diagnostics.push({
1681
- code: makeCode(ctx, "UNKNOWN_EXTENSION", 1),
1912
+ code: "UNKNOWN_EXTENSION",
1682
1913
  message,
1683
1914
  severity: "warning",
1684
1915
  primaryLocation: primary,
1685
1916
  relatedLocations: []
1686
1917
  });
1687
1918
  }
1919
+ function addConstraintBroadening(ctx, message, primary, related) {
1920
+ ctx.diagnostics.push({
1921
+ code: "CONSTRAINT_BROADENING",
1922
+ message,
1923
+ severity: "error",
1924
+ primaryLocation: primary,
1925
+ relatedLocations: [related]
1926
+ });
1927
+ }
1688
1928
  function findNumeric(constraints, constraintKind) {
1689
- return constraints.find(
1690
- (c) => c.constraintKind === constraintKind
1691
- );
1929
+ return constraints.find((c) => c.constraintKind === constraintKind);
1692
1930
  }
1693
1931
  function findLength(constraints, constraintKind) {
1694
- return constraints.find(
1695
- (c) => c.constraintKind === constraintKind
1696
- );
1932
+ return constraints.find((c) => c.constraintKind === constraintKind);
1697
1933
  }
1698
1934
  function findAllowedMembers(constraints) {
1699
1935
  return constraints.filter(
1700
1936
  (c) => c.constraintKind === "allowedMembers"
1701
1937
  );
1702
1938
  }
1939
+ function isOrderedBoundConstraint(constraint) {
1940
+ 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";
1941
+ }
1942
+ function pathKey(constraint) {
1943
+ return constraint.path?.segments.join(".") ?? "";
1944
+ }
1945
+ function orderedBoundFamily(kind) {
1946
+ switch (kind) {
1947
+ case "minimum":
1948
+ case "exclusiveMinimum":
1949
+ return "numeric-lower";
1950
+ case "maximum":
1951
+ case "exclusiveMaximum":
1952
+ return "numeric-upper";
1953
+ case "minLength":
1954
+ return "minLength";
1955
+ case "minItems":
1956
+ return "minItems";
1957
+ case "maxLength":
1958
+ return "maxLength";
1959
+ case "maxItems":
1960
+ return "maxItems";
1961
+ default: {
1962
+ const _exhaustive = kind;
1963
+ return _exhaustive;
1964
+ }
1965
+ }
1966
+ }
1967
+ function isNumericLowerKind(kind) {
1968
+ return kind === "minimum" || kind === "exclusiveMinimum";
1969
+ }
1970
+ function isNumericUpperKind(kind) {
1971
+ return kind === "maximum" || kind === "exclusiveMaximum";
1972
+ }
1973
+ function describeConstraintTag(constraint) {
1974
+ return `@${constraint.constraintKind}`;
1975
+ }
1976
+ function compareConstraintStrength(current, previous) {
1977
+ const family = orderedBoundFamily(current.constraintKind);
1978
+ if (family === "numeric-lower") {
1979
+ if (!isNumericLowerKind(current.constraintKind) || !isNumericLowerKind(previous.constraintKind)) {
1980
+ throw new Error("numeric-lower family received non-numeric lower-bound constraint");
1981
+ }
1982
+ if (current.value !== previous.value) {
1983
+ return current.value > previous.value ? 1 : -1;
1984
+ }
1985
+ if (current.constraintKind === "exclusiveMinimum" && previous.constraintKind === "minimum") {
1986
+ return 1;
1987
+ }
1988
+ if (current.constraintKind === "minimum" && previous.constraintKind === "exclusiveMinimum") {
1989
+ return -1;
1990
+ }
1991
+ return 0;
1992
+ }
1993
+ if (family === "numeric-upper") {
1994
+ if (!isNumericUpperKind(current.constraintKind) || !isNumericUpperKind(previous.constraintKind)) {
1995
+ throw new Error("numeric-upper family received non-numeric upper-bound constraint");
1996
+ }
1997
+ if (current.value !== previous.value) {
1998
+ return current.value < previous.value ? 1 : -1;
1999
+ }
2000
+ if (current.constraintKind === "exclusiveMaximum" && previous.constraintKind === "maximum") {
2001
+ return 1;
2002
+ }
2003
+ if (current.constraintKind === "maximum" && previous.constraintKind === "exclusiveMaximum") {
2004
+ return -1;
2005
+ }
2006
+ return 0;
2007
+ }
2008
+ switch (family) {
2009
+ case "minLength":
2010
+ case "minItems":
2011
+ if (current.value === previous.value) {
2012
+ return 0;
2013
+ }
2014
+ return current.value > previous.value ? 1 : -1;
2015
+ case "maxLength":
2016
+ case "maxItems":
2017
+ if (current.value === previous.value) {
2018
+ return 0;
2019
+ }
2020
+ return current.value < previous.value ? 1 : -1;
2021
+ default: {
2022
+ const _exhaustive = family;
2023
+ return _exhaustive;
2024
+ }
2025
+ }
2026
+ }
2027
+ function checkConstraintBroadening(ctx, fieldName, constraints) {
2028
+ const strongestByKey = /* @__PURE__ */ new Map();
2029
+ for (const constraint of constraints) {
2030
+ if (!isOrderedBoundConstraint(constraint)) {
2031
+ continue;
2032
+ }
2033
+ const key = `${orderedBoundFamily(constraint.constraintKind)}:${pathKey(constraint)}`;
2034
+ const previous = strongestByKey.get(key);
2035
+ if (previous === void 0) {
2036
+ strongestByKey.set(key, constraint);
2037
+ continue;
2038
+ }
2039
+ const strength = compareConstraintStrength(constraint, previous);
2040
+ if (strength < 0) {
2041
+ const displayFieldName = formatPathTargetFieldName(
2042
+ fieldName,
2043
+ constraint.path?.segments ?? []
2044
+ );
2045
+ addConstraintBroadening(
2046
+ ctx,
2047
+ `Field "${displayFieldName}": ${describeConstraintTag(constraint)} (${String(constraint.value)}) is broader than earlier ${describeConstraintTag(previous)} (${String(previous.value)}). Constraints can only narrow.`,
2048
+ constraint.provenance,
2049
+ previous.provenance
2050
+ );
2051
+ continue;
2052
+ }
2053
+ if (strength <= 0) {
2054
+ continue;
2055
+ }
2056
+ strongestByKey.set(key, constraint);
2057
+ }
2058
+ }
1703
2059
  function checkNumericContradictions(ctx, fieldName, constraints) {
1704
2060
  const min = findNumeric(constraints, "minimum");
1705
2061
  const max = findNumeric(constraints, "maximum");
@@ -1796,6 +2152,8 @@ function typeLabel(type) {
1796
2152
  return "array";
1797
2153
  case "object":
1798
2154
  return "object";
2155
+ case "record":
2156
+ return "record";
1799
2157
  case "union":
1800
2158
  return "union";
1801
2159
  case "reference":
@@ -1810,74 +2168,140 @@ function typeLabel(type) {
1810
2168
  }
1811
2169
  }
1812
2170
  }
1813
- function checkTypeApplicability(ctx, fieldName, type, constraints) {
1814
- const isNumber = type.kind === "primitive" && type.primitiveKind === "number";
1815
- const isString = type.kind === "primitive" && type.primitiveKind === "string";
1816
- const isArray = type.kind === "array";
1817
- const isEnum = type.kind === "enum";
1818
- const label = typeLabel(type);
1819
- for (const constraint of constraints) {
1820
- const ck = constraint.constraintKind;
1821
- switch (ck) {
1822
- case "minimum":
1823
- case "maximum":
1824
- case "exclusiveMinimum":
1825
- case "exclusiveMaximum":
1826
- case "multipleOf": {
1827
- if (!isNumber) {
1828
- addTypeMismatch(
1829
- ctx,
1830
- `Field "${fieldName}": constraint "${ck}" is only valid on number fields, but field type is "${label}"`,
1831
- constraint.provenance
1832
- );
1833
- }
1834
- break;
2171
+ function dereferenceType(ctx, type) {
2172
+ let current = type;
2173
+ const seen = /* @__PURE__ */ new Set();
2174
+ while (current.kind === "reference") {
2175
+ if (seen.has(current.name)) {
2176
+ return current;
2177
+ }
2178
+ seen.add(current.name);
2179
+ const definition = ctx.typeRegistry[current.name];
2180
+ if (definition === void 0) {
2181
+ return current;
2182
+ }
2183
+ current = definition.type;
2184
+ }
2185
+ return current;
2186
+ }
2187
+ function resolvePathTargetType(ctx, type, segments) {
2188
+ const effectiveType = dereferenceType(ctx, type);
2189
+ if (segments.length === 0) {
2190
+ return { kind: "resolved", type: effectiveType };
2191
+ }
2192
+ if (effectiveType.kind === "array") {
2193
+ return resolvePathTargetType(ctx, effectiveType.items, segments);
2194
+ }
2195
+ if (effectiveType.kind === "object") {
2196
+ const [segment, ...rest] = segments;
2197
+ if (segment === void 0) {
2198
+ throw new Error("Invariant violation: object path traversal requires a segment");
2199
+ }
2200
+ const property = effectiveType.properties.find((prop) => prop.name === segment);
2201
+ if (property === void 0) {
2202
+ return { kind: "missing-property", segment };
2203
+ }
2204
+ return resolvePathTargetType(ctx, property.type, rest);
2205
+ }
2206
+ return { kind: "unresolvable", type: effectiveType };
2207
+ }
2208
+ function formatPathTargetFieldName(fieldName, path2) {
2209
+ return path2.length === 0 ? fieldName : `${fieldName}.${path2.join(".")}`;
2210
+ }
2211
+ function checkConstraintOnType(ctx, fieldName, type, constraint) {
2212
+ const effectiveType = dereferenceType(ctx, type);
2213
+ const isNumber = effectiveType.kind === "primitive" && effectiveType.primitiveKind === "number";
2214
+ const isString = effectiveType.kind === "primitive" && effectiveType.primitiveKind === "string";
2215
+ const isArray = effectiveType.kind === "array";
2216
+ const isEnum = effectiveType.kind === "enum";
2217
+ const label = typeLabel(effectiveType);
2218
+ const ck = constraint.constraintKind;
2219
+ switch (ck) {
2220
+ case "minimum":
2221
+ case "maximum":
2222
+ case "exclusiveMinimum":
2223
+ case "exclusiveMaximum":
2224
+ case "multipleOf": {
2225
+ if (!isNumber) {
2226
+ addTypeMismatch(
2227
+ ctx,
2228
+ `Field "${fieldName}": constraint "${ck}" is only valid on number fields, but field type is "${label}"`,
2229
+ constraint.provenance
2230
+ );
1835
2231
  }
1836
- case "minLength":
1837
- case "maxLength":
1838
- case "pattern": {
1839
- if (!isString) {
1840
- addTypeMismatch(
1841
- ctx,
1842
- `Field "${fieldName}": constraint "${ck}" is only valid on string fields, but field type is "${label}"`,
1843
- constraint.provenance
1844
- );
1845
- }
1846
- break;
2232
+ break;
2233
+ }
2234
+ case "minLength":
2235
+ case "maxLength":
2236
+ case "pattern": {
2237
+ if (!isString) {
2238
+ addTypeMismatch(
2239
+ ctx,
2240
+ `Field "${fieldName}": constraint "${ck}" is only valid on string fields, but field type is "${label}"`,
2241
+ constraint.provenance
2242
+ );
1847
2243
  }
1848
- case "minItems":
1849
- case "maxItems":
1850
- case "uniqueItems": {
1851
- if (!isArray) {
1852
- addTypeMismatch(
1853
- ctx,
1854
- `Field "${fieldName}": constraint "${ck}" is only valid on array fields, but field type is "${label}"`,
1855
- constraint.provenance
1856
- );
1857
- }
1858
- break;
2244
+ break;
2245
+ }
2246
+ case "minItems":
2247
+ case "maxItems":
2248
+ case "uniqueItems": {
2249
+ if (!isArray) {
2250
+ addTypeMismatch(
2251
+ ctx,
2252
+ `Field "${fieldName}": constraint "${ck}" is only valid on array fields, but field type is "${label}"`,
2253
+ constraint.provenance
2254
+ );
1859
2255
  }
1860
- case "allowedMembers": {
1861
- if (!isEnum) {
1862
- addTypeMismatch(
1863
- ctx,
1864
- `Field "${fieldName}": constraint "allowedMembers" is only valid on enum fields, but field type is "${label}"`,
1865
- constraint.provenance
1866
- );
1867
- }
1868
- break;
2256
+ break;
2257
+ }
2258
+ case "allowedMembers": {
2259
+ if (!isEnum) {
2260
+ addTypeMismatch(
2261
+ ctx,
2262
+ `Field "${fieldName}": constraint "allowedMembers" is only valid on enum fields, but field type is "${label}"`,
2263
+ constraint.provenance
2264
+ );
1869
2265
  }
1870
- case "custom": {
1871
- checkCustomConstraint(ctx, fieldName, type, constraint);
1872
- break;
2266
+ break;
2267
+ }
2268
+ case "custom": {
2269
+ checkCustomConstraint(ctx, fieldName, effectiveType, constraint);
2270
+ break;
2271
+ }
2272
+ default: {
2273
+ const _exhaustive = constraint;
2274
+ throw new Error(
2275
+ `Unhandled constraint kind: ${_exhaustive.constraintKind}`
2276
+ );
2277
+ }
2278
+ }
2279
+ }
2280
+ function checkTypeApplicability(ctx, fieldName, type, constraints) {
2281
+ for (const constraint of constraints) {
2282
+ if (constraint.path) {
2283
+ const resolution = resolvePathTargetType(ctx, type, constraint.path.segments);
2284
+ const targetFieldName = formatPathTargetFieldName(fieldName, constraint.path.segments);
2285
+ if (resolution.kind === "missing-property") {
2286
+ addTypeMismatch(
2287
+ ctx,
2288
+ `Field "${fieldName}": path-targeted constraint "${constraint.constraintKind}" references unknown path segment "${resolution.segment}"`,
2289
+ constraint.provenance
2290
+ );
2291
+ continue;
1873
2292
  }
1874
- default: {
1875
- const _exhaustive = constraint;
1876
- throw new Error(
1877
- `Unhandled constraint kind: ${_exhaustive.constraintKind}`
2293
+ if (resolution.kind === "unresolvable") {
2294
+ addTypeMismatch(
2295
+ ctx,
2296
+ `Field "${targetFieldName}": path-targeted constraint "${constraint.constraintKind}" is invalid because type "${typeLabel(resolution.type)}" cannot be traversed`,
2297
+ constraint.provenance
1878
2298
  );
2299
+ continue;
1879
2300
  }
2301
+ checkConstraintOnType(ctx, targetFieldName, resolution.type, constraint);
2302
+ continue;
1880
2303
  }
2304
+ checkConstraintOnType(ctx, fieldName, type, constraint);
1881
2305
  }
1882
2306
  }
1883
2307
  function checkCustomConstraint(ctx, fieldName, type, constraint) {
@@ -1921,6 +2345,7 @@ function validateConstraints(ctx, name, type, constraints) {
1921
2345
  checkNumericContradictions(ctx, name, constraints);
1922
2346
  checkLengthContradictions(ctx, name, constraints);
1923
2347
  checkAllowedMembersContradiction(ctx, name, constraints);
2348
+ checkConstraintBroadening(ctx, name, constraints);
1924
2349
  checkTypeApplicability(ctx, name, type, constraints);
1925
2350
  }
1926
2351
  function validateElement(ctx, element) {
@@ -1947,8 +2372,8 @@ function validateElement(ctx, element) {
1947
2372
  function validateIR(ir, options) {
1948
2373
  const ctx = {
1949
2374
  diagnostics: [],
1950
- vendorPrefix: options?.vendorPrefix ?? "FORMSPEC",
1951
- extensionRegistry: options?.extensionRegistry
2375
+ extensionRegistry: options?.extensionRegistry,
2376
+ typeRegistry: ir.typeRegistry
1952
2377
  };
1953
2378
  for (const element of ir.elements) {
1954
2379
  validateElement(ctx, element);