@formspec/build 0.1.0-alpha.14 → 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 (60) 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 +11 -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__/jsdoc-constraints.test.d.ts +4 -5
  9. package/dist/__tests__/jsdoc-constraints.test.d.ts.map +1 -1
  10. package/dist/__tests__/parity/fixtures/plan-status/chain-dsl.d.ts +19 -0
  11. package/dist/__tests__/parity/fixtures/plan-status/chain-dsl.d.ts.map +1 -0
  12. package/dist/__tests__/parity/fixtures/plan-status/expected-ir.d.ts +6 -0
  13. package/dist/__tests__/parity/fixtures/plan-status/expected-ir.d.ts.map +1 -0
  14. package/dist/__tests__/parity/fixtures/plan-status/tsdoc.d.ts +17 -0
  15. package/dist/__tests__/parity/fixtures/plan-status/tsdoc.d.ts.map +1 -0
  16. package/dist/__tests__/parity/fixtures/usd-cents/chain-dsl.d.ts +9 -0
  17. package/dist/__tests__/parity/fixtures/usd-cents/chain-dsl.d.ts.map +1 -0
  18. package/dist/__tests__/parity/fixtures/usd-cents/expected-ir.d.ts +6 -0
  19. package/dist/__tests__/parity/fixtures/usd-cents/expected-ir.d.ts.map +1 -0
  20. package/dist/__tests__/parity/fixtures/usd-cents/tsdoc.d.ts +19 -0
  21. package/dist/__tests__/parity/fixtures/usd-cents/tsdoc.d.ts.map +1 -0
  22. package/dist/__tests__/parity/utils.d.ts +6 -1
  23. package/dist/__tests__/parity/utils.d.ts.map +1 -1
  24. package/dist/analyzer/class-analyzer.d.ts +1 -1
  25. package/dist/analyzer/class-analyzer.d.ts.map +1 -1
  26. package/dist/analyzer/jsdoc-constraints.d.ts +7 -51
  27. package/dist/analyzer/jsdoc-constraints.d.ts.map +1 -1
  28. package/dist/analyzer/tsdoc-parser.d.ts +6 -8
  29. package/dist/analyzer/tsdoc-parser.d.ts.map +1 -1
  30. package/dist/browser.cjs +387 -98
  31. package/dist/browser.cjs.map +1 -1
  32. package/dist/browser.d.ts +15 -2
  33. package/dist/browser.d.ts.map +1 -1
  34. package/dist/browser.js +385 -98
  35. package/dist/browser.js.map +1 -1
  36. package/dist/build.d.ts +131 -5
  37. package/dist/canonicalize/tsdoc-canonicalizer.d.ts +3 -3
  38. package/dist/cli.cjs +272 -69
  39. package/dist/cli.cjs.map +1 -1
  40. package/dist/cli.js +271 -72
  41. package/dist/cli.js.map +1 -1
  42. package/dist/index.cjs +257 -67
  43. package/dist/index.cjs.map +1 -1
  44. package/dist/index.d.ts +20 -3
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +255 -71
  47. package/dist/index.js.map +1 -1
  48. package/dist/internals.cjs +461 -137
  49. package/dist/internals.cjs.map +1 -1
  50. package/dist/internals.js +459 -139
  51. package/dist/internals.js.map +1 -1
  52. package/dist/json-schema/generator.d.ts +8 -2
  53. package/dist/json-schema/generator.d.ts.map +1 -1
  54. package/dist/json-schema/ir-generator.d.ts +24 -2
  55. package/dist/json-schema/ir-generator.d.ts.map +1 -1
  56. package/dist/json-schema/types.d.ts +1 -1
  57. package/dist/json-schema/types.d.ts.map +1 -1
  58. package/dist/validate/constraint-validator.d.ts +3 -7
  59. package/dist/validate/constraint-validator.d.ts.map +1 -1
  60. package/package.json +1 -1
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,11 +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
- isBuiltinConstraintName as isBuiltinConstraintName2,
466
- normalizeConstraintTagName as normalizeConstraintTagName2
467
- } from "@formspec/core";
468
463
 
