@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
@@ -228,7 +228,7 @@ function canonicalizeArrayField(field) {
228
228
  const itemsType = {
229
229
  kind: "object",
230
230
  properties: itemProperties,
231
- additionalProperties: false
231
+ additionalProperties: true
232
232
  };
233
233
  const type = { kind: "array", items: itemsType };
234
234
  const constraints = [];
@@ -263,7 +263,7 @@ function canonicalizeObjectField(field) {
263
263
  const type = {
264
264
  kind: "object",
265
265
  properties,
266
- additionalProperties: false
266
+ additionalProperties: true
267
267
  };
268
268
  return buildFieldNode(field.name, type, field.required, buildAnnotations(field.label));
269
269
  }
@@ -511,7 +511,6 @@ var ts4 = __toESM(require("typescript"), 1);
511
511
 
512
512
  // src/analyzer/jsdoc-constraints.ts
513
513
  var ts3 = __toESM(require("typescript"), 1);
514
- var import_core4 = require("@formspec/core");
515
514
 
516
515
  // src/analyzer/tsdoc-parser.ts
517
516
  var ts2 = __toESM(require("typescript"), 1);
@@ -553,6 +552,15 @@ function createFormSpecTSDocConfig() {
553
552
  })
554
553
  );
555
554
  }
555
+ for (const tagName of ["displayName", "description"]) {
556
+ config.addTagDefinition(
557
+ new import_tsdoc.TSDocTagDefinition({
558
+ tagName: "@" + tagName,
559
+ syntaxKind: import_tsdoc.TSDocTagSyntaxKind.BlockTag,
560
+ allowMultiple: true
561
+ })
562
+ );
563
+ }
556
564
  return config;
557
565
  }
558
566
  var sharedParser;
@@ -582,6 +590,27 @@ function parseTSDocTags(node, file = "") {
582
590
  const docComment = parserContext.docComment;
583
591
  for (const block of docComment.customBlocks) {
584
592
  const tagName = (0, import_core3.normalizeConstraintTagName)(block.blockTag.tagName.substring(1));
593
+ if (tagName === "displayName" || tagName === "description") {
594
+ const text2 = extractBlockText(block).trim();
595
+ if (text2 === "") continue;
596
+ const provenance2 = provenanceForComment(range, sourceFile, file, tagName);
597
+ if (tagName === "displayName") {
598
+ annotations.push({
599
+ kind: "annotation",
600
+ annotationKind: "displayName",
601
+ value: text2,
602
+ provenance: provenance2
603
+ });
604
+ } else {
605
+ annotations.push({
606
+ kind: "annotation",
607
+ annotationKind: "description",
608
+ value: text2,
609
+ provenance: provenance2
610
+ });
611
+ }
612
+ continue;
613
+ }
585
614
  if (TAGS_REQUIRING_RAW_TEXT.has(tagName)) continue;
586
615
  const text = extractBlockText(block).trim();
587
616
  if (text === "") continue;
@@ -613,41 +642,6 @@ function parseTSDocTags(node, file = "") {
613
642
  constraints.push(constraintNode);
614
643
  }
615
644
  }
616
- let displayName;
617
- let description;
618
- let displayNameTag;
619
- let descriptionTag;
620
- for (const tag of jsDocTagsAll) {
621
- const tagName = tag.tagName.text;
622
- const commentText = getTagCommentText(tag);
623
- if (commentText === void 0 || commentText.trim() === "") {
624
- continue;
625
- }
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
- });
650
- }
651
645
  return { constraints, annotations };
652
646
  }
653
647
  function extractPathTarget(text) {
@@ -912,18 +906,19 @@ function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting) {
912
906
  const tsType = checker.getTypeAtLocation(prop);
913
907
  const optional = prop.questionToken !== void 0;
914
908
  const provenance = provenanceForNode(prop, file);
915
- const type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting);
909
+ let type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting);
916
910
  const constraints = [];
917
911
  if (prop.type) {
918
912
  constraints.push(...extractTypeAliasConstraintNodes(prop.type, checker, file));
919
913
  }
920
914
  constraints.push(...extractJSDocConstraintNodes(prop, file));
921
- const annotations = [];
915
+ let annotations = [];
922
916
  annotations.push(...extractJSDocAnnotationNodes(prop, file));
923
917
  const defaultAnnotation = extractDefaultValueAnnotation(prop.initializer, file);
924
918
  if (defaultAnnotation) {
925
919
  annotations.push(defaultAnnotation);
926
920
  }
