@formspec/build 0.1.0-alpha.14 → 0.1.0-alpha.16

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 (68) 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 +22 -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__/fixtures/mixed-authoring-shipping-address.d.ts +30 -0
  9. package/dist/__tests__/fixtures/mixed-authoring-shipping-address.d.ts.map +1 -0
  10. package/dist/__tests__/mixed-authoring.test.d.ts +2 -0
  11. package/dist/__tests__/mixed-authoring.test.d.ts.map +1 -0
  12. package/dist/__tests__/parity/fixtures/plan-status/chain-dsl.d.ts +19 -0
  13. package/dist/__tests__/parity/fixtures/plan-status/chain-dsl.d.ts.map +1 -0
  14. package/dist/__tests__/parity/fixtures/plan-status/expected-ir.d.ts +6 -0
  15. package/dist/__tests__/parity/fixtures/plan-status/expected-ir.d.ts.map +1 -0
  16. package/dist/__tests__/parity/fixtures/plan-status/tsdoc.d.ts +17 -0
  17. package/dist/__tests__/parity/fixtures/plan-status/tsdoc.d.ts.map +1 -0
  18. package/dist/__tests__/parity/fixtures/usd-cents/chain-dsl.d.ts +9 -0
  19. package/dist/__tests__/parity/fixtures/usd-cents/chain-dsl.d.ts.map +1 -0
  20. package/dist/__tests__/parity/fixtures/usd-cents/expected-ir.d.ts +6 -0
  21. package/dist/__tests__/parity/fixtures/usd-cents/expected-ir.d.ts.map +1 -0
  22. package/dist/__tests__/parity/fixtures/usd-cents/tsdoc.d.ts +19 -0
  23. package/dist/__tests__/parity/fixtures/usd-cents/tsdoc.d.ts.map +1 -0
  24. package/dist/__tests__/parity/utils.d.ts +11 -4
  25. package/dist/__tests__/parity/utils.d.ts.map +1 -1
  26. package/dist/analyzer/class-analyzer.d.ts +5 -3
  27. package/dist/analyzer/class-analyzer.d.ts.map +1 -1
  28. package/dist/analyzer/jsdoc-constraints.d.ts +7 -51
  29. package/dist/analyzer/jsdoc-constraints.d.ts.map +1 -1
  30. package/dist/analyzer/tsdoc-parser.d.ts +25 -9
  31. package/dist/analyzer/tsdoc-parser.d.ts.map +1 -1
  32. package/dist/browser.cjs +546 -102
  33. package/dist/browser.cjs.map +1 -1
  34. package/dist/browser.d.ts +15 -2
  35. package/dist/browser.d.ts.map +1 -1
  36. package/dist/browser.js +544 -102
  37. package/dist/browser.js.map +1 -1
  38. package/dist/build.d.ts +170 -6
  39. package/dist/canonicalize/tsdoc-canonicalizer.d.ts +3 -3
  40. package/dist/canonicalize/tsdoc-canonicalizer.d.ts.map +1 -1
  41. package/dist/cli.cjs +877 -128
  42. package/dist/cli.cjs.map +1 -1
  43. package/dist/cli.js +876 -131
  44. package/dist/cli.js.map +1 -1
  45. package/dist/generators/mixed-authoring.d.ts +45 -0
  46. package/dist/generators/mixed-authoring.d.ts.map +1 -0
  47. package/dist/index.cjs +850 -125
  48. package/dist/index.cjs.map +1 -1
  49. package/dist/index.d.ts +22 -3
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +847 -129
  52. package/dist/index.js.map +1 -1
  53. package/dist/internals.cjs +946 -187
  54. package/dist/internals.cjs.map +1 -1
  55. package/dist/internals.js +944 -189
  56. package/dist/internals.js.map +1 -1
  57. package/dist/json-schema/generator.d.ts +8 -2
  58. package/dist/json-schema/generator.d.ts.map +1 -1
  59. package/dist/json-schema/ir-generator.d.ts +27 -4
  60. package/dist/json-schema/ir-generator.d.ts.map +1 -1
  61. package/dist/json-schema/types.d.ts +1 -1
  62. package/dist/json-schema/types.d.ts.map +1 -1
  63. package/dist/ui-schema/ir-generator.d.ts.map +1 -1
  64. package/dist/validate/constraint-validator.d.ts +3 -7
  65. package/dist/validate/constraint-validator.d.ts.map +1 -1
  66. package/package.json +3 -3
  67. package/dist/__tests__/jsdoc-constraints.test.d.ts +0 -10
  68. package/dist/__tests__/jsdoc-constraints.test.d.ts.map +0 -1
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,13 +363,26 @@ 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);
383
+ if (typeDef.annotations && typeDef.annotations.length > 0) {
384
+ applyAnnotations(ctx.defs[name], typeDef.annotations, ctx);
385
+ }
371
386
  }
372
387
  const properties = {};
373
388
  const required = [];
@@ -379,6 +394,9 @@ function generateJsonSchemaFromIR(ir) {
379
394
  properties,
380
395
  ...uniqueRequired.length > 0 && { required: uniqueRequired }
381
396
  };
397
+ if (ir.annotations && ir.annotations.length > 0) {
398
+ applyAnnotations(result, ir.annotations, ctx);
399
+ }
382
400
  if (Object.keys(ctx.defs).length > 0) {
383
401
  result.$defs = ctx.defs;
384
402
  }
@@ -408,25 +426,54 @@ function collectFields(elements, properties, required, ctx) {
408
426
  }
