@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/browser.cjs CHANGED
@@ -25,7 +25,9 @@ __export(browser_exports, {
25
25
  categorizationSchema: () => categorizationSchema,
26
26
  categorySchema: () => categorySchema,
27
27
  controlSchema: () => controlSchema,
28
+ createExtensionRegistry: () => createExtensionRegistry,
28
29
  generateJsonSchema: () => generateJsonSchema,
30
+ generateJsonSchemaFromIR: () => generateJsonSchemaFromIR,
29
31
  generateUiSchema: () => generateUiSchema,
30
32
  getSchemaExtension: () => getSchemaExtension,
31
33
  groupLayoutSchema: () => groupLayoutSchema,
@@ -225,7 +227,7 @@ function canonicalizeArrayField(field) {
225
227
  const itemsType = {
226
228
  kind: "object",
227
229
  properties: itemProperties,
228
- additionalProperties: false
230
+ additionalProperties: true
229
231
  };
230
232
  const type = { kind: "array", items: itemsType };
231
233
  const constraints = [];
@@ -260,7 +262,7 @@ function canonicalizeObjectField(field) {
260
262
  const type = {
261
263
  kind: "object",
262
264
  properties,
263
- additionalProperties: false
265
+ additionalProperties: true
264
266
  };
265
267
  return buildFieldNode(field.name, type, field.required, buildAnnotations(field.label));
266
268
  }
@@ -361,11 +363,21 @@ function buildObjectProperties(elements, insideConditional = false) {
361
363
  var import_core2 = require("@formspec/core");
362
364
 
363
365
  // src/json-schema/ir-generator.ts
364
- function makeContext() {
365
- return { defs: {} };
366
+ function makeContext(options) {
367
+ const vendorPrefix = options?.vendorPrefix ?? "x-formspec";
368
+ if (!vendorPrefix.startsWith("x-")) {
369
+ throw new Error(
370
+ `Invalid vendorPrefix "${vendorPrefix}". Extension JSON Schema keywords must start with "x-".`
371
+ );
372
+ }
373
+ return {
374
+ defs: {},
375
+ extensionRegistry: options?.extensionRegistry,
376
+ vendorPrefix
377
+ };
366
378
  }
367
- function generateJsonSchemaFromIR(ir) {
368
- const ctx = makeContext();
379
+ function generateJsonSchemaFromIR(ir, options) {
380
+ const ctx = makeContext(options);
369
381
  for (const [name, typeDef] of Object.entries(ir.typeRegistry)) {
370
382
  ctx.defs[name] = generateTypeNode(typeDef.type, ctx);
371
383
  }
@@ -408,8 +420,70 @@ function collectFields(elements, properties, required, ctx) {
408
420
  }
409
421
  function generateFieldSchema(field, ctx) {
410
422
  const schema = generateTypeNode(field.type, ctx);
411
- applyConstraints(schema, field.constraints);
412
- applyAnnotations(schema, field.annotations);
423
+ const directConstraints = [];
424
+ const pathConstraints = [];
425
+ for (const c of field.constraints) {
426
+ if (c.path) {
427
+ pathConstraints.push(c);
428
+ } else {
429
+ directConstraints.push(c);
430
+ }
431
+ }
432
+ applyConstraints(schema, directConstraints, ctx);
433
+ applyAnnotations(schema, field.annotations, ctx);
434
+ if (pathConstraints.length === 0) {
435
+ return schema;
436
+ }
437
+ return applyPathTargetedConstraints(schema, pathConstraints, ctx);
438
+ }
439
+ function applyPathTargetedConstraints(schema, pathConstraints, ctx) {
440
+ if (schema.type === "array" && schema.items) {
441
+ schema.items = applyPathTargetedConstraints(schema.items, pathConstraints, ctx);
442
+ return schema;
443
+ }
444
+ const byTarget = /* @__PURE__ */ new Map();
445
+ for (const c of pathConstraints) {
446
+ const target = c.path?.segments[0];
447
+ if (!target) continue;
448
+ const group = byTarget.get(target) ?? [];
449
+ group.push(c);
450
+ byTarget.set(target, group);
451
+ }
452
+ const propertyOverrides = {};
453
+ for (const [target, constraints] of byTarget) {
454
+ const subSchema = {};
455
+ applyConstraints(subSchema, constraints, ctx);
456
+ propertyOverrides[target] = subSchema;
457
+ }
458
+ if (schema.$ref) {
459
+ const { $ref, ...rest } = schema;
460
+ const refPart = { $ref };
461
+ const overridePart = {
462
+ properties: propertyOverrides,
463
+ ...rest
464
+ };
465
+ return { allOf: [refPart, overridePart] };
466
+ }
467
+ if (schema.type === "object" && schema.properties) {
468
+ const missingOverrides = {};
469
+ for (const [target, overrideSchema] of Object.entries(propertyOverrides)) {
470
+ if (schema.properties[target]) {
471
+ Object.assign(schema.properties[target], overrideSchema);
472
+ } else {
473
+ missingOverrides[target] = overrideSchema;
474
+ }
475
+ }
476
+ if (Object.keys(missingOverrides).length === 0) {
477
+ return schema;
478
+ }
479
+ return {
480
+ allOf: [schema, { properties: missingOverrides }]
481
+ };
482
+ }
483
+ if (schema.allOf) {
484
+ schema.allOf = [...schema.allOf, { properties: propertyOverrides }];
485
+ return schema;
486
+ }
413
487
  return schema;
414
488
  }
415
489
  function generateTypeNode(type, ctx) {
@@ -422,6 +496,8 @@ function generateTypeNode(type, ctx) {
422
496
  return generateArrayType(type, ctx);
423
497
  case "object":
424
498
  return generateObjectType(type, ctx);
499
+ case "record":
500
+ return generateRecordType(type, ctx);
425
501
  case "union":
426
502
  return generateUnionType(type, ctx);
427
503
  case "reference":
@@ -429,7 +505,7 @@ function generateTypeNode(type, ctx) {
429
505
  case "dynamic":
430
506
  return generateDynamicType(type);
431
507
  case "custom":
432
- return generateCustomType(type);
508
+ return generateCustomType(type, ctx);
433
509
  default: {
434
510
  const _exhaustive = type;
435
511
  return _exhaustive;
@@ -478,16 +554,27 @@ function generateObjectType(type, ctx) {
478
554
  }
479
555
  return schema;
480
556
  }
557
+ function generateRecordType(type, ctx) {
558
+ return {
559
+ type: "object",
560
+ additionalProperties: generateTypeNode(type.valueType, ctx)
561
+ };
562
+ }
481
563
  function generatePropertySchema(prop, ctx) {
482
564
  const schema = generateTypeNode(prop.type, ctx);
483
- applyConstraints(schema, prop.constraints);
484
- applyAnnotations(schema, prop.annotations);
565
+ applyConstraints(schema, prop.constraints, ctx);
566
+ applyAnnotations(schema, prop.annotations, ctx);
485
567
  return schema;
486
568
  }
487
569
  function generateUnionType(type, ctx) {
488
570
  if (isBooleanUnion(type)) {
489
571
  return { type: "boolean" };
490
572
  }
573
+ if (isNullableUnion(type)) {
574
+ return {
575
+ oneOf: type.members.map((m) => generateTypeNode(m, ctx))
576
+ };
577
+ }
491
578
  return {
492
579
  anyOf: type.members.map((m) => generateTypeNode(m, ctx))
493
580
  };
@@ -497,6 +584,13 @@ function isBooleanUnion(type) {
497
584
  const kinds = type.members.map((m) => m.kind);
498
585
  return kinds.every((k) => k === "primitive") && type.members.every((m) => m.kind === "primitive" && m.primitiveKind === "boolean");
499
586
  }
587
+ function isNullableUnion(type) {
588
+ if (type.members.length !== 2) return false;
589
+ const nullCount = type.members.filter(
590
+ (m) => m.kind === "primitive" && m.primitiveKind === "null"
591
+ ).length;
592
+ return nullCount === 1;
593
+ }
500
594
  function generateReferenceType(type) {
501
595
  return { $ref: `#/$defs/${type.name}` };
502
596
  }
@@ -517,10 +611,7 @@ function generateDynamicType(type) {
517
611
  "x-formspec-schemaSource": type.sourceKey
518
612
  };
519
613
  }
520
- function generateCustomType(_type) {
521
- return { type: "object" };
522
- }
523
- function applyConstraints(schema, constraints) {
614
+ function applyConstraints(schema, constraints, ctx) {
524
615
  for (const constraint of constraints) {
525
616
  switch (constraint.constraintKind) {
526
617
  case "minimum":
@@ -565,6 +656,7 @@ function applyConstraints(schema, constraints) {
565
656
  case "allowedMembers":
566
657
  break;
567
658
  case "custom":
659
+ applyCustomConstraint(schema, constraint, ctx);
568
660
  break;
569
661
  default: {
570
662
  const _exhaustive = constraint;
@@ -573,7 +665,7 @@ function applyConstraints(schema, constraints) {
573
665
  }
574
666
  }
575
667
  }
576
- function applyAnnotations(schema, annotations) {
668
+ function applyAnnotations(schema, annotations, ctx) {
577
669
  for (const annotation of annotations) {
578
670
  switch (annotation.annotationKind) {
579
671
  case "displayName":
@@ -593,6 +685,7 @@ function applyAnnotations(schema, annotations) {
593
685
  case "formatHint":
594
686
  break;
595
687
  case "custom":
688
+ applyCustomAnnotation(schema, annotation, ctx);
596
689
  break;
597
690
  default: {
598
691
  const _exhaustive = annotation;
@@ -601,11 +694,41 @@ function applyAnnotations(schema, annotations) {
601
694
  }
602
695
  }
603
696
  }
697
+ function generateCustomType(type, ctx) {
698
+ const registration = ctx.extensionRegistry?.findType(type.typeId);
699
+ if (registration === void 0) {
700
+ throw new Error(
701
+ `Cannot generate JSON Schema for custom type "${type.typeId}" without a matching extension registration`
702
+ );
703
+ }
704
+ return registration.toJsonSchema(type.payload, ctx.vendorPrefix);
705
+ }
706
+ function applyCustomConstraint(schema, constraint, ctx) {
707
+ const registration = ctx.extensionRegistry?.findConstraint(constraint.constraintId);
708
+ if (registration === void 0) {
709
+ throw new Error(
710
+ `Cannot generate JSON Schema for custom constraint "${constraint.constraintId}" without a matching extension registration`
711
+ );
712
+ }
713
+ Object.assign(schema, registration.toJsonSchema(constraint.payload, ctx.vendorPrefix));
714
+ }
715
+ function applyCustomAnnotation(schema, annotation, ctx) {
716
+ const registration = ctx.extensionRegistry?.findAnnotation(annotation.annotationId);
717
+ if (registration === void 0) {
718
+ throw new Error(
719
+ `Cannot generate JSON Schema for custom annotation "${annotation.annotationId}" without a matching extension registration`
720
+ );
721
+ }
722
+ if (registration.toJsonSchema === void 0) {
723
+ return;
724
+ }
725
+ Object.assign(schema, registration.toJsonSchema(annotation.value, ctx.vendorPrefix));
726
+ }
604
727
 
605
728
  // src/json-schema/generator.ts
606
- function generateJsonSchema(form) {
729
+ function generateJsonSchema(form, options) {
607
730
  const ir = canonicalizeChainDSL(form);
608
- return generateJsonSchemaFromIR(ir);
731
+ return generateJsonSchemaFromIR(ir, options);
609
732
  }
610
733
 
611
734
  // src/ui-schema/schema.ts
@@ -835,6 +958,48 @@ function getSchemaExtension(schema, key) {
835
958
  return schema[key];
836
959
  }
837
960
 
961
+ // src/extensions/registry.ts
962
+ function createExtensionRegistry(extensions) {
963
+ const typeMap = /* @__PURE__ */ new Map();
964
+ const constraintMap = /* @__PURE__ */ new Map();
965
+ const annotationMap = /* @__PURE__ */ new Map();
966
+ for (const ext of extensions) {
967
+ if (ext.types !== void 0) {
968
+ for (const type of ext.types) {
969
+ const qualifiedId = `${ext.extensionId}/${type.typeName}`;
970
+ if (typeMap.has(qualifiedId)) {
971
+ throw new Error(`Duplicate custom type ID: "${qualifiedId}"`);
972
+ }
973
+ typeMap.set(qualifiedId, type);
974
+ }
975
+ }
976
+ if (ext.constraints !== void 0) {
977
+ for (const constraint of ext.constraints) {
978
+ const qualifiedId = `${ext.extensionId}/${constraint.constraintName}`;
979
+ if (constraintMap.has(qualifiedId)) {
980
+ throw new Error(`Duplicate custom constraint ID: "${qualifiedId}"`);
981
+ }
982
+ constraintMap.set(qualifiedId, constraint);
983
+ }
984
+ }
985
+ if (ext.annotations !== void 0) {
986
+ for (const annotation of ext.annotations) {
987
+ const qualifiedId = `${ext.extensionId}/${annotation.annotationName}`;
988
+ if (annotationMap.has(qualifiedId)) {
989
+ throw new Error(`Duplicate custom annotation ID: "${qualifiedId}"`);
990
+ }
991
+ annotationMap.set(qualifiedId, annotation);
992
+ }
993
+ }
994
+ }
995
+ return {
996
+ extensions,
997
+ findType: (typeId) => typeMap.get(typeId),
998
+ findConstraint: (constraintId) => constraintMap.get(constraintId),
999
+ findAnnotation: (annotationId) => annotationMap.get(annotationId)
1000
+ };
1001
+ }
1002
+
838
1003
  // src/json-schema/schema.ts
839
1004
  var import_zod3 = require("zod");
840
1005
  var jsonSchemaTypeSchema = import_zod3.z.enum([
@@ -898,12 +1063,9 @@ var jsonSchema7Schema = import_zod3.z.lazy(
898
1063
  );
899
1064
 
900
1065
  // src/validate/constraint-validator.ts
901
- function makeCode(ctx, category, number) {
902
- return `${ctx.vendorPrefix}-${category}-${String(number).padStart(3, "0")}`;
903
- }
904
1066
  function addContradiction(ctx, message, primary, related) {
905
1067
  ctx.diagnostics.push({
906
- code: makeCode(ctx, "CONTRADICTION", 1),
1068
+ code: "CONTRADICTING_CONSTRAINTS",
907
1069
  message,
908
1070
  severity: "error",
909
1071
  primaryLocation: primary,
@@ -912,7 +1074,7 @@ function addContradiction(ctx, message, primary, related) {
912
1074
  }
913
1075
  function addTypeMismatch(ctx, message, primary) {
914
1076
  ctx.diagnostics.push({
915
- code: makeCode(ctx, "TYPE_MISMATCH", 1),
1077
+ code: "TYPE_MISMATCH",
916
1078
  message,
917
1079
  severity: "error",
918
1080
  primaryLocation: primary,
@@ -921,28 +1083,153 @@ function addTypeMismatch(ctx, message, primary) {
921
1083
  }
922
1084
  function addUnknownExtension(ctx, message, primary) {
923
1085
  ctx.diagnostics.push({
924
- code: makeCode(ctx, "UNKNOWN_EXTENSION", 1),
1086
+ code: "UNKNOWN_EXTENSION",
925
1087
  message,
926
1088
  severity: "warning",
927
1089
  primaryLocation: primary,
928
1090
  relatedLocations: []
929
1091
  });
930
1092
  }
1093
+ function addConstraintBroadening(ctx, message, primary, related) {
1094
+ ctx.diagnostics.push({
1095
+ code: "CONSTRAINT_BROADENING",
1096
+ message,
1097
+ severity: "error",
1098
+ primaryLocation: primary,
1099
+ relatedLocations: [related]
1100
+ });
1101
+ }
931
1102
  function findNumeric(constraints, constraintKind) {
932
- return constraints.find(
933
- (c) => c.constraintKind === constraintKind
934
- );
1103
+ return constraints.find((c) => c.constraintKind === constraintKind);
935
1104
  }
936
1105
  function findLength(constraints, constraintKind) {
937
- return constraints.find(
938
- (c) => c.constraintKind === constraintKind
939
- );
1106
+ return constraints.find((c) => c.constraintKind === constraintKind);
940
1107
  }
941
1108
  function findAllowedMembers(constraints) {
942
1109
  return constraints.filter(
943
1110
  (c) => c.constraintKind === "allowedMembers"
944
1111
  );
945
1112
  }
1113
+ function isOrderedBoundConstraint(constraint) {
1114
+ 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";
1115
+ }
1116
+ function pathKey(constraint) {
1117
+ return constraint.path?.segments.join(".") ?? "";
1118
+ }
1119
+ function orderedBoundFamily(kind) {
1120
+ switch (kind) {
1121
+ case "minimum":
1122
+ case "exclusiveMinimum":
1123
+ return "numeric-lower";
1124
+ case "maximum":
1125
+ case "exclusiveMaximum":
1126
+ return "numeric-upper";
1127
+ case "minLength":
1128
+ return "minLength";
1129
+ case "minItems":
1130
+ return "minItems";
1131
+ case "maxLength":
1132
+ return "maxLength";
1133
+ case "maxItems":
1134
+ return "maxItems";
1135
+ default: {
1136
+ const _exhaustive = kind;
1137
+ return _exhaustive;
1138
+ }
1139
+ }
1140
+ }
1141
+ function isNumericLowerKind(kind) {
1142
+ return kind === "minimum" || kind === "exclusiveMinimum";
1143
+ }
1144
+ function isNumericUpperKind(kind) {
1145
+ return kind === "maximum" || kind === "exclusiveMaximum";
1146
+ }
1147
+ function describeConstraintTag(constraint) {
1148
+ return `@${constraint.constraintKind}`;
1149
+ }
1150
+ function compareConstraintStrength(current, previous) {
1151
+ const family = orderedBoundFamily(current.constraintKind);
1152
+ if (family === "numeric-lower") {
1153
+ if (!isNumericLowerKind(current.constraintKind) || !isNumericLowerKind(previous.constraintKind)) {
1154
+ throw new Error("numeric-lower family received non-numeric lower-bound constraint");
1155
+ }
1156
+ if (current.value !== previous.value) {
1157
+ return current.value > previous.value ? 1 : -1;
1158
+ }
1159
+ if (current.constraintKind === "exclusiveMinimum" && previous.constraintKind === "minimum") {
1160
+ return 1;
1161
+ }
1162
+ if (current.constraintKind === "minimum" && previous.constraintKind === "exclusiveMinimum") {
1163
+ return -1;
1164
+ }
1165
+ return 0;
1166
+ }
1167
+ if (family === "numeric-upper") {
1168
+ if (!isNumericUpperKind(current.constraintKind) || !isNumericUpperKind(previous.constraintKind)) {
1169
+ throw new Error("numeric-upper family received non-numeric upper-bound constraint");
1170
+ }
1171
+ if (current.value !== previous.value) {
1172
+ return current.value < previous.value ? 1 : -1;
1173
+ }
1174
+ if (current.constraintKind === "exclusiveMaximum" && previous.constraintKind === "maximum") {
1175
+ return 1;
1176
+ }
1177
+ if (current.constraintKind === "maximum" && previous.constraintKind === "exclusiveMaximum") {
1178
+ return -1;
1179
+ }
1180
+ return 0;
1181
+ }
1182
+ switch (family) {
1183
+ case "minLength":
1184
+ case "minItems":
1185
+ if (current.value === previous.value) {
1186
+ return 0;
1187
+ }
1188
+ return current.value > previous.value ? 1 : -1;
1189
+ case "maxLength":
1190
+ case "maxItems":
1191
+ if (current.value === previous.value) {
1192
+ return 0;
1193
+ }
1194
+ return current.value < previous.value ? 1 : -1;
1195
+ default: {
1196
+ const _exhaustive = family;
1197
+ return _exhaustive;
1198
+ }
1199
+ }
1200
+ }
1201
+ function checkConstraintBroadening(ctx, fieldName, constraints) {
1202
+ const strongestByKey = /* @__PURE__ */ new Map();
1203
+ for (const constraint of constraints) {
1204
+ if (!isOrderedBoundConstraint(constraint)) {
1205
+ continue;
1206
+ }
1207
+ const key = `${orderedBoundFamily(constraint.constraintKind)}:${pathKey(constraint)}`;
1208
+ const previous = strongestByKey.get(key);
1209
+ if (previous === void 0) {
1210
+ strongestByKey.set(key, constraint);
1211
+ continue;
1212
+ }
1213
+ const strength = compareConstraintStrength(constraint, previous);
1214
+ if (strength < 0) {
1215
+ const displayFieldName = formatPathTargetFieldName(
1216
+ fieldName,
1217
+ constraint.path?.segments ?? []
1218
+ );
1219
+ addConstraintBroadening(
1220
+ ctx,
1221
+ `Field "${displayFieldName}": ${describeConstraintTag(constraint)} (${String(constraint.value)}) is broader than earlier ${describeConstraintTag(previous)} (${String(previous.value)}). Constraints can only narrow.`,
1222
+ constraint.provenance,
1223
+ previous.provenance
1224
+ );
1225
+ continue;
1226
+ }
1227
+ if (strength <= 0) {
1228
+ continue;
1229
+ }
1230
+ strongestByKey.set(key, constraint);
1231
+ }
1232
+ }
946
1233
  function checkNumericContradictions(ctx, fieldName, constraints) {
947
1234
  const min = findNumeric(constraints, "minimum");
948
1235
  const max = findNumeric(constraints, "maximum");
@@ -1039,6 +1326,8 @@ function typeLabel(type) {
1039
1326
  return "array";
1040
1327
  case "object":
1041
1328
  return "object";
1329
+ case "record":
1330
+ return "record";
1042
1331
  case "union":
1043
1332
  return "union";
1044
1333
  case "reference":
@@ -1053,74 +1342,140 @@ function typeLabel(type) {
1053
1342
  }
1054
1343
  }
1055
1344
  }
1056
- function checkTypeApplicability(ctx, fieldName, type, constraints) {
1057
- const isNumber = type.kind === "primitive" && type.primitiveKind === "number";
1058
- const isString = type.kind === "primitive" && type.primitiveKind === "string";
1059
- const isArray = type.kind === "array";
1060
- const isEnum = type.kind === "enum";
1061
- const label = typeLabel(type);
1062
- for (const constraint of constraints) {
1063
- const ck = constraint.constraintKind;
1064
- switch (ck) {
1065
- case "minimum":
1066
- case "maximum":
1067
- case "exclusiveMinimum":
1068
- case "exclusiveMaximum":
1069
- case "multipleOf": {
1070
- if (!isNumber) {
1071
- addTypeMismatch(
1072
- ctx,
1073
- `Field "${fieldName}": constraint "${ck}" is only valid on number fields, but field type is "${label}"`,
1074
- constraint.provenance
1075
- );
1076
- }
1077
- break;
1345
+ function dereferenceType(ctx, type) {
1346
+ let current = type;
1347
+ const seen = /* @__PURE__ */ new Set();
1348
+ while (current.kind === "reference") {
1349
+ if (seen.has(current.name)) {
1350
+ return current;
1351
+ }
1352
+ seen.add(current.name);
1353
+ const definition = ctx.typeRegistry[current.name];
1354
+ if (definition === void 0) {
1355
+ return current;
1356
+ }
1357
+ current = definition.type;
1358
+ }
1359
+ return current;
1360
+ }
1361
+ function resolvePathTargetType(ctx, type, segments) {
1362
+ const effectiveType = dereferenceType(ctx, type);
1363
+ if (segments.length === 0) {
1364
+ return { kind: "resolved", type: effectiveType };
1365
+ }
1366
+ if (effectiveType.kind === "array") {
1367
+ return resolvePathTargetType(ctx, effectiveType.items, segments);
1368
+ }
1369
+ if (effectiveType.kind === "object") {
1370
+ const [segment, ...rest] = segments;
1371
+ if (segment === void 0) {
1372
+ throw new Error("Invariant violation: object path traversal requires a segment");
1373
+ }
1374
+ const property = effectiveType.properties.find((prop) => prop.name === segment);
1375
+ if (property === void 0) {
1376
+ return { kind: "missing-property", segment };
1377
+ }
1378
+ return resolvePathTargetType(ctx, property.type, rest);
1379
+ }
1380
+ return { kind: "unresolvable", type: effectiveType };
1381
+ }
1382
+ function formatPathTargetFieldName(fieldName, path) {
1383
+ return path.length === 0 ? fieldName : `${fieldName}.${path.join(".")}`;
1384
+ }
1385
+ function checkConstraintOnType(ctx, fieldName, type, constraint) {
1386
+ const effectiveType = dereferenceType(ctx, type);
1387
+ const isNumber = effectiveType.kind === "primitive" && effectiveType.primitiveKind === "number";
1388
+ const isString = effectiveType.kind === "primitive" && effectiveType.primitiveKind === "string";
1389
+ const isArray = effectiveType.kind === "array";
1390
+ const isEnum = effectiveType.kind === "enum";
1391
+ const label = typeLabel(effectiveType);
1392
+ const ck = constraint.constraintKind;
1393
+ switch (ck) {
1394
+ case "minimum":
1395
+ case "maximum":
1396
+ case "exclusiveMinimum":
1397
+ case "exclusiveMaximum":
1398
+ case "multipleOf": {
1399
+ if (!isNumber) {
1400
+ addTypeMismatch(
1401
+ ctx,
1402
+ `Field "${fieldName}": constraint "${ck}" is only valid on number fields, but field type is "${label}"`,
1403
+ constraint.provenance
1404
+ );
1078
1405
  }
1079
- case "minLength":
1080
- case "maxLength":
1081
- case "pattern": {
1082
- if (!isString) {
1083
- addTypeMismatch(
1084
- ctx,
1085
- `Field "${fieldName}": constraint "${ck}" is only valid on string fields, but field type is "${label}"`,
1086
- constraint.provenance
1087
- );
1088
- }
1089
- break;
1406
+ break;
1407
+ }
1408
+ case "minLength":
1409
+ case "maxLength":
1410
+ case "pattern": {
1411
+ if (!isString) {
1412
+ addTypeMismatch(
1413
+ ctx,
1414
+ `Field "${fieldName}": constraint "${ck}" is only valid on string fields, but field type is "${label}"`,
1415
+ constraint.provenance
1416
+ );
1090
1417
  }
1091
- case "minItems":
1092
- case "maxItems":
1093
- case "uniqueItems": {
1094
- if (!isArray) {
1095
- addTypeMismatch(
1096
- ctx,
1097
- `Field "${fieldName}": constraint "${ck}" is only valid on array fields, but field type is "${label}"`,
1098
- constraint.provenance
1099
- );
1100
- }
1101
- break;
1418
+ break;
1419
+ }
1420
+ case "minItems":
1421
+ case "maxItems":
1422
+ case "uniqueItems": {
1423
+ if (!isArray) {
1424
+ addTypeMismatch(
1425
+ ctx,
1426
+ `Field "${fieldName}": constraint "${ck}" is only valid on array fields, but field type is "${label}"`,
1427
+ constraint.provenance
1428
+ );
1102
1429
  }
1103
- case "allowedMembers": {
1104
- if (!isEnum) {
1105
- addTypeMismatch(
1106
- ctx,
1107
- `Field "${fieldName}": constraint "allowedMembers" is only valid on enum fields, but field type is "${label}"`,
1108
- constraint.provenance
1109
- );
1110
- }
1111
- break;
1430
+ break;
1431
+ }
1432
+ case "allowedMembers": {
1433
+ if (!isEnum) {
1434
+ addTypeMismatch(
1435
+ ctx,
1436
+ `Field "${fieldName}": constraint "allowedMembers" is only valid on enum fields, but field type is "${label}"`,
1437
+ constraint.provenance
1438
+ );
1112
1439
  }
1113
- case "custom": {
1114
- checkCustomConstraint(ctx, fieldName, type, constraint);
1115
- break;
1440
+ break;
1441
+ }
1442
+ case "custom": {
1443
+ checkCustomConstraint(ctx, fieldName, effectiveType, constraint);
1444
+ break;
1445
+ }
1446
+ default: {
1447
+ const _exhaustive = constraint;
1448
+ throw new Error(
1449
+ `Unhandled constraint kind: ${_exhaustive.constraintKind}`
1450
+ );
1451
+ }
1452
+ }
1453
+ }
1454
+ function checkTypeApplicability(ctx, fieldName, type, constraints) {
1455
+ for (const constraint of constraints) {
1456
+ if (constraint.path) {
1457
+ const resolution = resolvePathTargetType(ctx, type, constraint.path.segments);
1458
+ const targetFieldName = formatPathTargetFieldName(fieldName, constraint.path.segments);
1459
+ if (resolution.kind === "missing-property") {
1460
+ addTypeMismatch(
1461
+ ctx,
1462
+ `Field "${fieldName}": path-targeted constraint "${constraint.constraintKind}" references unknown path segment "${resolution.segment}"`,
1463
+ constraint.provenance
1464
+ );
1465
+ continue;
1116
1466
  }
1117
- default: {
1118
- const _exhaustive = constraint;
1119
- throw new Error(
1120
- `Unhandled constraint kind: ${_exhaustive.constraintKind}`
1467
+ if (resolution.kind === "unresolvable") {
1468
+ addTypeMismatch(
1469
+ ctx,
1470
+ `Field "${targetFieldName}": path-targeted constraint "${constraint.constraintKind}" is invalid because type "${typeLabel(resolution.type)}" cannot be traversed`,
1471
+ constraint.provenance
1121
1472
  );
1473
+ continue;
1122
1474
  }
1475
+ checkConstraintOnType(ctx, targetFieldName, resolution.type, constraint);
1476
+ continue;
1123
1477
  }
1478
+ checkConstraintOnType(ctx, fieldName, type, constraint);
1124
1479
  }
1125
1480
  }
1126
1481
  function checkCustomConstraint(ctx, fieldName, type, constraint) {
@@ -1164,6 +1519,7 @@ function validateConstraints(ctx, name, type, constraints) {
1164
1519
  checkNumericContradictions(ctx, name, constraints);
1165
1520
  checkLengthContradictions(ctx, name, constraints);
1166
1521
  checkAllowedMembersContradiction(ctx, name, constraints);
1522
+ checkConstraintBroadening(ctx, name, constraints);
1167
1523
  checkTypeApplicability(ctx, name, type, constraints);
1168
1524
  }
1169
1525
  function validateElement(ctx, element) {
@@ -1190,8 +1546,8 @@ function validateElement(ctx, element) {
1190
1546
  function validateIR(ir, options) {
1191
1547
  const ctx = {
1192
1548
  diagnostics: [],
1193
- vendorPrefix: options?.vendorPrefix ?? "FORMSPEC",
1194
- extensionRegistry: options?.extensionRegistry
1549
+ extensionRegistry: options?.extensionRegistry,
1550
+ typeRegistry: ir.typeRegistry
1195
1551
  };
1196
1552
  for (const element of ir.elements) {
1197
1553
  validateElement(ctx, element);
@@ -1203,9 +1559,9 @@ function validateIR(ir, options) {
1203
1559
  }
1204
1560
 
1205
1561
  // src/browser.ts
1206
- function buildFormSchemas(form) {
1562
+ function buildFormSchemas(form, options) {
1207
1563
  return {
1208
- jsonSchema: generateJsonSchema(form),
1564
+ jsonSchema: generateJsonSchema(form, options),
1209
1565
  uiSchema: generateUiSchema(form)
1210
1566
  };
1211
1567
  }
@@ -1216,7 +1572,9 @@ function buildFormSchemas(form) {
1216
1572
  categorizationSchema,
1217
1573
  categorySchema,
1218
1574
  controlSchema,
1575
+ createExtensionRegistry,
1219
1576
  generateJsonSchema,
1577
+ generateJsonSchemaFromIR,
1220
1578
  generateUiSchema,
1221
1579
  getSchemaExtension,
1222
1580
  groupLayoutSchema,