469
464
  // src/analyzer/tsdoc-parser.ts
470
465
  import * as ts2 from "typescript";
@@ -518,6 +513,15 @@ function createFormSpecTSDocConfig() {
518
513
  })
519
514
  );
520
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
+ }
521
525
  return config;
522
526
  }
523
527
  var sharedParser;
@@ -547,6 +551,27 @@ function parseTSDocTags(node, file = "") {
547
551
  const docComment = parserContext.docComment;
548
552
  for (const block of docComment.customBlocks) {
549
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
+ }
550
575
  if (TAGS_REQUIRING_RAW_TEXT.has(tagName)) continue;
551
576
  const text = extractBlockText(block).trim();
552
577
  if (text === "") continue;
@@ -578,41 +603,6 @@ function parseTSDocTags(node, file = "") {
578
603
  constraints.push(constraintNode);
579
604
  }
580
605
  }
581
- let displayName;
582
- let description;
583
- let displayNameTag;
584
- let descriptionTag;
585
- for (const tag of jsDocTagsAll) {
586
- const tagName = tag.tagName.text;
587
- const commentText = getTagCommentText(tag);
588
- if (commentText === void 0 || commentText.trim() === "") {
589
- continue;
590
- }
591
- const trimmed = commentText.trim();
592
- if (tagName === "Field_displayName") {
593
- displayName = trimmed;
594
- displayNameTag = tag;
595
- } else if (tagName === "Field_description") {
596
- description = trimmed;
597
- descriptionTag = tag;
598
- }
599
- }
600
- if (displayName !== void 0 && displayNameTag) {
601
- annotations.push({
602
- kind: "annotation",
603
- annotationKind: "displayName",
604
- value: displayName,
605
- provenance: provenanceForJSDocTag(displayNameTag, file)
606
- });
607
- }
608
- if (description !== void 0 && descriptionTag) {
609
- annotations.push({
610
- kind: "annotation",
611
- annotationKind: "description",
612
- value: description,
613
- provenance: provenanceForJSDocTag(descriptionTag, file)
614
- });
615
- }
616
606
  return { constraints, annotations };
617
607
  }
618
608
  function extractPathTarget(text) {
@@ -877,18 +867,19 @@ function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting) {
877
867
  const tsType = checker.getTypeAtLocation(prop);
878
868
  const optional = prop.questionToken !== void 0;
879
869
  const provenance = provenanceForNode(prop, file);
880
- const type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting);
870
+ let type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting);
881
871
  const constraints = [];
882
872
  if (prop.type) {
883
873
  constraints.push(...extractTypeAliasConstraintNodes(prop.type, checker, file));
884
874
  }
885
875
  constraints.push(...extractJSDocConstraintNodes(prop, file));
886
- const annotations = [];
876
+ let annotations = [];
887
877
  annotations.push(...extractJSDocAnnotationNodes(prop, file));
888
878
  const defaultAnnotation = extractDefaultValueAnnotation(prop.initializer, file);
889
879
  if (defaultAnnotation) {
890
880
  annotations.push(defaultAnnotation);
891
881
  }
882
+ ({ type, annotations } = applyEnumMemberDisplayNames(type, annotations));
892
883
  return {
893
884
  kind: "field",
894
885
  name,
@@ -907,14 +898,15 @@ function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visitin
907
898
  const tsType = checker.getTypeAtLocation(prop);
908
899
  const optional = prop.questionToken !== void 0;
909
900
  const provenance = provenanceForNode(prop, file);
910
- const type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting);
901
+ let type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting);
911
902
  const constraints = [];
912
903
  if (prop.type) {
913
904
  constraints.push(...extractTypeAliasConstraintNodes(prop.type, checker, file));
914
905
  }
915
906
  constraints.push(...extractJSDocConstraintNodes(prop, file));
916
- const annotations = [];
907
+ let annotations = [];
917
908
  annotations.push(...extractJSDocAnnotationNodes(prop, file));