921
+ ({ type, annotations } = applyEnumMemberDisplayNames(type, annotations));
927
922
  return {
928
923
  kind: "field",
929
924
  name,
@@ -942,14 +937,15 @@ function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visitin
942
937
  const tsType = checker.getTypeAtLocation(prop);
943
938
  const optional = prop.questionToken !== void 0;
944
939
  const provenance = provenanceForNode(prop, file);
945
- const type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting);
940
+ let type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting);
946
941
  const constraints = [];
947
942
  if (prop.type) {
948
943
  constraints.push(...extractTypeAliasConstraintNodes(prop.type, checker, file));
949
944
  }
950
945
  constraints.push(...extractJSDocConstraintNodes(prop, file));
951
- const annotations = [];
946
+ let annotations = [];
952
947
  annotations.push(...extractJSDocAnnotationNodes(prop, file));
948
+ ({ type, annotations } = applyEnumMemberDisplayNames(type, annotations));
953
949
  return {
954
950
  kind: "field",
955
951
  name,
@@ -960,6 +956,68 @@ function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visitin
960
956
  provenance
961
957
  };
962
958
  }
959
+ function applyEnumMemberDisplayNames(type, annotations) {
960
+ if (!annotations.some(
961
+ (annotation) => annotation.annotationKind === "displayName" && annotation.value.trim().startsWith(":")
962
+ )) {
963
+ return { type, annotations: [...annotations] };
964
+ }
965
+ const consumed = /* @__PURE__ */ new Set();
966
+ const nextType = rewriteEnumDisplayNames(type, annotations, consumed);
967
+ if (consumed.size === 0) {
968
+ return { type, annotations: [...annotations] };
969
+ }
970
+ return {
971
+ type: nextType,
972
+ annotations: annotations.filter((annotation) => !consumed.has(annotation))
973
+ };
974
+ }
975
+ function rewriteEnumDisplayNames(type, annotations, consumed) {
976
+ switch (type.kind) {
977
+ case "enum":
978
+ return applyEnumMemberDisplayNamesToEnum(type, annotations, consumed);
979
+ case "union": {
980
+ return {
981
+ ...type,
982
+ members: type.members.map(
983
+ (member) => rewriteEnumDisplayNames(member, annotations, consumed)
984
+ )
985
+ };
986
+ }
987
+ default:
988
+ return type;
989
+ }
990
+ }
991
+ function applyEnumMemberDisplayNamesToEnum(type, annotations, consumed) {
992
+ const displayNames = /* @__PURE__ */ new Map();
993
+ for (const annotation of annotations) {
994
+ if (annotation.annotationKind !== "displayName") continue;
995
+ const parsed = parseEnumMemberDisplayName(annotation.value);
996
+ if (!parsed) continue;
997
+ consumed.add(annotation);
998
+ const member = type.members.find((m) => String(m.value) === parsed.value);
999
+ if (!member) continue;
1000
+ displayNames.set(String(member.value), parsed.label);
1001
+ }
1002
+ if (displayNames.size === 0) {
1003
+ return type;
1004
+ }
1005
+ return {
1006
+ ...type,
1007
+ members: type.members.map((member) => {
1008
+ const displayName = displayNames.get(String(member.value));
1009
+ return displayName !== void 0 ? { ...member, displayName } : member;
1010
+ })
1011
+ };
1012
+ }
1013
+ function parseEnumMemberDisplayName(value) {
1014
+ const trimmed = value.trim();
1015
+ const match = /^:([^\s]+)\s+([\s\S]+)$/.exec(trimmed);
1016
+ if (!match?.[1] || !match[2]) return null;
1017
+ const label = match[2].trim();
1018
+ if (label === "") return null;
1019
+ return { value: match[1], label };
1020
+ }
963
1021
  function resolveTypeNode(type, checker, file, typeRegistry, visiting) {
964
1022
  if (type.flags & ts4.TypeFlags.String) {
965
1023
  return { kind: "primitive", primitiveKind: "string" };
@@ -1070,7 +1128,30 @@ function resolveArrayType(type, checker, file, typeRegistry, visiting) {
1070
1128
  const items = elementType ? resolveTypeNode(elementType, checker, file, typeRegistry, visiting) : { kind: "primitive", primitiveKind: "string" };
1071
1129
  return { kind: "array", items };
1072
1130
  }
1131
+ function tryResolveRecordType(type, checker, file, typeRegistry, visiting) {
1132
+ if (type.getProperties().length > 0) {
1133
+ return null;
1134
+ }
1135
+ const indexInfo = checker.getIndexInfoOfType(type, ts4.IndexKind.String);
1136
+ if (!indexInfo) {
1137
+ return null;
1138
+ }
1139
+ if (visiting.has(type)) {
1140
+ return null;
1141
+ }
1142
+ visiting.add(type);
1143
+ try {
1144
+ const valueType = resolveTypeNode(indexInfo.type, checker, file, typeRegistry, visiting);
1145
+ return { kind: "record", valueType };
1146
+ } finally {
1147
+ visiting.delete(type);
1148
+ }
1149
+ }
1073
1150
  function resolveObjectType(type, checker, file, typeRegistry, visiting) {
1151
+ const recordNode = tryResolveRecordType(type, checker, file, typeRegistry, visiting);
1152
+ if (recordNode) {
1153
+ return recordNode;
1154
+ }
1074
1155
  if (visiting.has(type)) {
1075
1156
  return { kind: "object", properties: [], additionalProperties: false };
1076
1157
  }
@@ -1102,7 +1183,7 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting) {
1102
1183
  const objectNode = {
1103
1184
  kind: "object",
1104
1185
  properties,
1105
- additionalProperties: false
1186
+ additionalProperties: true
1106
1187
  };
1107
1188
  if (typeName) {
1108
1189
  typeRegistry[typeName] = {
@@ -1262,11 +1343,21 @@ function detectFormSpecReference(typeNode) {
1262
1343
  }
1263
1344
 
1264
1345
  // src/json-schema/ir-generator.ts
1265
- function makeContext() {
1266
- return { defs: {} };
1346
+ function makeContext(options) {
1347
+ const vendorPrefix = options?.vendorPrefix ?? "x-formspec";
1348
+ if (!vendorPrefix.startsWith("x-")) {
1349
+ throw new Error(
1350
+ `Invalid vendorPrefix "${vendorPrefix}". Extension JSON Schema keywords must start with "x-".`
1351
+ );
1352
+ }
1353
+ return {
1354
+ defs: {},
1355
+ extensionRegistry: options?.extensionRegistry,
1356
+ vendorPrefix
1357
+ };
1267
1358
  }
1268
- function generateJsonSchemaFromIR(ir) {
1269
- const ctx = makeContext();
1359
+ function generateJsonSchemaFromIR(ir, options) {
1360
+ const ctx = makeContext(options);
1270
1361
  for (const [name, typeDef] of Object.entries(ir.typeRegistry)) {
1271
1362
  ctx.defs[name] = generateTypeNode(typeDef.type, ctx);
1272
1363
  }
@@ -1318,16 +1409,16 @@ function generateFieldSchema(field, ctx) {
1318
1409
  directConstraints.push(c);
1319
1410
  }
1320
1411
  }
1321
- applyConstraints(schema, directConstraints);
1322
- applyAnnotations(schema, field.annotations);
1412
+ applyConstraints(schema, directConstraints, ctx);
1413
+ applyAnnotations(schema, field.annotations, ctx);
1323
1414
  if (pathConstraints.length === 0) {
1324
1415
  return schema;
1325
1416
  }
1326
- return applyPathTargetedConstraints(schema, pathConstraints);
1417
+ return applyPathTargetedConstraints(schema, pathConstraints, ctx);
1327
1418
  }
1328
- function applyPathTargetedConstraints(schema, pathConstraints) {
1419
+ function applyPathTargetedConstraints(schema, pathConstraints, ctx) {
1329
1420
  if (schema.type === "array" && schema.items) {
1330
- schema.items = applyPathTargetedConstraints(schema.items, pathConstraints);
1421
+ schema.items = applyPathTargetedConstraints(schema.items, pathConstraints, ctx);
1331
1422
  return schema;
1332
1423
  }
1333
1424
  const byTarget = /* @__PURE__ */ new Map();
@@ -1341,7 +1432,7 @@ function applyPathTargetedConstraints(schema, pathConstraints) {
1341
1432
  const propertyOverrides = {};
1342
1433
  for (const [target, constraints] of byTarget) {
1343
1434
  const subSchema = {};
1344
- applyConstraints(subSchema, constraints);
1435
+ applyConstraints(subSchema, constraints, ctx);
1345
1436
  propertyOverrides[target] = subSchema;
1346
1437
  }
1347
1438
  if (schema.$ref) {
@@ -1385,6 +1476,8 @@ function generateTypeNode(type, ctx) {
1385
1476
  return generateArrayType(type, ctx);
1386
1477
  case "object":
1387
1478
  return generateObjectType(type, ctx);
1479
+ case "record":
1480
+ return generateRecordType(type, ctx);
1388
1481
  case "union":
1389
1482
  return generateUnionType(type, ctx);
1390
1483
  case "reference":
@@ -1392,7 +1485,7 @@ function generateTypeNode(type, ctx) {
1392
1485
  case "dynamic":
1393
1486
  return generateDynamicType(type);
1394
1487
  case "custom":
1395
- return generateCustomType(type);
1488
+ return generateCustomType(type, ctx);
1396
1489
  default: {
1397
1490
  const _exhaustive = type;
1398
1491
  return _exhaustive;
@@ -1441,16 +1534,27 @@ function generateObjectType(type, ctx) {
1441
1534
  }
1442
1535
  return schema;
1443
1536
  }
1537
+ function generateRecordType(type, ctx) {
1538
+ return {
1539
+ type: "object",
1540
+ additionalProperties: generateTypeNode(type.valueType, ctx)
1541
+ };
1542
+ }
1444
1543
  function generatePropertySchema(prop, ctx) {
1445
1544
  const schema = generateTypeNode(prop.type, ctx);
1446
- applyConstraints(schema, prop.constraints);
1447
- applyAnnotations(schema, prop.annotations);
1545
+ applyConstraints(schema, prop.constraints, ctx);
1546
+ applyAnnotations(schema, prop.annotations, ctx);
1448
1547
  return schema;
1449
1548
  }
1450
1549
  function generateUnionType(type, ctx) {
1451
1550
  if (isBooleanUnion(type)) {
1452
1551
  return { type: "boolean" };
1453
1552
  }
1553
+ if (isNullableUnion(type)) {
1554
+ return {
1555
+ oneOf: type.members.map((m) => generateTypeNode(m, ctx))
1556
+ };
1557
+ }
1454
1558
  return {
1455
1559
  anyOf: type.members.map((m) => generateTypeNode(m, ctx))
1456
1560
  };
@@ -1460,6 +1564,13 @@ function isBooleanUnion(type) {
1460
1564
  const kinds = type.members.map((m) => m.kind);
1461
1565
  return kinds.every((k) => k === "primitive") && type.members.every((m) => m.kind === "primitive" && m.primitiveKind === "boolean");
1462
1566
  }
1567
+ function isNullableUnion(type) {
1568
+ if (type.members.length !== 2) return false;
1569
+ const nullCount = type.members.filter(
1570
+ (m) => m.kind === "primitive" && m.primitiveKind === "null"
1571
+ ).length;
1572
+ return nullCount === 1;
1573
+ }
1463
1574
  function generateReferenceType(type) {
1464
1575
  return { $ref: `#/$defs/${type.name}` };
1465
1576
  }
@@ -1480,10 +1591,7 @@ function generateDynamicType(type) {
1480
1591
  "x-formspec-schemaSource": type.sourceKey
1481
1592
  };
1482
1593
  }
1483
- function generateCustomType(_type) {
1484
- return { type: "object" };
1485
- }
1486
- function applyConstraints(schema, constraints) {
1594
+ function applyConstraints(schema, constraints, ctx) {
1487
1595
  for (const constraint of constraints) {
1488
1596
  switch (constraint.constraintKind) {
1489
1597
  case "minimum":
@@ -1528,6 +1636,7 @@ function applyConstraints(schema, constraints) {
1528
1636
  case "allowedMembers":
1529
1637
  break;
1530
1638
  case "custom":
1639
+ applyCustomConstraint(schema, constraint, ctx);
1531
1640
  break;
1532
1641
  default: {
1533
1642
  const _exhaustive = constraint;
@@ -1536,7 +1645,7 @@ function applyConstraints(schema, constraints) {
1536
1645
  }
1537
1646
  }
1538
1647
  }
1539
- function applyAnnotations(schema, annotations) {
1648
+ function applyAnnotations(schema, annotations, ctx) {
1540
1649
  for (const annotation of annotations) {
1541
1650
  switch (annotation.annotationKind) {
1542
1651
  case "displayName":
@@ -1556,6 +1665,7 @@ function applyAnnotations(schema, annotations) {
1556
1665
  case "formatHint":
1557
1666
  break;
1558
1667
  case "custom":
1668
+ applyCustomAnnotation(schema, annotation, ctx);
1559
1669
  break;
1560
1670
  default: {
1561
1671
  const _exhaustive = annotation;
@@ -1564,6 +1674,36 @@ function applyAnnotations(schema, annotations) {
1564
1674
  }
1565
1675
  }
1566
1676
  }
1677
+ function generateCustomType(type, ctx) {
1678
+ const registration = ctx.extensionRegistry?.findType(type.typeId);
1679
+ if (registration === void 0) {
1680
+ throw new Error(
1681
+ `Cannot generate JSON Schema for custom type "${type.typeId}" without a matching extension registration`
1682
+ );
1683
+ }
1684
+ return registration.toJsonSchema(type.payload, ctx.vendorPrefix);
1685
+ }
1686
+ function applyCustomConstraint(schema, constraint, ctx) {
1687
+ const registration = ctx.extensionRegistry?.findConstraint(constraint.constraintId);
1688
+ if (registration === void 0) {
1689
+ throw new Error(
1690
+ `Cannot generate JSON Schema for custom constraint "${constraint.constraintId}" without a matching extension registration`
1691
+ );
1692
+ }
1693
+ Object.assign(schema, registration.toJsonSchema(constraint.payload, ctx.vendorPrefix));
1694
+ }
1695
+ function applyCustomAnnotation(schema, annotation, ctx) {
1696
+ const registration = ctx.extensionRegistry?.findAnnotation(annotation.annotationId);
1697
+ if (registration === void 0) {
1698
+ throw new Error(
1699
+ `Cannot generate JSON Schema for custom annotation "${annotation.annotationId}" without a matching extension registration`
1700
+ );
1701
+ }
1702
+ if (registration.toJsonSchema === void 0) {
1703
+ return;
1704
+ }
1705
+ Object.assign(schema, registration.toJsonSchema(annotation.value, ctx.vendorPrefix));
1706
+ }
1567
1707
 
1568
1708
  // src/ui-schema/schema.ts
1569
1709
  var import_zod = require("zod");
@@ -1788,12 +1928,9 @@ function generateClassSchemas(analysis, source) {
1788
1928
  }
1789
1929
 
1790
1930
  // src/validate/constraint-validator.ts
1791
- function makeCode(ctx, category, number) {
1792
- return `${ctx.vendorPrefix}-${category}-${String(number).padStart(3, "0")}`;
1793
- }
1794
1931
  function addContradiction(ctx, message, primary, related) {
1795
1932
  ctx.diagnostics.push({
1796
- code: makeCode(ctx, "CONTRADICTION", 1),
1933
+ code: "CONTRADICTING_CONSTRAINTS",
1797
1934
  message,
1798
1935
  severity: "error",
1799
1936
  primaryLocation: primary,
@@ -1802,7 +1939,7 @@ function addContradiction(ctx, message, primary, related) {
1802
1939
  }
1803
1940
  function addTypeMismatch(ctx, message, primary) {
1804
1941
  ctx.diagnostics.push({
1805
- code: makeCode(ctx, "TYPE_MISMATCH", 1),
1942
+ code: "TYPE_MISMATCH",
1806
1943
  message,
1807
1944
  severity: "error",
1808
1945
  primaryLocation: primary,
@@ -1811,13 +1948,22 @@ function addTypeMismatch(ctx, message, primary) {
1811
1948
  }
1812
1949
  function addUnknownExtension(ctx, message, primary) {
1813
1950
  ctx.diagnostics.push({
1814
- code: makeCode(ctx, "UNKNOWN_EXTENSION", 1),
1951
+ code: "UNKNOWN_EXTENSION",
1815
1952
  message,
1816
1953
  severity: "warning",
1817
1954
  primaryLocation: primary,
1818
1955
  relatedLocations: []
1819
1956
  });
1820
1957
  }
1958
+ function addConstraintBroadening(ctx, message, primary, related) {
1959
+ ctx.diagnostics.push({
1960
+ code: "CONSTRAINT_BROADENING",
1961
+ message,
1962
+ severity: "error",
1963
+ primaryLocation: primary,
1964
+ relatedLocations: [related]
1965
+ });
1966
+ }
1821
1967
  function findNumeric(constraints, constraintKind) {
1822
1968
  return constraints.find((c) => c.constraintKind === constraintKind);
1823
1969
  }
@@ -1829,6 +1975,126 @@ function findAllowedMembers(constraints) {
1829
1975
  (c) => c.constraintKind === "allowedMembers"
1830
1976
  );
1831
1977
  }
1978
+ function isOrderedBoundConstraint(constraint) {
1979
+ return constraint.constraintKind === "minimum" || constraint.constraintKind === "exclusiveMinimum" || constraint.constraintKind === "minLength" || constraint.constraintKind === "minItems" || constraint.constraintKind === "maximum" || constraint.constraintKind === "exclusiveMaximum" || constraint.constraintKind === "maxLength" || constraint.constraintKind === "maxItems";
1980
+ }
1981
+ function pathKey(constraint) {
1982
+ return constraint.path?.segments.join(".") ?? "";
1983
+ }
1984
+ function orderedBoundFamily(kind) {
1985
+ switch (kind) {
1986
+ case "minimum":
1987
+ case "exclusiveMinimum":
1988
+ return "numeric-lower";
1989
+ case "maximum":
1990
+ case "exclusiveMaximum":
1991
+ return "numeric-upper";
1992
+ case "minLength":
1993
+ return "minLength";
1994
+ case "minItems":
1995
+ return "minItems";
1996
+ case "maxLength":
1997
+ return "maxLength";
1998
+ case "maxItems":
1999
+ return "maxItems";
2000
+ default: {
2001
+ const _exhaustive = kind;
2002
+ return _exhaustive;
2003
+ }
2004
+ }
2005
+ }
2006
+ function isNumericLowerKind(kind) {
2007
+ return kind === "minimum" || kind === "exclusiveMinimum";
2008
+ }
2009
+ function isNumericUpperKind(kind) {
2010
+ return kind === "maximum" || kind === "exclusiveMaximum";
2011
+ }
2012
+ function describeConstraintTag(constraint) {
2013
+ return `@${constraint.constraintKind}`;
2014
+ }
2015
+ function compareConstraintStrength(current, previous) {
2016
+ const family = orderedBoundFamily(current.constraintKind);
2017
+ if (family === "numeric-lower") {
2018
+ if (!isNumericLowerKind(current.constraintKind) || !isNumericLowerKind(previous.constraintKind)) {
2019
+ throw new Error("numeric-lower family received non-numeric lower-bound constraint");
2020
+ }
2021
+ if (current.value !== previous.value) {
2022
+ return current.value > previous.value ? 1 : -1;
2023
+ }
2024
+ if (current.constraintKind === "exclusiveMinimum" && previous.constraintKind === "minimum") {
2025
+ return 1;
2026
+ }
2027
+ if (current.constraintKind === "minimum" && previous.constraintKind === "exclusiveMinimum") {
2028
+ return -1;
2029
+ }
2030
+ return 0;
2031
+ }
2032
+ if (family === "numeric-upper") {
2033
+ if (!isNumericUpperKind(current.constraintKind) || !isNumericUpperKind(previous.constraintKind)) {
2034
+ throw new Error("numeric-upper family received non-numeric upper-bound constraint");
2035
+ }
2036
+ if (current.value !== previous.value) {
2037
+ return current.value < previous.value ? 1 : -1;
2038
+ }
2039
+ if (current.constraintKind === "exclusiveMaximum" && previous.constraintKind === "maximum") {
2040
+ return 1;
2041
+ }
2042
+ if (current.constraintKind === "maximum" && previous.constraintKind === "exclusiveMaximum") {
2043
+ return -1;
2044
+ }
2045
+ return 0;
2046
+ }
2047
+ switch (family) {
2048
+ case "minLength":
2049
+ case "minItems":
2050
+ if (current.value === previous.value) {
2051
+ return 0;
2052
+ }
2053
+ return current.value > previous.value ? 1 : -1;
2054
+ case "maxLength":
2055
+ case "maxItems":
2056
+ if (current.value === previous.value) {
2057
+ return 0;
2058
+ }
2059
+ return current.value < previous.value ? 1 : -1;
2060
+ default: {
2061
+ const _exhaustive = family;
2062
+ return _exhaustive;
2063
+ }
2064
+ }
2065
+ }
2066
+ function checkConstraintBroadening(ctx, fieldName, constraints) {
2067
+ const strongestByKey = /* @__PURE__ */ new Map();
2068
+ for (const constraint of constraints) {
2069
+ if (!isOrderedBoundConstraint(constraint)) {
2070
+ continue;
2071
+ }
2072
+ const key = `${orderedBoundFamily(constraint.constraintKind)}:${pathKey(constraint)}`;
2073
+ const previous = strongestByKey.get(key);
2074
+ if (previous === void 0) {
2075
+ strongestByKey.set(key, constraint);
2076
+ continue;
2077
+ }
2078
+ const strength = compareConstraintStrength(constraint, previous);
2079
+ if (strength < 0) {
2080
+ const displayFieldName = formatPathTargetFieldName(
2081
+ fieldName,
2082
+ constraint.path?.segments ?? []
2083
+ );
2084
+ addConstraintBroadening(
2085
+ ctx,
2086
+ `Field "${displayFieldName}": ${describeConstraintTag(constraint)} (${String(constraint.value)}) is broader than earlier ${describeConstraintTag(previous)} (${String(previous.value)}). Constraints can only narrow.`,
2087
+ constraint.provenance,
2088
+ previous.provenance
2089
+ );
2090
+ continue;
2091
+ }
2092
+ if (strength <= 0) {
2093
+ continue;
2094
+ }
2095
+ strongestByKey.set(key, constraint);
2096
+ }
2097
+ }
1832
2098
  function checkNumericContradictions(ctx, fieldName, constraints) {
1833
2099
  const min = findNumeric(constraints, "minimum");
1834
2100
  const max = findNumeric(constraints, "maximum");
@@ -1925,6 +2191,8 @@ function typeLabel(type) {
1925
2191
  return "array";
1926
2192
  case "object":
1927
2193
  return "object";
2194
+ case "record":
2195
+ return "record";
1928
2196
  case "union":
1929
2197
  return "union";
1930
2198
  case "reference":
@@ -1939,85 +2207,140 @@ function typeLabel(type) {
1939
2207
  }
1940
2208
  }
1941
2209
  }
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) {
2210
+ function dereferenceType(ctx, type) {
2211
+ let current = type;
2212
+ const seen = /* @__PURE__ */ new Set();
2213
+ while (current.kind === "reference") {
2214
+ if (seen.has(current.name)) {
2215
+ return current;
2216
+ }
2217
+ seen.add(current.name);
2218
+ const definition = ctx.typeRegistry[current.name];
2219
+ if (definition === void 0) {
2220
+ return current;
2221
+ }
2222
+ current = definition.type;
2223
+ }
2224
+ return current;
2225
+ }
2226
+ function resolvePathTargetType(ctx, type, segments) {
2227
+ const effectiveType = dereferenceType(ctx, type);
2228
+ if (segments.length === 0) {
2229
+ return { kind: "resolved", type: effectiveType };
2230
+ }
2231
+ if (effectiveType.kind === "array") {
2232
+ return resolvePathTargetType(ctx, effectiveType.items, segments);
2233
+ }
2234
+ if (effectiveType.kind === "object") {
2235
+ const [segment, ...rest] = segments;
2236
+ if (segment === void 0) {
2237
+ throw new Error("Invariant violation: object path traversal requires a segment");
2238
+ }
2239
+ const property = effectiveType.properties.find((prop) => prop.name === segment);
2240
+ if (property === void 0) {
2241
+ return { kind: "missing-property", segment };
2242
+ }
2243
+ return resolvePathTargetType(ctx, property.type, rest);
2244
+ }
2245
+ return { kind: "unresolvable", type: effectiveType };
2246
+ }
2247
+ function formatPathTargetFieldName(fieldName, path2) {
2248
+ return path2.length === 0 ? fieldName : `${fieldName}.${path2.join(".")}`;
2249
+ }
2250
+ function checkConstraintOnType(ctx, fieldName, type, constraint) {
2251
+ const effectiveType = dereferenceType(ctx, type);
2252
+ const isNumber = effectiveType.kind === "primitive" && effectiveType.primitiveKind === "number";
2253
+ const isString = effectiveType.kind === "primitive" && effectiveType.primitiveKind === "string";
2254
+ const isArray = effectiveType.kind === "array";
2255
+ const isEnum = effectiveType.kind === "enum";
2256
+ const label = typeLabel(effectiveType);
2257
+ const ck = constraint.constraintKind;
2258
+ switch (ck) {
2259
+ case "minimum":
2260
+ case "maximum":
2261
+ case "exclusiveMinimum":
2262
+ case "exclusiveMaximum":
2263
+ case "multipleOf": {
2264
+ if (!isNumber) {
1952
2265
  addTypeMismatch(
1953
2266
  ctx,
1954
- `Field "${fieldName}": path-targeted constraint "${constraint.constraintKind}" is invalid because type "${label}" cannot be traversed`,
2267
+ `Field "${fieldName}": constraint "${ck}" is only valid on number fields, but field type is "${label}"`,
1955
2268
  constraint.provenance
1956
2269
  );
1957
2270
  }
1958
- continue;
2271
+ break;
1959
2272
  }
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;
1975
- }
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;
2273
+ case "minLength":
2274
+ case "maxLength":
2275
+ case "pattern": {
2276
+ if (!isString) {
2277
+ addTypeMismatch(
2278
+ ctx,
2279
+ `Field "${fieldName}": constraint "${ck}" is only valid on string fields, but field type is "${label}"`,
2280
+ constraint.provenance
2281
+ );
1987
2282
  }
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
- }
1998
- break;
2283
+ break;
2284
+ }
2285
+ case "minItems":
2286
+ case "maxItems":
2287
+ case "uniqueItems": {
2288
+ if (!isArray) {
2289
+ addTypeMismatch(
2290
+ ctx,
2291
+ `Field "${fieldName}": constraint "${ck}" is only valid on array fields, but field type is "${label}"`,
2292
+ constraint.provenance
2293
+ );
1999
2294
  }
2000
- case "allowedMembers": {
2001
- if (!isEnum) {
2002
- addTypeMismatch(
2003
- ctx,
2004
- `Field "${fieldName}": constraint "allowedMembers" is only valid on enum fields, but field type is "${label}"`,
2005
- constraint.provenance
2006
- );
2007
- }
2008
- break;
2295
+ break;
2296
+ }
2297
+ case "allowedMembers": {
2298
+ if (!isEnum) {
2299
+ addTypeMismatch(
2300
+ ctx,
2301
+ `Field "${fieldName}": constraint "allowedMembers" is only valid on enum fields, but field type is "${label}"`,
2302
+ constraint.provenance
2303
+ );
2009
2304
  }
2010
- case "custom": {
2011
- checkCustomConstraint(ctx, fieldName, type, constraint);
2012
- break;
2305
+ break;
2306
+ }
2307
+ case "custom": {
2308
+ checkCustomConstraint(ctx, fieldName, effectiveType, constraint);
2309
+ break;
2310
+ }
2311
+ default: {
2312
+ const _exhaustive = constraint;
2313
+ throw new Error(
2314
+ `Unhandled constraint kind: ${_exhaustive.constraintKind}`
2315
+ );
2316
+ }
2317
+ }
2318
+ }
2319
+ function checkTypeApplicability(ctx, fieldName, type, constraints) {
2320
+ for (const constraint of constraints) {
2321
+ if (constraint.path) {
2322
+ const resolution = resolvePathTargetType(ctx, type, constraint.path.segments);
2323
+ const targetFieldName = formatPathTargetFieldName(fieldName, constraint.path.segments);
2324
+ if (resolution.kind === "missing-property") {
2325
+ addTypeMismatch(
2326
+ ctx,
2327
+ `Field "${fieldName}": path-targeted constraint "${constraint.constraintKind}" references unknown path segment "${resolution.segment}"`,
2328
+ constraint.provenance
2329
+ );
2330
+ continue;
2013
2331
  }
2014
- default: {
2015
- const _exhaustive = constraint;
2016
- throw new Error(
2017
- `Unhandled constraint kind: ${_exhaustive.constraintKind}`
2332
+ if (resolution.kind === "unresolvable") {
2333
+ addTypeMismatch(
2334
+ ctx,
2335
+ `Field "${targetFieldName}": path-targeted constraint "${constraint.constraintKind}" is invalid because type "${typeLabel(resolution.type)}" cannot be traversed`,
2336
+ constraint.provenance
2018
2337
  );
2338
+ continue;
2019
2339
  }
2340
+ checkConstraintOnType(ctx, targetFieldName, resolution.type, constraint);
2341
+ continue;
2020
2342
  }
2343
+ checkConstraintOnType(ctx, fieldName, type, constraint);
2021
2344
  }
2022
2345
  }
2023
2346
  function checkCustomConstraint(ctx, fieldName, type, constraint) {
@@ -2061,6 +2384,7 @@ function validateConstraints(ctx, name, type, constraints) {
2061
2384
  checkNumericContradictions(ctx, name, constraints);
2062
2385
  checkLengthContradictions(ctx, name, constraints);
2063
2386
  checkAllowedMembersContradiction(ctx, name, constraints);
2387
+ checkConstraintBroadening(ctx, name, constraints);
2064
2388
  checkTypeApplicability(ctx, name, type, constraints);
2065
2389
  }
2066
2390
  function validateElement(ctx, element) {
@@ -2087,8 +2411,8 @@ function validateElement(ctx, element) {
2087
2411
  function validateIR(ir, options) {
2088
2412
  const ctx = {
2089
2413
  diagnostics: [],
2090
- vendorPrefix: options?.vendorPrefix ?? "FORMSPEC",
2091
- extensionRegistry: options?.extensionRegistry
2414
+ extensionRegistry: options?.extensionRegistry,
2415
+ typeRegistry: ir.typeRegistry
2092
2416
  };
2093
2417
  for (const element of ir.elements) {
2094
2418
  validateElement(ctx, element);
@@ -2142,7 +2466,7 @@ function createExtensionRegistry(extensions) {
2142
2466
  }
2143
2467
 
2144
2468
  // src/generators/method-schema.ts
2145
- var import_core5 = require("@formspec/core");
2469
+ var import_core4 = require("@formspec/core");
2146
2470
  function typeToJsonSchema(type, checker) {
2147
2471
  const typeRegistry = {};
2148
2472
  const visiting = /* @__PURE__ */ new Set();
@@ -2150,7 +2474,7 @@ function typeToJsonSchema(type, checker) {
2150
2474
  const fieldProvenance = { surface: "tsdoc", file: "", line: 0, column: 0 };
2151
2475
  const ir = {
2152
2476
  kind: "form-ir",
2153
- irVersion: import_core5.IR_VERSION,
2477
+ irVersion: import_core4.IR_VERSION,
2154
2478
  elements: [
2155
2479
  {
2156
2480
  kind: "field",