409
427
  function generateFieldSchema(field, ctx) {
410
428
  const schema = generateTypeNode(field.type, ctx);
429
+ const itemStringSchema = schema.type === "array" && schema.items?.type === "string" ? schema.items : void 0;
411
430
  const directConstraints = [];
431
+ const itemConstraints = [];
412
432
  const pathConstraints = [];
413
433
  for (const c of field.constraints) {
414
434
  if (c.path) {
415
435
  pathConstraints.push(c);
436
+ } else if (itemStringSchema !== void 0 && isStringItemConstraint(c)) {
437
+ itemConstraints.push(c);
416
438
  } else {
417
439
  directConstraints.push(c);
418
440
  }
419
441
  }
420
- applyConstraints(schema, directConstraints);
421
- applyAnnotations(schema, field.annotations);
442
+ applyConstraints(schema, directConstraints, ctx);
443
+ if (itemStringSchema !== void 0) {
444
+ applyConstraints(itemStringSchema, itemConstraints, ctx);
445
+ }
446
+ const rootAnnotations = [];
447
+ const itemAnnotations = [];
448
+ for (const annotation of field.annotations) {
449
+ if (itemStringSchema !== void 0 && annotation.annotationKind === "format") {
450
+ itemAnnotations.push(annotation);
451
+ } else {
452
+ rootAnnotations.push(annotation);
453
+ }
454
+ }
455
+ applyAnnotations(schema, rootAnnotations, ctx);
456
+ if (itemStringSchema !== void 0) {
457
+ applyAnnotations(itemStringSchema, itemAnnotations, ctx);
458
+ }
422
459
  if (pathConstraints.length === 0) {
423
460
  return schema;
424
461
  }
425
- return applyPathTargetedConstraints(schema, pathConstraints);
462
+ return applyPathTargetedConstraints(schema, pathConstraints, ctx);
463
+ }
464
+ function isStringItemConstraint(constraint) {
465
+ switch (constraint.constraintKind) {
466
+ case "minLength":
467
+ case "maxLength":
468
+ case "pattern":
469
+ return true;
470
+ default:
471
+ return false;
472
+ }
426
473
  }
