@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.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
  }
@@ -313,11 +313,21 @@ function buildObjectProperties(elements, insideConditional = false) {
313
313
  import { IR_VERSION as IR_VERSION2 } from "@formspec/core";
314
314
 
315
315
  // src/json-schema/ir-generator.ts
316
- function makeContext() {
317
- return { defs: {} };
316
+ function makeContext(options) {
317
+ const vendorPrefix = options?.vendorPrefix ?? "x-formspec";
318
+ if (!vendorPrefix.startsWith("x-")) {
319
+ throw new Error(
320
+ `Invalid vendorPrefix "${vendorPrefix}". Extension JSON Schema keywords must start with "x-".`
321
+ );
322
+ }
323
+ return {
324
+ defs: {},
325
+ extensionRegistry: options?.extensionRegistry,
326
+ vendorPrefix
327
+ };
318
328
  }
319
- function generateJsonSchemaFromIR(ir) {
320
- const ctx = makeContext();
329
+ function generateJsonSchemaFromIR(ir, options) {
330
+ const ctx = makeContext(options);
321
331
  for (const [name, typeDef] of Object.entries(ir.typeRegistry)) {
322
332
  ctx.defs[name] = generateTypeNode(typeDef.type, ctx);
323
333
  }
@@ -360,8 +370,70 @@ function collectFields(elements, properties, required, ctx) {
360
370
  }
361
371
  function generateFieldSchema(field, ctx) {
362
372
  const schema = generateTypeNode(field.type, ctx);
363
- applyConstraints(schema, field.constraints);
364
- applyAnnotations(schema, field.annotations);
373
+ const directConstraints = [];
374
+ const pathConstraints = [];
375
+ for (const c of field.constraints) {
376
+ if (c.path) {
377
+ pathConstraints.push(c);
378
+ } else {
379
+ directConstraints.push(c);
380
+ }
381
+ }
382
+ applyConstraints(schema, directConstraints, ctx);
383
+ applyAnnotations(schema, field.annotations, ctx);
384
+ if (pathConstraints.length === 0) {
385
+ return schema;
386
+ }
387
+ return applyPathTargetedConstraints(schema, pathConstraints, ctx);
388
+ }
389
+ function applyPathTargetedConstraints(schema, pathConstraints, ctx) {
390
+ if (schema.type === "array" && schema.items) {
391
+ schema.items = applyPathTargetedConstraints(schema.items, pathConstraints, ctx);
392
+ return schema;
393
+ }
394
+ const byTarget = /* @__PURE__ */ new Map();
395
+ for (const c of pathConstraints) {
396
+ const target = c.path?.segments[0];
397
+ if (!target) continue;
398
+ const group = byTarget.get(target) ?? [];
399
+ group.push(c);
400
+ byTarget.set(target, group);
401
+ }
402
+ const propertyOverrides = {};
403
+ for (const [target, constraints] of byTarget) {
404
+ const subSchema = {};
405
+ applyConstraints(subSchema, constraints, ctx);
406
+ propertyOverrides[target] = subSchema;
407
+ }
408
+ if (schema.$ref) {
409
+ const { $ref, ...rest } = schema;
410
+ const refPart = { $ref };
411
+ const overridePart = {
412
+ properties: propertyOverrides,
413
+ ...rest
414
+ };
415
+ return { allOf: [refPart, overridePart] };
416
+ }
417
+ if (schema.type === "object" && schema.properties) {
418
+ const missingOverrides = {};
419
+ for (const [target, overrideSchema] of Object.entries(propertyOverrides)) {
420
+ if (schema.properties[target]) {
421
+ Object.assign(schema.properties[target], overrideSchema);
422
+ } else {
423
+ missingOverrides[target] = overrideSchema;
424
+ }
425
+ }
426
+ if (Object.keys(missingOverrides).length === 0) {
427
+ return schema;
428
+ }
429
+ return {
430
+ allOf: [schema, { properties: missingOverrides }]
431
+ };
432
+ }
433
+ if (schema.allOf) {
434
+ schema.allOf = [...schema.allOf, { properties: propertyOverrides }];
435
+ return schema;
436
+ }
365
437
  return schema;
366
438
  }
367
439
  function generateTypeNode(type, ctx) {
@@ -374,6 +446,8 @@ function generateTypeNode(type, ctx) {
374
446
  return generateArrayType(type, ctx);
375
447
  case "object":
376
448
  return generateObjectType(type, ctx);
449
+ case "record":
450
+ return generateRecordType(type, ctx);
377
451
  case "union":
378
452
  return generateUnionType(type, ctx);
379
453
  case "reference":
@@ -381,7 +455,7 @@ function generateTypeNode(type, ctx) {
381
455
  case "dynamic":
382
456
  return generateDynamicType(type);
383
457
  case "custom":
384
- return generateCustomType(type);
458
+ return generateCustomType(type, ctx);
385
459
  default: {
386
460
  const _exhaustive = type;
387
461
  return _exhaustive;
@@ -430,16 +504,27 @@ function generateObjectType(type, ctx) {
430
504
  }
431
505
  return schema;
432
506
  }
507
+ function generateRecordType(type, ctx) {
508
+ return {
509
+ type: "object",
510
+ additionalProperties: generateTypeNode(type.valueType, ctx)
511
+ };
512
+ }
433
513
  function generatePropertySchema(prop, ctx) {
434
514
  const schema = generateTypeNode(prop.type, ctx);
435
- applyConstraints(schema, prop.constraints);
436
- applyAnnotations(schema, prop.annotations);
515
+ applyConstraints(schema, prop.constraints, ctx);
516
+ applyAnnotations(schema, prop.annotations, ctx);
437
517
  return schema;
438
518
  }
439
519
  function generateUnionType(type, ctx) {
440
520
  if (isBooleanUnion(type)) {
441
521
  return { type: "boolean" };
442
522
  }
523
+ if (isNullableUnion(type)) {
524
+ return {
525
+ oneOf: type.members.map((m) => generateTypeNode(m, ctx))
526
+ };
527
+ }
443
528
  return {
444
529
  anyOf: type.members.map((m) => generateTypeNode(m, ctx))
445
530
  };
@@ -449,6 +534,13 @@ function isBooleanUnion(type) {
449
534
  const kinds = type.members.map((m) => m.kind);
450
535
  return kinds.every((k) => k === "primitive") && type.members.every((m) => m.kind === "primitive" && m.primitiveKind === "boolean");
451
536
  }
537
+ function isNullableUnion(type) {
538
+ if (type.members.length !== 2) return false;
539
+ const nullCount = type.members.filter(
540
+ (m) => m.kind === "primitive" && m.primitiveKind === "null"
541
+ ).length;
542
+ return nullCount === 1;
543
+ }
452
544
  function generateReferenceType(type) {
453
545
  return { $ref: `#/$defs/${type.name}` };
454
546
  }
@@ -469,10 +561,7 @@ function generateDynamicType(type) {
469
561
  "x-formspec-schemaSource": type.sourceKey
470
562
  };
471
563
  }
472
- function generateCustomType(_type) {
473
- return { type: "object" };
474
- }
475
- function applyConstraints(schema, constraints) {
564
+ function applyConstraints(schema, constraints, ctx) {
476
565
  for (const constraint of constraints) {
477
566
  switch (constraint.constraintKind) {
478
567
  case "minimum":
@@ -517,6 +606,7 @@ function applyConstraints(schema, constraints) {
517
606
  case "allowedMembers":
518
607
  break;
519
608
  case "custom":
609
+ applyCustomConstraint(schema, constraint, ctx);
520
610
  break;
521
611
  default: {
522
612
  const _exhaustive = constraint;
@@ -525,7 +615,7 @@ function applyConstraints(schema, constraints) {
525
615
  }
526
616
  }
527
617
  }
528
- function applyAnnotations(schema, annotations) {
618
+ function applyAnnotations(schema, annotations, ctx) {
529
619
  for (const annotation of annotations) {
530
620
  switch (annotation.annotationKind) {
531
621
  case "displayName":
@@ -545,6 +635,7 @@ function applyAnnotations(schema, annotations) {
545
635
  case "formatHint":
546
636
  break;
547
637
  case "custom":
638
+ applyCustomAnnotation(schema, annotation, ctx);
548
639
  break;
549
640
  default: {
550
641
  const _exhaustive = annotation;
@@ -553,11 +644,41 @@ function applyAnnotations(schema, annotations) {
553
644
  }
554
645
  }
555
646
  }
647
+ function generateCustomType(type, ctx) {
648
+ const registration = ctx.extensionRegistry?.findType(type.typeId);
649
+ if (registration === void 0) {
650
+ throw new Error(
651
+ `Cannot generate JSON Schema for custom type "${type.typeId}" without a matching extension registration`
652
+ );
653
+ }
654
+ return registration.toJsonSchema(type.payload, ctx.vendorPrefix);
655
+ }
656
+ function applyCustomConstraint(schema, constraint, ctx) {
657
+ const registration = ctx.extensionRegistry?.findConstraint(constraint.constraintId);
658
+ if (registration === void 0) {
659
+ throw new Error(
660
+ `Cannot generate JSON Schema for custom constraint "${constraint.constraintId}" without a matching extension registration`
661
+ );
662
+ }
663
+ Object.assign(schema, registration.toJsonSchema(constraint.payload, ctx.vendorPrefix));
664
+ }
665
+ function applyCustomAnnotation(schema, annotation, ctx) {
666
+ const registration = ctx.extensionRegistry?.findAnnotation(annotation.annotationId);
667
+ if (registration === void 0) {
668
+ throw new Error(
669
+ `Cannot generate JSON Schema for custom annotation "${annotation.annotationId}" without a matching extension registration`
670
+ );
671
+ }
672
+ if (registration.toJsonSchema === void 0) {
673
+ return;
674
+ }
675
+ Object.assign(schema, registration.toJsonSchema(annotation.value, ctx.vendorPrefix));
676
+ }
556
677
 
557
678
  // src/json-schema/generator.ts
558
- function generateJsonSchema(form) {
679
+ function generateJsonSchema(form, options) {
559
680
  const ir = canonicalizeChainDSL(form);
560
- return generateJsonSchemaFromIR(ir);
681
+ return generateJsonSchemaFromIR(ir, options);
561
682
  }
562
683
 
563
684
  // src/ui-schema/schema.ts
@@ -787,6 +908,48 @@ function getSchemaExtension(schema, key) {
787
908
  return schema[key];
788
909
  }
789
910
 
911
+ // src/extensions/registry.ts
912
+ function createExtensionRegistry(extensions) {
913
+ const typeMap = /* @__PURE__ */ new Map();
914
+ const constraintMap = /* @__PURE__ */ new Map();
915
+ const annotationMap = /* @__PURE__ */ new Map();
916
+ for (const ext of extensions) {
917
+ if (ext.types !== void 0) {
918
+ for (const type of ext.types) {
919
+ const qualifiedId = `${ext.extensionId}/${type.typeName}`;
920
+ if (typeMap.has(qualifiedId)) {
921
+ throw new Error(`Duplicate custom type ID: "${qualifiedId}"`);
922
+ }
923
+ typeMap.set(qualifiedId, type);
924
+ }
925
+ }
926
+ if (ext.constraints !== void 0) {
927
+ for (const constraint of ext.constraints) {
928
+ const qualifiedId = `${ext.extensionId}/${constraint.constraintName}`;
929
+ if (constraintMap.has(qualifiedId)) {
930
+ throw new Error(`Duplicate custom constraint ID: "${qualifiedId}"`);
931
+ }
932
+ constraintMap.set(qualifiedId, constraint);
933
+ }
934
+ }
935
+ if (ext.annotations !== void 0) {
936
+ for (const annotation of ext.annotations) {
937
+ const qualifiedId = `${ext.extensionId}/${annotation.annotationName}`;
938
+ if (annotationMap.has(qualifiedId)) {
939
+ throw new Error(`Duplicate custom annotation ID: "${qualifiedId}"`);
940
+ }
941
+ annotationMap.set(qualifiedId, annotation);
942
+ }
943
+ }
944
+ }
945
+ return {
946
+ extensions,
947
+ findType: (typeId) => typeMap.get(typeId),
948
+ findConstraint: (constraintId) => constraintMap.get(constraintId),
949
+ findAnnotation: (annotationId) => annotationMap.get(annotationId)
950
+ };
951
+ }
952
+
790
953
  // src/json-schema/schema.ts
791
954
  import { z as z3 } from "zod";
792
955
  var jsonSchemaTypeSchema = z3.enum([
@@ -850,12 +1013,9 @@ var jsonSchema7Schema = z3.lazy(
850
1013
  );
851
1014
 
852
1015
  // src/validate/constraint-validator.ts
853
- function makeCode(ctx, category, number) {
854
- return `${ctx.vendorPrefix}-${category}-${String(number).padStart(3, "0")}`;
855
- }
856
1016
  function addContradiction(ctx, message, primary, related) {
857
1017
  ctx.diagnostics.push({
858
- code: makeCode(ctx, "CONTRADICTION", 1),
1018
+ code: "CONTRADICTING_CONSTRAINTS",
859
1019
  message,
860
1020
  severity: "error",
861
1021
  primaryLocation: primary,
@@ -864,7 +1024,7 @@ function addContradiction(ctx, message, primary, related) {
864
1024
  }
865
1025
  function addTypeMismatch(ctx, message, primary) {
866
1026
  ctx.diagnostics.push({
867
- code: makeCode(ctx, "TYPE_MISMATCH", 1),
1027
+ code: "TYPE_MISMATCH",
868
1028
  message,
869
1029
  severity: "error",
870
1030
  primaryLocation: primary,
@@ -873,28 +1033,153 @@ function addTypeMismatch(ctx, message, primary) {
873
1033
  }
874
1034
  function addUnknownExtension(ctx, message, primary) {
875
1035
  ctx.diagnostics.push({
876
- code: makeCode(ctx, "UNKNOWN_EXTENSION", 1),
1036
+ code: "UNKNOWN_EXTENSION",
877
1037
  message,
878
1038
  severity: "warning",
879
1039
  primaryLocation: primary,
880
1040
  relatedLocations: []
881
1041
  });
882
1042
  }
1043
+ function addConstraintBroadening(ctx, message, primary, related) {
1044
+ ctx.diagnostics.push({
1045
+ code: "CONSTRAINT_BROADENING",
1046
+ message,
1047
+ severity: "error",
1048
+ primaryLocation: primary,
1049
+ relatedLocations: [related]
1050
+ });
1051
+ }
883
1052
  function findNumeric(constraints, constraintKind) {
884
- return constraints.find(
885
- (c) => c.constraintKind === constraintKind
886
- );
1053
+ return constraints.find((c) => c.constraintKind === constraintKind);
887
1054
  }
888
1055
  function findLength(constraints, constraintKind) {
889
- return constraints.find(
890
- (c) => c.constraintKind === constraintKind
891
- );
1056
+ return constraints.find((c) => c.constraintKind === constraintKind);
892
1057
  }
893
1058
  function findAllowedMembers(constraints) {
894
1059
  return constraints.filter(
895
1060
  (c) => c.constraintKind === "allowedMembers"
896
1061
  );
897
1062
  }
1063
+ function isOrderedBoundConstraint(constraint) {
1064
+ 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";
1065
+ }
1066
+ function pathKey(constraint) {
1067
+ return constraint.path?.segments.join(".") ?? "";
1068
+ }
1069
+ function orderedBoundFamily(kind) {
1070
+ switch (kind) {
1071
+ case "minimum":
1072
+ case "exclusiveMinimum":
1073
+ return "numeric-lower";
1074
+ case "maximum":
1075
+ case "exclusiveMaximum":
1076
+ return "numeric-upper";
1077
+ case "minLength":
1078
+ return "minLength";
1079
+ case "minItems":
1080
+ return "minItems";
1081
+ case "maxLength":
1082
+ return "maxLength";
1083
+ case "maxItems":
1084
+ return "maxItems";
1085
+ default: {
1086
+ const _exhaustive = kind;
1087
+ return _exhaustive;
1088
+ }
1089
+ }
1090
+ }
1091
+ function isNumericLowerKind(kind) {
1092
+ return kind === "minimum" || kind === "exclusiveMinimum";
1093
+ }
1094
+ function isNumericUpperKind(kind) {
1095
+ return kind === "maximum" || kind === "exclusiveMaximum";
1096
+ }
1097
+ function describeConstraintTag(constraint) {
1098
+ return `@${constraint.constraintKind}`;
1099
+ }
1100
+ function compareConstraintStrength(current, previous) {
1101
+ const family = orderedBoundFamily(current.constraintKind);
1102
+ if (family === "numeric-lower") {
1103
+ if (!isNumericLowerKind(current.constraintKind) || !isNumericLowerKind(previous.constraintKind)) {
1104
+ throw new Error("numeric-lower family received non-numeric lower-bound constraint");
1105
+ }
1106
+ if (current.value !== previous.value) {
1107
+ return current.value > previous.value ? 1 : -1;
1108
+ }
1109
+ if (current.constraintKind === "exclusiveMinimum" && previous.constraintKind === "minimum") {
1110
+ return 1;
1111
+ }
1112
+ if (current.constraintKind === "minimum" && previous.constraintKind === "exclusiveMinimum") {
1113
+ return -1;
1114
+ }
1115
+ return 0;
1116
+ }
1117
+ if (family === "numeric-upper") {
1118
+ if (!isNumericUpperKind(current.constraintKind) || !isNumericUpperKind(previous.constraintKind)) {
1119
+ throw new Error("numeric-upper family received non-numeric upper-bound constraint");
1120
+ }
1121
+ if (current.value !== previous.value) {
1122
+ return current.value < previous.value ? 1 : -1;
1123
+ }
1124
+ if (current.constraintKind === "exclusiveMaximum" && previous.constraintKind === "maximum") {
1125
+ return 1;
1126
+ }
1127
+ if (current.constraintKind === "maximum" && previous.constraintKind === "exclusiveMaximum") {
1128
+ return -1;
1129
+ }
1130
+ return 0;
1131
+ }
1132
+ switch (family) {
1133
+ case "minLength":
1134
+ case "minItems":
1135
+ if (current.value === previous.value) {
1136
+ return 0;
1137
+ }
1138
+ return current.value > previous.value ? 1 : -1;
1139
+ case "maxLength":
1140
+ case "maxItems":
1141
+ if (current.value === previous.value) {
1142
+ return 0;
1143
+ }
1144
+ return current.value < previous.value ? 1 : -1;
1145
+ default: {
1146
+ const _exhaustive = family;
1147
+ return _exhaustive;
1148
+ }
1149
+ }
1150
+ }
1151
+ function checkConstraintBroadening(ctx, fieldName, constraints) {
1152
+ const strongestByKey = /* @__PURE__ */ new Map();
1153
+ for (const constraint of constraints) {
1154
+ if (!isOrderedBoundConstraint(constraint)) {
1155
+ continue;
1156
+ }
1157
+ const key = `${orderedBoundFamily(constraint.constraintKind)}:${pathKey(constraint)}`;
1158
+ const previous = strongestByKey.get(key);
1159
+ if (previous === void 0) {
1160
+ strongestByKey.set(key, constraint);
1161
+ continue;
1162
+ }
1163
+ const strength = compareConstraintStrength(constraint, previous);
1164
+ if (strength < 0) {
1165
+ const displayFieldName = formatPathTargetFieldName(
1166
+ fieldName,
1167
+ constraint.path?.segments ?? []
1168
+ );
1169
+ addConstraintBroadening(
1170
+ ctx,
1171
+ `Field "${displayFieldName}": ${describeConstraintTag(constraint)} (${String(constraint.value)}) is broader than earlier ${describeConstraintTag(previous)} (${String(previous.value)}). Constraints can only narrow.`,
1172
+ constraint.provenance,
1173
+ previous.provenance
1174
+ );
1175
+ continue;
1176
+ }
1177
+ if (strength <= 0) {
1178
+ continue;
1179
+ }
1180
+ strongestByKey.set(key, constraint);
1181
+ }
1182
+ }
898
1183
  function checkNumericContradictions(ctx, fieldName, constraints) {
899
1184
  const min = findNumeric(constraints, "minimum");
900
1185
  const max = findNumeric(constraints, "maximum");
@@ -991,6 +1276,8 @@ function typeLabel(type) {
991
1276
  return "array";
992
1277
  case "object":
993
1278
  return "object";
1279
+ case "record":
1280
+ return "record";
994
1281
  case "union":
995
1282
  return "union";
996
1283
  case "reference":
@@ -1005,74 +1292,140 @@ function typeLabel(type) {
1005
1292
  }
1006
1293
  }
1007
1294
  }
1008
- function checkTypeApplicability(ctx, fieldName, type, constraints) {
1009
- const isNumber = type.kind === "primitive" && type.primitiveKind === "number";
1010
- const isString = type.kind === "primitive" && type.primitiveKind === "string";
1011
- const isArray = type.kind === "array";
1012
- const isEnum = type.kind === "enum";
1013
- const label = typeLabel(type);
1014
- for (const constraint of constraints) {
1015
- const ck = constraint.constraintKind;
1016
- switch (ck) {
1017
- case "minimum":
1018
- case "maximum":
1019
- case "exclusiveMinimum":
1020
- case "exclusiveMaximum":
1021
- case "multipleOf": {
1022
- if (!isNumber) {
1023
- addTypeMismatch(
1024
- ctx,
1025
- `Field "${fieldName}": constraint "${ck}" is only valid on number fields, but field type is "${label}"`,
1026
- constraint.provenance
1027
- );
1028
- }
1029
- break;
1295
+ function dereferenceType(ctx, type) {
1296
+ let current = type;
1297
+ const seen = /* @__PURE__ */ new Set();
1298
+ while (current.kind === "reference") {
1299
+ if (seen.has(current.name)) {
1300
+ return current;
1301
+ }
1302
+ seen.add(current.name);
1303
+ const definition = ctx.typeRegistry[current.name];
1304
+ if (definition === void 0) {
1305
+ return current;
1306
+ }
1307
+ current = definition.type;
1308
+ }
1309
+ return current;
1310
+ }
1311
+ function resolvePathTargetType(ctx, type, segments) {
1312
+ const effectiveType = dereferenceType(ctx, type);
1313
+ if (segments.length === 0) {
1314
+ return { kind: "resolved", type: effectiveType };
1315
+ }
1316
+ if (effectiveType.kind === "array") {
1317
+ return resolvePathTargetType(ctx, effectiveType.items, segments);
1318
+ }
1319
+ if (effectiveType.kind === "object") {
1320
+ const [segment, ...rest] = segments;
1321
+ if (segment === void 0) {
1322
+ throw new Error("Invariant violation: object path traversal requires a segment");
1323
+ }
1324
+ const property = effectiveType.properties.find((prop) => prop.name === segment);
1325
+ if (property === void 0) {
1326
+ return { kind: "missing-property", segment };
1327
+ }
1328
+ return resolvePathTargetType(ctx, property.type, rest);
1329
+ }
1330
+ return { kind: "unresolvable", type: effectiveType };
1331
+ }
1332
+ function formatPathTargetFieldName(fieldName, path) {
1333
+ return path.length === 0 ? fieldName : `${fieldName}.${path.join(".")}`;
1334
+ }
1335
+ function checkConstraintOnType(ctx, fieldName, type, constraint) {
1336
+ const effectiveType = dereferenceType(ctx, type);
1337
+ const isNumber = effectiveType.kind === "primitive" && effectiveType.primitiveKind === "number";
1338
+ const isString = effectiveType.kind === "primitive" && effectiveType.primitiveKind === "string";
1339
+ const isArray = effectiveType.kind === "array";
1340
+ const isEnum = effectiveType.kind === "enum";
1341
+ const label = typeLabel(effectiveType);
1342
+ const ck = constraint.constraintKind;
1343
+ switch (ck) {
1344
+ case "minimum":
1345
+ case "maximum":
1346
+ case "exclusiveMinimum":
1347
+ case "exclusiveMaximum":
1348
+ case "multipleOf": {
1349
+ if (!isNumber) {
1350
+ addTypeMismatch(
1351
+ ctx,
1352
+ `Field "${fieldName}": constraint "${ck}" is only valid on number fields, but field type is "${label}"`,
1353
+ constraint.provenance
1354
+ );
1030
1355
  }
1031
- case "minLength":
1032
- case "maxLength":
1033
- case "pattern": {
1034
- if (!isString) {
1035
- addTypeMismatch(
1036
- ctx,
1037
- `Field "${fieldName}": constraint "${ck}" is only valid on string fields, but field type is "${label}"`,
1038
- constraint.provenance
1039
- );
1040
- }
1041
- break;
1356
+ break;
1357
+ }
1358
+ case "minLength":
1359
+ case "maxLength":
1360
+ case "pattern": {
1361
+ if (!isString) {
1362
+ addTypeMismatch(
1363
+ ctx,
1364
+ `Field "${fieldName}": constraint "${ck}" is only valid on string fields, but field type is "${label}"`,
1365
+ constraint.provenance
1366
+ );
1042
1367
  }
1043
- case "minItems":
1044
- case "maxItems":
1045
- case "uniqueItems": {
1046
- if (!isArray) {
1047
- addTypeMismatch(
1048
- ctx,
1049
- `Field "${fieldName}": constraint "${ck}" is only valid on array fields, but field type is "${label}"`,
1050
- constraint.provenance
1051
- );
1052
- }
1053
- break;
1368
+ break;
1369
+ }
1370
+ case "minItems":
1371
+ case "maxItems":
1372
+ case "uniqueItems": {
1373
+ if (!isArray) {
1374
+ addTypeMismatch(
1375
+ ctx,
1376
+ `Field "${fieldName}": constraint "${ck}" is only valid on array fields, but field type is "${label}"`,
1377
+ constraint.provenance
1378
+ );
1054
1379
  }
1055
- case "allowedMembers": {
1056
- if (!isEnum) {
1057
- addTypeMismatch(
1058
- ctx,
1059
- `Field "${fieldName}": constraint "allowedMembers" is only valid on enum fields, but field type is "${label}"`,
1060
- constraint.provenance
1061
- );
1062
- }
1063
- break;
1380
+ break;
1381
+ }
1382
+ case "allowedMembers": {
1383
+ if (!isEnum) {
1384
+ addTypeMismatch(
1385
+ ctx,
1386
+ `Field "${fieldName}": constraint "allowedMembers" is only valid on enum fields, but field type is "${label}"`,
1387
+ constraint.provenance
1388
+ );
1064
1389
  }
1065
- case "custom": {
1066
- checkCustomConstraint(ctx, fieldName, type, constraint);
1067
- break;
1390
+ break;
1391
+ }
1392
+ case "custom": {
1393
+ checkCustomConstraint(ctx, fieldName, effectiveType, constraint);
1394
+ break;
1395
+ }
1396
+ default: {
1397
+ const _exhaustive = constraint;
1398
+ throw new Error(
1399
+ `Unhandled constraint kind: ${_exhaustive.constraintKind}`
1400
+ );
1401
+ }
1402
+ }
1403
+ }
1404
+ function checkTypeApplicability(ctx, fieldName, type, constraints) {
1405
+ for (const constraint of constraints) {
1406
+ if (constraint.path) {
1407
+ const resolution = resolvePathTargetType(ctx, type, constraint.path.segments);
1408
+ const targetFieldName = formatPathTargetFieldName(fieldName, constraint.path.segments);
1409
+ if (resolution.kind === "missing-property") {
1410
+ addTypeMismatch(
1411
+ ctx,
1412
+ `Field "${fieldName}": path-targeted constraint "${constraint.constraintKind}" references unknown path segment "${resolution.segment}"`,
1413
+ constraint.provenance
1414
+ );
1415
+ continue;
1068
1416
  }
1069
- default: {
1070
- const _exhaustive = constraint;
1071
- throw new Error(
1072
- `Unhandled constraint kind: ${_exhaustive.constraintKind}`
1417
+ if (resolution.kind === "unresolvable") {
1418
+ addTypeMismatch(
1419
+ ctx,
1420
+ `Field "${targetFieldName}": path-targeted constraint "${constraint.constraintKind}" is invalid because type "${typeLabel(resolution.type)}" cannot be traversed`,
1421
+ constraint.provenance
1073
1422
  );
1423
+ continue;
1074
1424
  }
1425
+ checkConstraintOnType(ctx, targetFieldName, resolution.type, constraint);
1426
+ continue;
1075
1427
  }
1428
+ checkConstraintOnType(ctx, fieldName, type, constraint);
1076
1429
  }
1077
1430
  }
1078
1431
  function checkCustomConstraint(ctx, fieldName, type, constraint) {
@@ -1116,6 +1469,7 @@ function validateConstraints(ctx, name, type, constraints) {
1116
1469
  checkNumericContradictions(ctx, name, constraints);
1117
1470
  checkLengthContradictions(ctx, name, constraints);
1118
1471
  checkAllowedMembersContradiction(ctx, name, constraints);
1472
+ checkConstraintBroadening(ctx, name, constraints);
1119
1473
  checkTypeApplicability(ctx, name, type, constraints);
1120
1474
  }
1121
1475
  function validateElement(ctx, element) {
@@ -1142,8 +1496,8 @@ function validateElement(ctx, element) {
1142
1496
  function validateIR(ir, options) {
1143
1497
  const ctx = {
1144
1498
  diagnostics: [],
1145
- vendorPrefix: options?.vendorPrefix ?? "FORMSPEC",
1146
- extensionRegistry: options?.extensionRegistry
1499
+ extensionRegistry: options?.extensionRegistry,
1500
+ typeRegistry: ir.typeRegistry
1147
1501
  };
1148
1502
  for (const element of ir.elements) {
1149
1503
  validateElement(ctx, element);
@@ -1155,9 +1509,9 @@ function validateIR(ir, options) {
1155
1509
  }
1156
1510
 
1157
1511
  // src/browser.ts
1158
- function buildFormSchemas(form) {
1512
+ function buildFormSchemas(form, options) {
1159
1513
  return {
1160
- jsonSchema: generateJsonSchema(form),
1514
+ jsonSchema: generateJsonSchema(form, options),
1161
1515
  uiSchema: generateUiSchema(form)
1162
1516
  };
1163
1517
  }
@@ -1167,7 +1521,9 @@ export {
1167
1521
  categorizationSchema,
1168
1522
  categorySchema,
1169
1523
  controlSchema,
1524
+ createExtensionRegistry,
1170
1525
  generateJsonSchema,
1526
+ generateJsonSchemaFromIR,
1171
1527
  generateUiSchema,
1172
1528
  getSchemaExtension,
1173
1529
  groupLayoutSchema,