909
+ ({ type, annotations } = applyEnumMemberDisplayNames(type, annotations));
918
910
  return {
919
911
  kind: "field",
920
912
  name,
@@ -925,6 +917,68 @@ function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visitin
925
917
  provenance
926
918
  };
927
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
+ }
928
982
  function resolveTypeNode(type, checker, file, typeRegistry, visiting) {
929
983
  if (type.flags & ts4.TypeFlags.String) {
930
984
  return { kind: "primitive", primitiveKind: "string" };
@@ -1035,7 +1089,30 @@ function resolveArrayType(type, checker, file, typeRegistry, visiting) {
1035
1089
  const items = elementType ? resolveTypeNode(elementType, checker, file, typeRegistry, visiting) : { kind: "primitive", primitiveKind: "string" };
1036
1090
  return { kind: "array", items };
1037
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
+ }
1038
1111
  function resolveObjectType(type, checker, file, typeRegistry, visiting) {
1112
+ const recordNode = tryResolveRecordType(type, checker, file, typeRegistry, visiting);
1113
+ if (recordNode) {
1114
+ return recordNode;
1115
+ }
1039
1116
  if (visiting.has(type)) {
1040
1117
  return { kind: "object", properties: [], additionalProperties: false };
1041
1118
  }
@@ -1067,7 +1144,7 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting) {
1067
1144
  const objectNode = {
1068
1145
  kind: "object",
1069
1146
  properties,
1070
- additionalProperties: false
1147
+ additionalProperties: true
1071
1148
  };
1072
1149
  if (typeName) {
1073
1150
  typeRegistry[typeName] = {
@@ -1227,11 +1304,21 @@ function detectFormSpecReference(typeNode) {
1227
1304
  }
1228
1305
 
1229
1306
  // src/json-schema/ir-generator.ts
1230
- function makeContext() {
1231
- 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
+ };
1232
1319
  }
1233
- function generateJsonSchemaFromIR(ir) {
1234
- const ctx = makeContext();
1320
+ function generateJsonSchemaFromIR(ir, options) {
1321
+ const ctx = makeContext(options);
1235
1322
  for (const [name, typeDef] of Object.entries(ir.typeRegistry)) {
1236
1323
  ctx.defs[name] = generateTypeNode(typeDef.type, ctx);
1237
1324
  }
@@ -1283,16 +1370,16 @@ function generateFieldSchema(field, ctx) {
1283
1370
  directConstraints.push(c);
1284
1371
  }
1285
1372
  }
1286
- applyConstraints(schema, directConstraints);
1287
- applyAnnotations(schema, field.annotations);
1373
+ applyConstraints(schema, directConstraints, ctx);
1374
+ applyAnnotations(schema, field.annotations, ctx);
1288
1375
  if (pathConstraints.length === 0) {
1289
1376
  return schema;
1290
1377
  }
1291
- return applyPathTargetedConstraints(schema, pathConstraints);
1378
+ return applyPathTargetedConstraints(schema, pathConstraints, ctx);
1292
1379
  }
1293
- function applyPathTargetedConstraints(schema, pathConstraints) {
1380
+ function applyPathTargetedConstraints(schema, pathConstraints, ctx) {
1294
1381
  if (schema.type === "array" && schema.items) {
1295
- schema.items = applyPathTargetedConstraints(schema.items, pathConstraints);
1382
+ schema.items = applyPathTargetedConstraints(schema.items, pathConstraints, ctx);
1296
1383
  return schema;
1297
1384
  }
1298
1385
  const byTarget = /* @__PURE__ */ new Map();
@@ -1306,7 +1393,7 @@ function applyPathTargetedConstraints(schema, pathConstraints) {
1306
1393
  const propertyOverrides = {};
1307
1394
  for (const [target, constraints] of byTarget) {
1308
1395
  const subSchema = {};
1309
- applyConstraints(subSchema, constraints);
1396
+ applyConstraints(subSchema, constraints, ctx);
1310
1397
  propertyOverrides[target] = subSchema;
1311
1398
  }
1312
1399
  if (schema.$ref) {
@@ -1350,6 +1437,8 @@ function generateTypeNode(type, ctx) {
1350
1437
  return generateArrayType(type, ctx);
1351
1438
  case "object":
1352
1439
  return generateObjectType(type, ctx);
1440
+ case "record":
1441
+ return generateRecordType(type, ctx);
1353
1442
  case "union":
1354
1443
  return generateUnionType(type, ctx);
1355
1444
  case "reference":
@@ -1357,7 +1446,7 @@ function generateTypeNode(type, ctx) {
1357
1446
  case "dynamic":
1358
1447
  return generateDynamicType(type);
1359
1448
  case "custom":
1360
- return generateCustomType(type);
1449
+ return generateCustomType(type, ctx);
1361
1450
  default: {
1362
1451
  const _exhaustive = type;
1363
1452
  return _exhaustive;
@@ -1406,16 +1495,27 @@ function generateObjectType(type, ctx) {
1406
1495
  }
1407
1496
  return schema;
1408
1497
  }
1498
+ function generateRecordType(type, ctx) {
1499
+ return {
1500
+ type: "object",
1501
+ additionalProperties: generateTypeNode(type.valueType, ctx)
1502
+ };
1503
+ }
1409
1504
  function generatePropertySchema(prop, ctx) {
1410
1505
  const schema = generateTypeNode(prop.type, ctx);
1411
- applyConstraints(schema, prop.constraints);
1412
- applyAnnotations(schema, prop.annotations);
1506
+ applyConstraints(schema, prop.constraints, ctx);
1507
+ applyAnnotations(schema, prop.annotations, ctx);
1413
1508
  return schema;
1414
1509
  }
1415
1510
  function generateUnionType(type, ctx) {
1416
1511
  if (isBooleanUnion(type)) {
1417
1512
  return { type: "boolean" };
1418
1513
  }
1514
+ if (isNullableUnion(type)) {
1515
+ return {
1516
+ oneOf: type.members.map((m) => generateTypeNode(m, ctx))
1517
+ };
1518
+ }
1419
1519
  return {
1420
1520
  anyOf: type.members.map((m) => generateTypeNode(m, ctx))
1421
1521
  };
@@ -1425,6 +1525,13 @@ function isBooleanUnion(type) {
1425
1525
  const kinds = type.members.map((m) => m.kind);
1426
1526
  return kinds.every((k) => k === "primitive") && type.members.every((m) => m.kind === "primitive" && m.primitiveKind === "boolean");
1427
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
+ }
1428
1535
  function generateReferenceType(type) {
1429
1536
  return { $ref: `#/$defs/${type.name}` };
1430
1537
  }
@@ -1445,10 +1552,7 @@ function generateDynamicType(type) {
1445
1552
  "x-formspec-schemaSource": type.sourceKey
1446
1553
  };
1447
1554
  }
1448
- function generateCustomType(_type) {
1449
- return { type: "object" };
1450
- }
1451
- function applyConstraints(schema, constraints) {
1555
+ function applyConstraints(schema, constraints, ctx) {
1452
1556
  for (const constraint of constraints) {
1453
1557
  switch (constraint.constraintKind) {
1454
1558
  case "minimum":
@@ -1493,6 +1597,7 @@ function applyConstraints(schema, constraints) {
1493
1597
  case "allowedMembers":
1494
1598
  break;
1495
1599
  case "custom":
1600
+ applyCustomConstraint(schema, constraint, ctx);
1496
1601
  break;
1497
1602
  default: {
1498
1603
  const _exhaustive = constraint;
@@ -1501,7 +1606,7 @@ function applyConstraints(schema, constraints) {
1501
1606
  }
1502
1607
  }
1503
1608
  }
1504
- function applyAnnotations(schema, annotations) {
1609
+ function applyAnnotations(schema, annotations, ctx) {
1505
1610
  for (const annotation of annotations) {
1506
1611
  switch (annotation.annotationKind) {
1507
1612
  case "displayName":
@@ -1521,6 +1626,7 @@ function applyAnnotations(schema, annotations) {
1521
1626
  case "formatHint":
1522
1627
  break;
1523
1628
  case "custom":
1629
+ applyCustomAnnotation(schema, annotation, ctx);
1524
1630
  break;
1525
1631
  default: {
1526
1632
  const _exhaustive = annotation;
@@ -1529,6 +1635,36 @@ function applyAnnotations(schema, annotations) {
1529
1635
  }
1530
1636
  }
1531
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
+ }
1532
1668
 
1533
1669
  // src/ui-schema/schema.ts
1534
1670
  import { z } from "zod";
@@ -1753,12 +1889,9 @@ function generateClassSchemas(analysis, source) {
1753
1889
  }
1754
1890
 
1755
1891
  // src/validate/constraint-validator.ts
1756
- function makeCode(ctx, category, number) {
1757
- return `${ctx.vendorPrefix}-${category}-${String(number).padStart(3, "0")}`;
1758
- }
1759
1892
  function addContradiction(ctx, message, primary, related) {
1760
1893
  ctx.diagnostics.push({
1761
- code: makeCode(ctx, "CONTRADICTION", 1),
1894
+ code: "CONTRADICTING_CONSTRAINTS",
1762
1895
  message,
1763
1896
  severity: "error",
1764
1897
  primaryLocation: primary,
@@ -1767,7 +1900,7 @@ function addContradiction(ctx, message, primary, related) {
1767
1900
  }
1768
1901
  function addTypeMismatch(ctx, message, primary) {
1769
1902
  ctx.diagnostics.push({
1770
- code: makeCode(ctx, "TYPE_MISMATCH", 1),
1903
+ code: "TYPE_MISMATCH",
1771
1904
  message,
1772
1905
  severity: "error",
1773
1906
  primaryLocation: primary,
@@ -1776,13 +1909,22 @@ function addTypeMismatch(ctx, message, primary) {
1776
1909
  }
1777
1910
  function addUnknownExtension(ctx, message, primary) {
1778
1911
  ctx.diagnostics.push({
1779
- code: makeCode(ctx, "UNKNOWN_EXTENSION", 1),
1912
+ code: "UNKNOWN_EXTENSION",
1780
1913
  message,
1781
1914
  severity: "warning",
1782
1915
  primaryLocation: primary,
1783
1916
  relatedLocations: []
1784
1917
  });
1785
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
+ }
1786
1928
  function findNumeric(constraints, constraintKind) {
1787
1929
  return constraints.find((c) => c.constraintKind === constraintKind);
1788
1930
  }
@@ -1794,6 +1936,126 @@ function findAllowedMembers(constraints) {
1794
1936
  (c) => c.constraintKind === "allowedMembers"
1795
1937
  );
1796
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
+ }
1797
2059
  function checkNumericContradictions(ctx, fieldName, constraints) {
1798
2060
  const min = findNumeric(constraints, "minimum");
1799
2061
  const max = findNumeric(constraints, "maximum");
@@ -1890,6 +2152,8 @@ function typeLabel(type) {
1890
2152
  return "array";
1891
2153
  case "object":
1892
2154
  return "object";
2155
+ case "record":
2156
+ return "record";
1893
2157
  case "union":
1894
2158
  return "union";
1895
2159
  case "reference":
@@ -1904,85 +2168,140 @@ function typeLabel(type) {
1904
2168
  }
1905
2169
  }
1906
2170
  }
1907
- function checkTypeApplicability(ctx, fieldName, type, constraints) {
1908
- const isNumber = type.kind === "primitive" && type.primitiveKind === "number";
1909
- const isString = type.kind === "primitive" && type.primitiveKind === "string";
1910
- const isArray = type.kind === "array";
1911
- const isEnum = type.kind === "enum";
1912
- const label = typeLabel(type);
1913
- for (const constraint of constraints) {
1914
- if (constraint.path) {
1915
- const isTraversable = type.kind === "object" || type.kind === "array" || type.kind === "reference";
1916
- if (!isTraversable) {
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) {
1917
2226
  addTypeMismatch(
1918
2227
  ctx,
1919
- `Field "${fieldName}": path-targeted constraint "${constraint.constraintKind}" is invalid because type "${label}" cannot be traversed`,
2228
+ `Field "${fieldName}": constraint "${ck}" is only valid on number fields, but field type is "${label}"`,
1920
2229
  constraint.provenance
1921
2230
  );
1922
2231
  }
1923
- continue;
2232
+ break;
1924
2233
  }
1925
- const ck = constraint.constraintKind;
1926
- switch (ck) {
1927
- case "minimum":
1928
- case "maximum":
1929
- case "exclusiveMinimum":
1930
- case "exclusiveMaximum":
1931
- case "multipleOf": {
1932
- if (!isNumber) {
1933
- addTypeMismatch(
1934
- ctx,
1935
- `Field "${fieldName}": constraint "${ck}" is only valid on number fields, but field type is "${label}"`,
1936
- constraint.provenance
1937
- );
1938
- }
1939
- break;
1940
- }
1941
- case "minLength":
1942
- case "maxLength":
1943
- case "pattern": {
1944
- if (!isString) {
1945
- addTypeMismatch(
1946
- ctx,
1947
- `Field "${fieldName}": constraint "${ck}" is only valid on string fields, but field type is "${label}"`,
1948
- constraint.provenance
1949
- );
1950
- }
1951
- break;
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
+ );
1952
2243
  }
1953
- case "minItems":
1954
- case "maxItems":
1955
- case "uniqueItems": {
1956
- if (!isArray) {
1957
- addTypeMismatch(
1958
- ctx,
1959
- `Field "${fieldName}": constraint "${ck}" is only valid on array fields, but field type is "${label}"`,
1960
- constraint.provenance
1961
- );
1962
- }
1963
- 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
+ );
1964
2255
  }
1965
- case "allowedMembers": {
1966
- if (!isEnum) {
1967
- addTypeMismatch(
1968
- ctx,
1969
- `Field "${fieldName}": constraint "allowedMembers" is only valid on enum fields, but field type is "${label}"`,
1970
- constraint.provenance
1971
- );
1972
- }
1973
- 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
+ );
1974
2265
  }
1975
- case "custom": {
1976
- checkCustomConstraint(ctx, fieldName, type, constraint);
1977
- 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;
1978
2292
  }
1979
- default: {
1980
- const _exhaustive = constraint;
1981
- throw new Error(
1982
- `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
1983
2298
  );
2299
+ continue;
1984
2300
  }
2301
+ checkConstraintOnType(ctx, targetFieldName, resolution.type, constraint);
2302
+ continue;
1985
2303
  }
2304
+ checkConstraintOnType(ctx, fieldName, type, constraint);
1986
2305
  }
1987
2306
  }
1988
2307
  function checkCustomConstraint(ctx, fieldName, type, constraint) {
@@ -2026,6 +2345,7 @@ function validateConstraints(ctx, name, type, constraints) {
2026
2345
  checkNumericContradictions(ctx, name, constraints);
2027
2346
  checkLengthContradictions(ctx, name, constraints);
2028
2347
  checkAllowedMembersContradiction(ctx, name, constraints);
2348
+ checkConstraintBroadening(ctx, name, constraints);
2029
2349
  checkTypeApplicability(ctx, name, type, constraints);
2030
2350
  }
2031
2351
  function validateElement(ctx, element) {
@@ -2052,8 +2372,8 @@ function validateElement(ctx, element) {
2052
2372
  function validateIR(ir, options) {
2053
2373
  const ctx = {
2054
2374
  diagnostics: [],
2055
- vendorPrefix: options?.vendorPrefix ?? "FORMSPEC",
2056
- extensionRegistry: options?.extensionRegistry
2375
+ extensionRegistry: options?.extensionRegistry,
2376
+ typeRegistry: ir.typeRegistry
2057
2377
  };
2058
2378
  for (const element of ir.elements) {
2059
2379
  validateElement(ctx, element);