427
- function applyPathTargetedConstraints(schema, pathConstraints) {
474
+ function applyPathTargetedConstraints(schema, pathConstraints, ctx) {
428
475
  if (schema.type === "array" && schema.items) {
429
- schema.items = applyPathTargetedConstraints(schema.items, pathConstraints);
476
+ schema.items = applyPathTargetedConstraints(schema.items, pathConstraints, ctx);
430
477
  return schema;
431
478
  }
432
479
  const byTarget = /* @__PURE__ */ new Map();
@@ -440,7 +487,7 @@ function applyPathTargetedConstraints(schema, pathConstraints) {
440
487
  const propertyOverrides = {};
441
488
  for (const [target, constraints] of byTarget) {
442
489
  const subSchema = {};
443
- applyConstraints(subSchema, constraints);
490
+ applyConstraints(subSchema, constraints, ctx);
444
491
  propertyOverrides[target] = subSchema;
445
492
  }
446
493
  if (schema.$ref) {
@@ -484,6 +531,8 @@ function generateTypeNode(type, ctx) {
484
531
  return generateArrayType(type, ctx);
485
532
  case "object":
486
533
  return generateObjectType(type, ctx);
534
+ case "record":
535
+ return generateRecordType(type, ctx);
487
536
  case "union":
488
537
  return generateUnionType(type, ctx);
489
538
  case "reference":
@@ -491,7 +540,7 @@ function generateTypeNode(type, ctx) {
491
540
  case "dynamic":
492
541
  return generateDynamicType(type);
493
542
  case "custom":
494
- return generateCustomType(type);
543
+ return generateCustomType(type, ctx);
495
544
  default: {
496
545
  const _exhaustive = type;
497
546
  return _exhaustive;
@@ -540,16 +589,27 @@ function generateObjectType(type, ctx) {
540
589
  }
541
590
  return schema;
542
591
  }
592
+ function generateRecordType(type, ctx) {
593
+ return {
594
+ type: "object",
595
+ additionalProperties: generateTypeNode(type.valueType, ctx)
596
+ };
597
+ }
543
598
  function generatePropertySchema(prop, ctx) {
544
599
  const schema = generateTypeNode(prop.type, ctx);
545
- applyConstraints(schema, prop.constraints);
546
- applyAnnotations(schema, prop.annotations);
600
+ applyConstraints(schema, prop.constraints, ctx);
601
+ applyAnnotations(schema, prop.annotations, ctx);
547
602
  return schema;
548
603
  }
549
604
  function generateUnionType(type, ctx) {
550
605
  if (isBooleanUnion(type)) {
551
606
  return { type: "boolean" };
552
607
  }
608
+ if (isNullableUnion(type)) {
609
+ return {
610
+ oneOf: type.members.map((m) => generateTypeNode(m, ctx))
611
+ };
612
+ }
553
613
  return {
554
614
  anyOf: type.members.map((m) => generateTypeNode(m, ctx))
555
615
  };
@@ -559,6 +619,13 @@ function isBooleanUnion(type) {
559
619
  const kinds = type.members.map((m) => m.kind);
560
620
  return kinds.every((k) => k === "primitive") && type.members.every((m) => m.kind === "primitive" && m.primitiveKind === "boolean");
561
621
  }
622
+ function isNullableUnion(type) {
623
+ if (type.members.length !== 2) return false;
624
+ const nullCount = type.members.filter(
625
+ (m) => m.kind === "primitive" && m.primitiveKind === "null"
626
+ ).length;
627
+ return nullCount === 1;
628
+ }
562
629
  function generateReferenceType(type) {
563
630
  return { $ref: `#/$defs/${type.name}` };
564
631
  }
@@ -579,10 +646,7 @@ function generateDynamicType(type) {
579
646
  "x-formspec-schemaSource": type.sourceKey
580
647
  };
581
648
  }
582
- function generateCustomType(_type) {
583
- return { type: "object" };
584
- }
585
- function applyConstraints(schema, constraints) {
649
+ function applyConstraints(schema, constraints, ctx) {
586
650
  for (const constraint of constraints) {
587
651
  switch (constraint.constraintKind) {
588
652
  case "minimum":
@@ -624,9 +688,13 @@ function applyConstraints(schema, constraints) {
624
688
  case "uniqueItems":
625
689
  schema.uniqueItems = constraint.value;
626
690
  break;
691
+ case "const":
692
+ schema.const = constraint.value;
693
+ break;
627
694
  case "allowedMembers":
628
695
  break;
629
696
  case "custom":
697
+ applyCustomConstraint(schema, constraint, ctx);
630
698
  break;
631
699
  default: {
632
700
  const _exhaustive = constraint;
@@ -635,7 +703,7 @@ function applyConstraints(schema, constraints) {
635
703
  }
636
704
  }
637
705
  }
638
- function applyAnnotations(schema, annotations) {
706
+ function applyAnnotations(schema, annotations, ctx) {
639
707
  for (const annotation of annotations) {
640
708
  switch (annotation.annotationKind) {
641
709
  case "displayName":
@@ -647,14 +715,21 @@ function applyAnnotations(schema, annotations) {
647
715
  case "defaultValue":
648
716
  schema.default = annotation.value;
649
717
  break;
718
+ case "format":
719
+ schema.format = annotation.value;
720
+ break;
650
721
  case "deprecated":
651
722
  schema.deprecated = true;
723
+ if (annotation.message !== void 0 && annotation.message !== "") {
724
+ schema["x-formspec-deprecation-description"] = annotation.message;
725
+ }
652
726
  break;
653
727
  case "placeholder":
654
728
  break;
655
729
  case "formatHint":
656
730
  break;
657
731
  case "custom":
732
+ applyCustomAnnotation(schema, annotation, ctx);
658
733
  break;
659
734
  default: {
660
735
  const _exhaustive = annotation;
@@ -663,11 +738,41 @@ function applyAnnotations(schema, annotations) {
663
738
  }
664
739
  }
665
740
  }
741
+ function generateCustomType(type, ctx) {
742
+ const registration = ctx.extensionRegistry?.findType(type.typeId);
743
+ if (registration === void 0) {
744
+ throw new Error(
745
+ `Cannot generate JSON Schema for custom type "${type.typeId}" without a matching extension registration`
746
+ );
747
+ }
748
+ return registration.toJsonSchema(type.payload, ctx.vendorPrefix);
749
+ }
750
+ function applyCustomConstraint(schema, constraint, ctx) {
751
+ const registration = ctx.extensionRegistry?.findConstraint(constraint.constraintId);
752
+ if (registration === void 0) {
753
+ throw new Error(
754
+ `Cannot generate JSON Schema for custom constraint "${constraint.constraintId}" without a matching extension registration`
755
+ );
756
+ }
757
+ Object.assign(schema, registration.toJsonSchema(constraint.payload, ctx.vendorPrefix));
758
+ }
759
+ function applyCustomAnnotation(schema, annotation, ctx) {
760
+ const registration = ctx.extensionRegistry?.findAnnotation(annotation.annotationId);
761
+ if (registration === void 0) {
762
+ throw new Error(
763
+ `Cannot generate JSON Schema for custom annotation "${annotation.annotationId}" without a matching extension registration`
764
+ );
765
+ }
766
+ if (registration.toJsonSchema === void 0) {
767
+ return;
768
+ }
769
+ Object.assign(schema, registration.toJsonSchema(annotation.value, ctx.vendorPrefix));
770
+ }
666
771
 
667
772
  // src/json-schema/generator.ts
668
- function generateJsonSchema(form) {
773
+ function generateJsonSchema(form, options) {
669
774
  const ir = canonicalizeChainDSL(form);
670
- return generateJsonSchemaFromIR(ir);
775
+ return generateJsonSchemaFromIR(ir, options);
671
776
  }
672
777
 
673
778
  // src/ui-schema/schema.ts
@@ -805,25 +910,31 @@ function createShowRule(fieldName, value) {
805
910
  }
806
911
  };
807
912
  }
913
+ function flattenConditionSchema(scope, schema) {
914
+ if (schema.allOf === void 0) {
915
+ if (scope === "#") {
916
+ return [schema];
917
+ }
918
+ const fieldName = scope.replace("#/properties/", "");
919
+ return [
920
+ {
921
+ properties: {
922
+ [fieldName]: schema
923
+ }
924
+ }
925
+ ];
926
+ }
927
+ return schema.allOf.flatMap((member) => flattenConditionSchema(scope, member));
928
+ }
808
929
  function combineRules(parentRule, childRule) {
809
- const parentCondition = parentRule.condition;
810
- const childCondition = childRule.condition;
811
930
  return {
812
931
  effect: "SHOW",
813
932
  condition: {
814
933
  scope: "#",
815
934
  schema: {
816
935
  allOf: [
817
- {
818
- properties: {
819
- [parentCondition.scope.replace("#/properties/", "")]: parentCondition.schema
820
- }
821
- },
822
- {
823
- properties: {
824
- [childCondition.scope.replace("#/properties/", "")]: childCondition.schema
825
- }
826
- }
936
+ ...flattenConditionSchema(parentRule.condition.scope, parentRule.condition.schema),
937
+ ...flattenConditionSchema(childRule.condition.scope, childRule.condition.schema)
827
938
  ]
828
939
  }
829
940
  }
@@ -831,10 +942,14 @@ function combineRules(parentRule, childRule) {
831
942
  }
832
943
  function fieldNodeToControl(field, parentRule) {
833
944
  const displayNameAnnotation = field.annotations.find((a) => a.annotationKind === "displayName");
945
+ const placeholderAnnotation = field.annotations.find((a) => a.annotationKind === "placeholder");
834
946
  const control = {
835
947
  type: "Control",
836
948
  scope: fieldToScope(field.name),
837
949
  ...displayNameAnnotation !== void 0 && { label: displayNameAnnotation.value },
950
+ ...placeholderAnnotation !== void 0 && {
951
+ options: { placeholder: placeholderAnnotation.value }
952
+ },
838
953
  ...parentRule !== void 0 && { rule: parentRule }
839
954
  };
840
955
  return control;
@@ -897,6 +1012,48 @@ function getSchemaExtension(schema, key) {
897
1012
  return schema[key];
898
1013
  }
899
1014
 
1015
+ // src/extensions/registry.ts
1016
+ function createExtensionRegistry(extensions) {
1017
+ const typeMap = /* @__PURE__ */ new Map();
1018
+ const constraintMap = /* @__PURE__ */ new Map();
1019
+ const annotationMap = /* @__PURE__ */ new Map();
1020
+ for (const ext of extensions) {
1021
+ if (ext.types !== void 0) {
1022
+ for (const type of ext.types) {
1023
+ const qualifiedId = `${ext.extensionId}/${type.typeName}`;
1024
+ if (typeMap.has(qualifiedId)) {
1025
+ throw new Error(`Duplicate custom type ID: "${qualifiedId}"`);
1026
+ }
1027
+ typeMap.set(qualifiedId, type);
1028
+ }
1029
+ }
1030
+ if (ext.constraints !== void 0) {
1031
+ for (const constraint of ext.constraints) {
1032
+ const qualifiedId = `${ext.extensionId}/${constraint.constraintName}`;
1033
+ if (constraintMap.has(qualifiedId)) {
1034
+ throw new Error(`Duplicate custom constraint ID: "${qualifiedId}"`);
1035
+ }
1036
+ constraintMap.set(qualifiedId, constraint);
1037
+ }
1038
+ }
1039
+ if (ext.annotations !== void 0) {
1040
+ for (const annotation of ext.annotations) {
1041
+ const qualifiedId = `${ext.extensionId}/${annotation.annotationName}`;
1042
+ if (annotationMap.has(qualifiedId)) {
1043
+ throw new Error(`Duplicate custom annotation ID: "${qualifiedId}"`);
1044
+ }
1045
+ annotationMap.set(qualifiedId, annotation);
1046
+ }
1047
+ }
1048
+ }
1049
+ return {
1050
+ extensions,
1051
+ findType: (typeId) => typeMap.get(typeId),
1052
+ findConstraint: (constraintId) => constraintMap.get(constraintId),
1053
+ findAnnotation: (annotationId) => annotationMap.get(annotationId)
1054
+ };
1055
+ }
1056
+
900
1057
  // src/json-schema/schema.ts
901
1058
  var import_zod3 = require("zod");
902
1059
  var jsonSchemaTypeSchema = import_zod3.z.enum([
@@ -960,12 +1117,9 @@ var jsonSchema7Schema = import_zod3.z.lazy(
960
1117
  );
961
1118
 
962
1119
  // src/validate/constraint-validator.ts
963
- function makeCode(ctx, category, number) {
964
- return `${ctx.vendorPrefix}-${category}-${String(number).padStart(3, "0")}`;
965
- }
966
1120
  function addContradiction(ctx, message, primary, related) {
967
1121
  ctx.diagnostics.push({
968
- code: makeCode(ctx, "CONTRADICTION", 1),
1122
+ code: "CONTRADICTING_CONSTRAINTS",
969
1123
  message,
970
1124
  severity: "error",
971
1125
  primaryLocation: primary,
@@ -974,7 +1128,7 @@ function addContradiction(ctx, message, primary, related) {
974
1128
  }
975
1129
  function addTypeMismatch(ctx, message, primary) {
976
1130
  ctx.diagnostics.push({
977
- code: makeCode(ctx, "TYPE_MISMATCH", 1),
1131
+ code: "TYPE_MISMATCH",
978
1132
  message,
979
1133
  severity: "error",
980
1134
  primaryLocation: primary,
@@ -983,13 +1137,31 @@ function addTypeMismatch(ctx, message, primary) {
983
1137
  }
984
1138
  function addUnknownExtension(ctx, message, primary) {
985
1139
  ctx.diagnostics.push({
986
- code: makeCode(ctx, "UNKNOWN_EXTENSION", 1),
1140
+ code: "UNKNOWN_EXTENSION",
987
1141
  message,
988
1142
  severity: "warning",
989
1143
  primaryLocation: primary,
990
1144
  relatedLocations: []
991
1145
  });
992
1146
  }
1147
+ function addUnknownPathTarget(ctx, message, primary) {
1148
+ ctx.diagnostics.push({
1149
+ code: "UNKNOWN_PATH_TARGET",
1150
+ message,
1151
+ severity: "error",
1152
+ primaryLocation: primary,
1153
+ relatedLocations: []
1154
+ });
1155
+ }
1156
+ function addConstraintBroadening(ctx, message, primary, related) {
1157
+ ctx.diagnostics.push({
1158
+ code: "CONSTRAINT_BROADENING",
1159
+ message,
1160
+ severity: "error",
1161
+ primaryLocation: primary,
1162
+ relatedLocations: [related]
1163
+ });
1164
+ }
993
1165
  function findNumeric(constraints, constraintKind) {
994
1166
  return constraints.find((c) => c.constraintKind === constraintKind);
995
1167
  }
@@ -1001,6 +1173,165 @@ function findAllowedMembers(constraints) {
1001
1173
  (c) => c.constraintKind === "allowedMembers"
1002
1174
  );
1003
1175
  }
1176
+ function findConstConstraints(constraints) {
1177
+ return constraints.filter(
1178
+ (c) => c.constraintKind === "const"
1179
+ );
1180
+ }
1181
+ function jsonValueEquals(left, right) {
1182
+ if (left === right) {
1183
+ return true;
1184
+ }
1185
+ if (Array.isArray(left) || Array.isArray(right)) {
1186
+ if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) {
1187
+ return false;
1188
+ }
1189
+ return left.every((item, index) => jsonValueEquals(item, right[index]));
1190
+ }
1191
+ if (isJsonObject(left) || isJsonObject(right)) {
1192
+ if (!isJsonObject(left) || !isJsonObject(right)) {
1193
+ return false;
1194
+ }
1195
+ const leftKeys = Object.keys(left).sort();
1196
+ const rightKeys = Object.keys(right).sort();
1197
+ if (leftKeys.length !== rightKeys.length) {
1198
+ return false;
1199
+ }
1200
+ return leftKeys.every((key, index) => {
1201
+ const rightKey = rightKeys[index];
1202
+ if (rightKey !== key) {
1203
+ return false;
1204
+ }
1205
+ const leftValue = left[key];
1206
+ const rightValue = right[rightKey];
1207
+ return leftValue !== void 0 && rightValue !== void 0 && jsonValueEquals(leftValue, rightValue);
1208
+ });
1209
+ }
1210
+ return false;
1211
+ }
1212
+ function isJsonObject(value) {
1213
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1214
+ }
1215
+ function isOrderedBoundConstraint(constraint) {
1216
+ 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";
1217
+ }
1218
+ function pathKey(constraint) {
1219
+ return constraint.path?.segments.join(".") ?? "";
1220
+ }
1221
+ function orderedBoundFamily(kind) {
1222
+ switch (kind) {
1223
+ case "minimum":
1224
+ case "exclusiveMinimum":
1225
+ return "numeric-lower";
1226
+ case "maximum":
1227
+ case "exclusiveMaximum":
1228
+ return "numeric-upper";
1229
+ case "minLength":
1230
+ return "minLength";
1231
+ case "minItems":
1232
+ return "minItems";
1233
+ case "maxLength":
1234
+ return "maxLength";
1235
+ case "maxItems":
1236
+ return "maxItems";
1237
+ default: {
1238
+ const _exhaustive = kind;
1239
+ return _exhaustive;
1240
+ }
1241
+ }
1242
+ }
1243
+ function isNumericLowerKind(kind) {
1244
+ return kind === "minimum" || kind === "exclusiveMinimum";
1245
+ }
1246
+ function isNumericUpperKind(kind) {
1247
+ return kind === "maximum" || kind === "exclusiveMaximum";
1248
+ }
1249
+ function describeConstraintTag(constraint) {
1250
+ return `@${constraint.constraintKind}`;
1251
+ }
1252
+ function compareConstraintStrength(current, previous) {
1253
+ const family = orderedBoundFamily(current.constraintKind);
1254
+ if (family === "numeric-lower") {
1255
+ if (!isNumericLowerKind(current.constraintKind) || !isNumericLowerKind(previous.constraintKind)) {
1256
+ throw new Error("numeric-lower family received non-numeric lower-bound constraint");
1257
+ }
1258
+ if (current.value !== previous.value) {
1259
+ return current.value > previous.value ? 1 : -1;
1260
+ }
1261
+ if (current.constraintKind === "exclusiveMinimum" && previous.constraintKind === "minimum") {
1262
+ return 1;
1263
+ }
1264
+ if (current.constraintKind === "minimum" && previous.constraintKind === "exclusiveMinimum") {
1265
+ return -1;
1266
+ }
1267
+ return 0;
1268
+ }
1269
+ if (family === "numeric-upper") {
1270
+ if (!isNumericUpperKind(current.constraintKind) || !isNumericUpperKind(previous.constraintKind)) {
1271
+ throw new Error("numeric-upper family received non-numeric upper-bound constraint");
1272
+ }
1273
+ if (current.value !== previous.value) {
1274
+ return current.value < previous.value ? 1 : -1;
1275
+ }
1276
+ if (current.constraintKind === "exclusiveMaximum" && previous.constraintKind === "maximum") {
1277
+ return 1;
1278
+ }
1279
+ if (current.constraintKind === "maximum" && previous.constraintKind === "exclusiveMaximum") {
1280
+ return -1;
1281
+ }
1282
+ return 0;
1283
+ }
1284
+ switch (family) {
1285
+ case "minLength":
1286
+ case "minItems":
1287
+ if (current.value === previous.value) {
1288
+ return 0;
1289
+ }
1290
+ return current.value > previous.value ? 1 : -1;
1291
+ case "maxLength":
1292
+ case "maxItems":
1293
+ if (current.value === previous.value) {
1294
+ return 0;
1295
+ }
1296
+ return current.value < previous.value ? 1 : -1;
1297
+ default: {
1298
+ const _exhaustive = family;
1299
+ return _exhaustive;
1300
+ }
1301
+ }
1302
+ }
1303
+ function checkConstraintBroadening(ctx, fieldName, constraints) {
1304
+ const strongestByKey = /* @__PURE__ */ new Map();
1305
+ for (const constraint of constraints) {
1306
+ if (!isOrderedBoundConstraint(constraint)) {
1307
+ continue;
1308
+ }
1309
+ const key = `${orderedBoundFamily(constraint.constraintKind)}:${pathKey(constraint)}`;
1310
+ const previous = strongestByKey.get(key);
1311
+ if (previous === void 0) {
1312
+ strongestByKey.set(key, constraint);
1313
+ continue;
1314
+ }
1315
+ const strength = compareConstraintStrength(constraint, previous);
1316
+ if (strength < 0) {
1317
+ const displayFieldName = formatPathTargetFieldName(
1318
+ fieldName,
1319
+ constraint.path?.segments ?? []
1320
+ );
1321
+ addConstraintBroadening(
1322
+ ctx,
1323
+ `Field "${displayFieldName}": ${describeConstraintTag(constraint)} (${String(constraint.value)}) is broader than earlier ${describeConstraintTag(previous)} (${String(previous.value)}). Constraints can only narrow.`,
1324
+ constraint.provenance,
1325
+ previous.provenance
1326
+ );
1327
+ continue;
1328
+ }
1329
+ if (strength <= 0) {
1330
+ continue;
1331
+ }
1332
+ strongestByKey.set(key, constraint);
1333
+ }
1334
+ }
1004
1335
  function checkNumericContradictions(ctx, fieldName, constraints) {
1005
1336
  const min = findNumeric(constraints, "minimum");
1006
1337
  const max = findNumeric(constraints, "maximum");
@@ -1087,6 +1418,25 @@ function checkAllowedMembersContradiction(ctx, fieldName, constraints) {
1087
1418
  }
1088
1419
  }
1089
1420
  }
1421
+ function checkConstContradictions(ctx, fieldName, constraints) {
1422
+ const constConstraints = findConstConstraints(constraints);
1423
+ if (constConstraints.length < 2) return;
1424
+ const first = constConstraints[0];
1425
+ if (first === void 0) return;
1426
+ for (let i = 1; i < constConstraints.length; i++) {
1427
+ const current = constConstraints[i];
1428
+ if (current === void 0) continue;
1429
+ if (jsonValueEquals(first.value, current.value)) {
1430
+ continue;
1431
+ }
1432
+ addContradiction(
1433
+ ctx,
1434
+ `Field "${fieldName}": conflicting @const constraints require both ${JSON.stringify(first.value)} and ${JSON.stringify(current.value)}`,
1435
+ first.provenance,
1436
+ current.provenance
1437
+ );
1438
+ }
1439
+ }
1090
1440
  function typeLabel(type) {
1091
1441
  switch (type.kind) {
1092
1442
  case "primitive":
@@ -1097,6 +1447,8 @@ function typeLabel(type) {
1097
1447
  return "array";
1098
1448
  case "object":
1099
1449
  return "object";
1450
+ case "record":
1451
+ return "record";
1100
1452
  case "union":
1101
1453
  return "union";
1102
1454
  case "reference":
@@ -1111,85 +1463,173 @@ function typeLabel(type) {
1111
1463
  }
1112
1464
  }
1113
1465
  }
1114
- function checkTypeApplicability(ctx, fieldName, type, constraints) {
1115
- const isNumber = type.kind === "primitive" && type.primitiveKind === "number";
1116
- const isString = type.kind === "primitive" && type.primitiveKind === "string";
1117
- const isArray = type.kind === "array";
1118
- const isEnum = type.kind === "enum";
1119
- const label = typeLabel(type);
1120
- for (const constraint of constraints) {
1121
- if (constraint.path) {
1122
- const isTraversable = type.kind === "object" || type.kind === "array" || type.kind === "reference";
1123
- if (!isTraversable) {
1466
+ function dereferenceType(ctx, type) {
1467
+ let current = type;
1468
+ const seen = /* @__PURE__ */ new Set();
1469
+ while (current.kind === "reference") {
1470
+ if (seen.has(current.name)) {
1471
+ return current;
1472
+ }
1473
+ seen.add(current.name);
1474
+ const definition = ctx.typeRegistry[current.name];
1475
+ if (definition === void 0) {
1476
+ return current;
1477
+ }
1478
+ current = definition.type;
1479
+ }
1480
+ return current;
1481
+ }
1482
+ function resolvePathTargetType(ctx, type, segments) {
1483
+ const effectiveType = dereferenceType(ctx, type);
1484
+ if (segments.length === 0) {
1485
+ return { kind: "resolved", type: effectiveType };
1486
+ }
1487
+ if (effectiveType.kind === "array") {
1488
+ return resolvePathTargetType(ctx, effectiveType.items, segments);
1489
+ }
1490
+ if (effectiveType.kind === "object") {
1491
+ const [segment, ...rest] = segments;
1492
+ if (segment === void 0) {
1493
+ throw new Error("Invariant violation: object path traversal requires a segment");
1494
+ }
1495
+ const property = effectiveType.properties.find((prop) => prop.name === segment);
1496
+ if (property === void 0) {
1497
+ return { kind: "missing-property", segment };
1498
+ }
1499
+ return resolvePathTargetType(ctx, property.type, rest);
1500
+ }
1501
+ return { kind: "unresolvable", type: effectiveType };
1502
+ }
1503
+ function formatPathTargetFieldName(fieldName, path) {
1504
+ return path.length === 0 ? fieldName : `${fieldName}.${path.join(".")}`;
1505
+ }
1506
+ function checkConstraintOnType(ctx, fieldName, type, constraint) {
1507
+ const effectiveType = dereferenceType(ctx, type);
1508
+ const isNumber = effectiveType.kind === "primitive" && effectiveType.primitiveKind === "number";
1509
+ const isString = effectiveType.kind === "primitive" && effectiveType.primitiveKind === "string";
1510
+ const isArray = effectiveType.kind === "array";
1511
+ const isEnum = effectiveType.kind === "enum";
1512
+ const arrayItemType = effectiveType.kind === "array" ? dereferenceType(ctx, effectiveType.items) : void 0;
1513
+ const isStringArray = arrayItemType?.kind === "primitive" && arrayItemType.primitiveKind === "string";
1514
+ const label = typeLabel(effectiveType);
1515
+ const ck = constraint.constraintKind;
1516
+ switch (ck) {
1517
+ case "minimum":
1518
+ case "maximum":
1519
+ case "exclusiveMinimum":
1520
+ case "exclusiveMaximum":
1521
+ case "multipleOf": {
1522
+ if (!isNumber) {
1124
1523
  addTypeMismatch(
1125
1524
  ctx,
1126
- `Field "${fieldName}": path-targeted constraint "${constraint.constraintKind}" is invalid because type "${label}" cannot be traversed`,
1525
+ `Field "${fieldName}": constraint "${ck}" is only valid on number fields, but field type is "${label}"`,
1127
1526
  constraint.provenance
1128
1527
  );
1129
1528
  }
1130
- continue;
1529
+ break;
1131
1530
  }
1132
- const ck = constraint.constraintKind;
1133
- switch (ck) {
1134
- case "minimum":
1135
- case "maximum":
1136
- case "exclusiveMinimum":
1137
- case "exclusiveMaximum":
1138
- case "multipleOf": {
1139
- if (!isNumber) {
1140
- addTypeMismatch(
1141
- ctx,
1142
- `Field "${fieldName}": constraint "${ck}" is only valid on number fields, but field type is "${label}"`,
1143
- constraint.provenance
1144
- );
1145
- }
1146
- break;
1531
+ case "minLength":
1532
+ case "maxLength":
1533
+ case "pattern": {
1534
+ if (!isString && !isStringArray) {
1535
+ addTypeMismatch(
1536
+ ctx,
1537
+ `Field "${fieldName}": constraint "${ck}" is only valid on string fields or string array items, but field type is "${label}"`,
1538
+ constraint.provenance
1539
+ );
1147
1540
  }
1148
- case "minLength":
1149
- case "maxLength":
1150
- case "pattern": {
1151
- if (!isString) {
1152
- addTypeMismatch(
1153
- ctx,
1154
- `Field "${fieldName}": constraint "${ck}" is only valid on string fields, but field type is "${label}"`,
1155
- constraint.provenance
1156
- );
1157
- }
1158
- break;
1541
+ break;
1542
+ }
1543
+ case "minItems":
1544
+ case "maxItems":
1545
+ case "uniqueItems": {
1546
+ if (!isArray) {
1547
+ addTypeMismatch(
1548
+ ctx,
1549
+ `Field "${fieldName}": constraint "${ck}" is only valid on array fields, but field type is "${label}"`,
1550
+ constraint.provenance
1551
+ );
1159
1552
  }
1160
- case "minItems":
1161
- case "maxItems":
1162
- case "uniqueItems": {
1163
- if (!isArray) {
1164
- addTypeMismatch(
1165
- ctx,
1166
- `Field "${fieldName}": constraint "${ck}" is only valid on array fields, but field type is "${label}"`,
1167
- constraint.provenance
1168
- );
1169
- }
1553
+ break;
1554
+ }
1555
+ case "allowedMembers": {
1556
+ if (!isEnum) {
1557
+ addTypeMismatch(
1558
+ ctx,
1559
+ `Field "${fieldName}": constraint "allowedMembers" is only valid on enum fields, but field type is "${label}"`,
1560
+ constraint.provenance
1561
+ );
1562
+ }
1563
+ break;
1564
+ }
1565
+ case "const": {
1566
+ const isPrimitiveConstType = effectiveType.kind === "primitive" && ["string", "number", "boolean", "null"].includes(effectiveType.primitiveKind) || effectiveType.kind === "enum";
1567
+ if (!isPrimitiveConstType) {
1568
+ addTypeMismatch(
1569
+ ctx,
1570
+ `Field "${fieldName}": constraint "const" is only valid on primitive or enum fields, but field type is "${label}"`,
1571
+ constraint.provenance
1572
+ );
1170
1573
  break;
1171
1574
  }
1172
- case "allowedMembers": {
1173
- if (!isEnum) {
1575
+ if (effectiveType.kind === "primitive") {
1576
+ const valueType = constraint.value === null ? "null" : Array.isArray(constraint.value) ? "array" : typeof constraint.value;
1577
+ if (valueType !== effectiveType.primitiveKind) {
1174
1578
  addTypeMismatch(
1175
1579
  ctx,
1176
- `Field "${fieldName}": constraint "allowedMembers" is only valid on enum fields, but field type is "${label}"`,
1580
+ `Field "${fieldName}": @const value type "${valueType}" is incompatible with field type "${effectiveType.primitiveKind}"`,
1177
1581
  constraint.provenance
1178
1582
  );
1179
1583
  }
1180
1584
  break;
1181
1585
  }
1182
- case "custom": {
1183
- checkCustomConstraint(ctx, fieldName, type, constraint);
1184
- break;
1586
+ const memberValues = effectiveType.members.map((member) => member.value);
1587
+ if (!memberValues.some((member) => jsonValueEquals(member, constraint.value))) {
1588
+ addTypeMismatch(
1589
+ ctx,
1590
+ `Field "${fieldName}": @const value ${JSON.stringify(constraint.value)} is not one of the enum members`,
1591
+ constraint.provenance
1592
+ );
1185
1593
  }
1186
- default: {
1187
- const _exhaustive = constraint;
1188
- throw new Error(
1189
- `Unhandled constraint kind: ${_exhaustive.constraintKind}`
1594
+ break;
1595
+ }
1596
+ case "custom": {
1597
+ checkCustomConstraint(ctx, fieldName, effectiveType, constraint);
1598
+ break;
1599
+ }
1600
+ default: {
1601
+ const _exhaustive = constraint;
1602
+ throw new Error(
1603
+ `Unhandled constraint kind: ${_exhaustive.constraintKind}`
1604
+ );
1605
+ }
1606
+ }
1607
+ }
1608
+ function checkTypeApplicability(ctx, fieldName, type, constraints) {
1609
+ for (const constraint of constraints) {
1610
+ if (constraint.path) {
1611
+ const resolution = resolvePathTargetType(ctx, type, constraint.path.segments);
1612
+ const targetFieldName = formatPathTargetFieldName(fieldName, constraint.path.segments);
1613
+ if (resolution.kind === "missing-property") {
1614
+ addUnknownPathTarget(
1615
+ ctx,
1616
+ `Field "${targetFieldName}": path-targeted constraint "${constraint.constraintKind}" references unknown path segment "${resolution.segment}"`,
1617
+ constraint.provenance
1190
1618
  );
1619
+ continue;
1191
1620
  }
1621
+ if (resolution.kind === "unresolvable") {
1622
+ addTypeMismatch(
1623
+ ctx,
1624
+ `Field "${targetFieldName}": path-targeted constraint "${constraint.constraintKind}" is invalid because type "${typeLabel(resolution.type)}" cannot be traversed`,
1625
+ constraint.provenance
1626
+ );
1627
+ continue;
1628
+ }
1629
+ checkConstraintOnType(ctx, targetFieldName, resolution.type, constraint);
1630
+ continue;
1192
1631
  }
1632
+ checkConstraintOnType(ctx, fieldName, type, constraint);
1193
1633
  }
1194
1634
  }
1195
1635
  function checkCustomConstraint(ctx, fieldName, type, constraint) {
@@ -1233,6 +1673,8 @@ function validateConstraints(ctx, name, type, constraints) {
1233
1673
  checkNumericContradictions(ctx, name, constraints);
1234
1674
  checkLengthContradictions(ctx, name, constraints);
1235
1675
  checkAllowedMembersContradiction(ctx, name, constraints);
1676
+ checkConstContradictions(ctx, name, constraints);
1677
+ checkConstraintBroadening(ctx, name, constraints);
1236
1678
  checkTypeApplicability(ctx, name, type, constraints);
1237
1679
  }
1238
1680
  function validateElement(ctx, element) {
@@ -1259,8 +1701,8 @@ function validateElement(ctx, element) {
1259
1701
  function validateIR(ir, options) {
1260
1702
  const ctx = {
1261
1703
  diagnostics: [],
1262
- vendorPrefix: options?.vendorPrefix ?? "FORMSPEC",
1263
- extensionRegistry: options?.extensionRegistry
1704
+ extensionRegistry: options?.extensionRegistry,
1705
+ typeRegistry: ir.typeRegistry
1264
1706
  };
1265
1707
  for (const element of ir.elements) {
1266
1708
  validateElement(ctx, element);
@@ -1272,9 +1714,9 @@ function validateIR(ir, options) {
1272
1714
  }
1273
1715
 
1274
1716
  // src/browser.ts
1275
- function buildFormSchemas(form) {
1717
+ function buildFormSchemas(form, options) {
1276
1718
  return {
1277
- jsonSchema: generateJsonSchema(form),
1719
+ jsonSchema: generateJsonSchema(form, options),
1278
1720
  uiSchema: generateUiSchema(form)
1279
1721
  };
1280
1722
  }
@@ -1285,7 +1727,9 @@ function buildFormSchemas(form) {
1285
1727
  categorizationSchema,
1286
1728
  categorySchema,
1287
1729
  controlSchema,
1730
+ createExtensionRegistry,
1288
1731
  generateJsonSchema,
1732
+ generateJsonSchemaFromIR,
1289
1733
  generateUiSchema,
1290
1734
  getSchemaExtension,
1291
1735
  groupLayoutSchema,