@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.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,13 +313,26 @@ 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);
333
+ if (typeDef.annotations && typeDef.annotations.length > 0) {
334
+ applyAnnotations(ctx.defs[name], typeDef.annotations, ctx);
335
+ }
323
336
  }
324
337
  const properties = {};
325
338
  const required = [];
@@ -331,6 +344,9 @@ function generateJsonSchemaFromIR(ir) {
331
344
  properties,
332
345
  ...uniqueRequired.length > 0 && { required: uniqueRequired }
333
346
  };
347
+ if (ir.annotations && ir.annotations.length > 0) {
348
+ applyAnnotations(result, ir.annotations, ctx);
349
+ }
334
350
  if (Object.keys(ctx.defs).length > 0) {
335
351
  result.$defs = ctx.defs;
336
352
  }
@@ -360,25 +376,54 @@ function collectFields(elements, properties, required, ctx) {
360
376
  }
361
377
  function generateFieldSchema(field, ctx) {
362
378
  const schema = generateTypeNode(field.type, ctx);
379
+ const itemStringSchema = schema.type === "array" && schema.items?.type === "string" ? schema.items : void 0;
363
380
  const directConstraints = [];
381
+ const itemConstraints = [];
364
382
  const pathConstraints = [];
365
383
  for (const c of field.constraints) {
366
384
  if (c.path) {
367
385
  pathConstraints.push(c);
386
+ } else if (itemStringSchema !== void 0 && isStringItemConstraint(c)) {
387
+ itemConstraints.push(c);
368
388
  } else {
369
389
  directConstraints.push(c);
370
390
  }
371
391
  }
372
- applyConstraints(schema, directConstraints);
373
- applyAnnotations(schema, field.annotations);
392
+ applyConstraints(schema, directConstraints, ctx);
393
+ if (itemStringSchema !== void 0) {
394
+ applyConstraints(itemStringSchema, itemConstraints, ctx);
395
+ }
396
+ const rootAnnotations = [];
397
+ const itemAnnotations = [];
398
+ for (const annotation of field.annotations) {
399
+ if (itemStringSchema !== void 0 && annotation.annotationKind === "format") {
400
+ itemAnnotations.push(annotation);
401
+ } else {
402
+ rootAnnotations.push(annotation);
403
+ }
404
+ }
405
+ applyAnnotations(schema, rootAnnotations, ctx);
406
+ if (itemStringSchema !== void 0) {
407
+ applyAnnotations(itemStringSchema, itemAnnotations, ctx);
408
+ }
374
409
  if (pathConstraints.length === 0) {
375
410
  return schema;
376
411
  }
377
- return applyPathTargetedConstraints(schema, pathConstraints);
412
+ return applyPathTargetedConstraints(schema, pathConstraints, ctx);
413
+ }
414
+ function isStringItemConstraint(constraint) {
415
+ switch (constraint.constraintKind) {
416
+ case "minLength":
417
+ case "maxLength":
418
+ case "pattern":
419
+ return true;
420
+ default:
421
+ return false;
422
+ }
378
423
  }
379
- function applyPathTargetedConstraints(schema, pathConstraints) {
424
+ function applyPathTargetedConstraints(schema, pathConstraints, ctx) {
380
425
  if (schema.type === "array" && schema.items) {
381
- schema.items = applyPathTargetedConstraints(schema.items, pathConstraints);
426
+ schema.items = applyPathTargetedConstraints(schema.items, pathConstraints, ctx);
382
427
  return schema;
383
428
  }
384
429
  const byTarget = /* @__PURE__ */ new Map();
@@ -392,7 +437,7 @@ function applyPathTargetedConstraints(schema, pathConstraints) {
392
437
  const propertyOverrides = {};
393
438
  for (const [target, constraints] of byTarget) {
394
439
  const subSchema = {};
395
- applyConstraints(subSchema, constraints);
440
+ applyConstraints(subSchema, constraints, ctx);
396
441
  propertyOverrides[target] = subSchema;
397
442
  }
398
443
  if (schema.$ref) {
@@ -436,6 +481,8 @@ function generateTypeNode(type, ctx) {
436
481
  return generateArrayType(type, ctx);
437
482
  case "object":
438
483
  return generateObjectType(type, ctx);
484
+ case "record":
485
+ return generateRecordType(type, ctx);
439
486
  case "union":
440
487
  return generateUnionType(type, ctx);
441
488
  case "reference":
@@ -443,7 +490,7 @@ function generateTypeNode(type, ctx) {
443
490
  case "dynamic":
444
491
  return generateDynamicType(type);
445
492
  case "custom":
446
- return generateCustomType(type);
493
+ return generateCustomType(type, ctx);
447
494
  default: {
448
495
  const _exhaustive = type;
449
496
  return _exhaustive;
@@ -492,16 +539,27 @@ function generateObjectType(type, ctx) {
492
539
  }
493
540
  return schema;
494
541
  }
542
+ function generateRecordType(type, ctx) {
543
+ return {
544
+ type: "object",
545
+ additionalProperties: generateTypeNode(type.valueType, ctx)
546
+ };
547
+ }
495
548
  function generatePropertySchema(prop, ctx) {
496
549
  const schema = generateTypeNode(prop.type, ctx);
497
- applyConstraints(schema, prop.constraints);
498
- applyAnnotations(schema, prop.annotations);
550
+ applyConstraints(schema, prop.constraints, ctx);
551
+ applyAnnotations(schema, prop.annotations, ctx);
499
552
  return schema;
500
553
  }
501
554
  function generateUnionType(type, ctx) {
502
555
  if (isBooleanUnion(type)) {
503
556
  return { type: "boolean" };
504
557
  }
558
+ if (isNullableUnion(type)) {
559
+ return {
560
+ oneOf: type.members.map((m) => generateTypeNode(m, ctx))
561
+ };
562
+ }
505
563
  return {
506
564
  anyOf: type.members.map((m) => generateTypeNode(m, ctx))
507
565
  };
@@ -511,6 +569,13 @@ function isBooleanUnion(type) {
511
569
  const kinds = type.members.map((m) => m.kind);
512
570
  return kinds.every((k) => k === "primitive") && type.members.every((m) => m.kind === "primitive" && m.primitiveKind === "boolean");
513
571
  }
572
+ function isNullableUnion(type) {
573
+ if (type.members.length !== 2) return false;
574
+ const nullCount = type.members.filter(
575
+ (m) => m.kind === "primitive" && m.primitiveKind === "null"
576
+ ).length;
577
+ return nullCount === 1;
578
+ }
514
579
  function generateReferenceType(type) {
515
580
  return { $ref: `#/$defs/${type.name}` };
516
581
  }
@@ -531,10 +596,7 @@ function generateDynamicType(type) {
531
596
  "x-formspec-schemaSource": type.sourceKey
532
597
  };
533
598
  }
534
- function generateCustomType(_type) {
535
- return { type: "object" };
536
- }
537
- function applyConstraints(schema, constraints) {
599
+ function applyConstraints(schema, constraints, ctx) {
538
600
  for (const constraint of constraints) {
539
601
  switch (constraint.constraintKind) {
540
602
  case "minimum":
@@ -576,9 +638,13 @@ function applyConstraints(schema, constraints) {
576
638
  case "uniqueItems":
577
639
  schema.uniqueItems = constraint.value;
578
640
  break;
641
+ case "const":
642
+ schema.const = constraint.value;
643
+ break;
579
644
  case "allowedMembers":
580
645
  break;
581
646
  case "custom":
647
+ applyCustomConstraint(schema, constraint, ctx);
582
648
  break;
583
649
  default: {
584
650
  const _exhaustive = constraint;
@@ -587,7 +653,7 @@ function applyConstraints(schema, constraints) {
587
653
  }
588
654
  }
589
655
  }
590
- function applyAnnotations(schema, annotations) {
656
+ function applyAnnotations(schema, annotations, ctx) {
591
657
  for (const annotation of annotations) {
592
658
  switch (annotation.annotationKind) {
593
659
  case "displayName":
@@ -599,14 +665,21 @@ function applyAnnotations(schema, annotations) {
599
665
  case "defaultValue":
600
666
  schema.default = annotation.value;
601
667
  break;
668
+ case "format":
669
+ schema.format = annotation.value;
670
+ break;
602
671
  case "deprecated":
603
672
  schema.deprecated = true;
673
+ if (annotation.message !== void 0 && annotation.message !== "") {
674
+ schema["x-formspec-deprecation-description"] = annotation.message;
675
+ }
604
676
  break;
605
677
  case "placeholder":
606
678
  break;
607
679
  case "formatHint":
608
680
  break;
609
681
  case "custom":
682
+ applyCustomAnnotation(schema, annotation, ctx);
610
683
  break;
611
684
  default: {
612
685
  const _exhaustive = annotation;
@@ -615,11 +688,41 @@ function applyAnnotations(schema, annotations) {
615
688
  }
616
689
  }
617
690
  }
691
+ function generateCustomType(type, ctx) {
692
+ const registration = ctx.extensionRegistry?.findType(type.typeId);
693
+ if (registration === void 0) {
694
+ throw new Error(
695
+ `Cannot generate JSON Schema for custom type "${type.typeId}" without a matching extension registration`
696
+ );
697
+ }
698
+ return registration.toJsonSchema(type.payload, ctx.vendorPrefix);
699
+ }
700
+ function applyCustomConstraint(schema, constraint, ctx) {
701
+ const registration = ctx.extensionRegistry?.findConstraint(constraint.constraintId);
702
+ if (registration === void 0) {
703
+ throw new Error(
704
+ `Cannot generate JSON Schema for custom constraint "${constraint.constraintId}" without a matching extension registration`
705
+ );
706
+ }
707
+ Object.assign(schema, registration.toJsonSchema(constraint.payload, ctx.vendorPrefix));
708
+ }
709
+ function applyCustomAnnotation(schema, annotation, ctx) {
710
+ const registration = ctx.extensionRegistry?.findAnnotation(annotation.annotationId);
711
+ if (registration === void 0) {
712
+ throw new Error(
713
+ `Cannot generate JSON Schema for custom annotation "${annotation.annotationId}" without a matching extension registration`
714
+ );
715
+ }
716
+ if (registration.toJsonSchema === void 0) {
717
+ return;
718
+ }
719
+ Object.assign(schema, registration.toJsonSchema(annotation.value, ctx.vendorPrefix));
720
+ }
618
721
 
619
722
  // src/json-schema/generator.ts
620
- function generateJsonSchema(form) {
723
+ function generateJsonSchema(form, options) {
621
724
  const ir = canonicalizeChainDSL(form);
622
- return generateJsonSchemaFromIR(ir);
725
+ return generateJsonSchemaFromIR(ir, options);
623
726
  }
624
727
 
625
728
  // src/ui-schema/schema.ts
@@ -757,25 +860,31 @@ function createShowRule(fieldName, value) {
757
860
  }
758
861
  };
759
862
  }
863
+ function flattenConditionSchema(scope, schema) {
864
+ if (schema.allOf === void 0) {
865
+ if (scope === "#") {
866
+ return [schema];
867
+ }
868
+ const fieldName = scope.replace("#/properties/", "");
869
+ return [
870
+ {
871
+ properties: {
872
+ [fieldName]: schema
873
+ }
874
+ }
875
+ ];
876
+ }
877
+ return schema.allOf.flatMap((member) => flattenConditionSchema(scope, member));
878
+ }
760
879
  function combineRules(parentRule, childRule) {
761
- const parentCondition = parentRule.condition;
762
- const childCondition = childRule.condition;
763
880
  return {
764
881
  effect: "SHOW",
765
882
  condition: {
766
883
  scope: "#",
767
884
  schema: {
768
885
  allOf: [
769
- {
770
- properties: {
771
- [parentCondition.scope.replace("#/properties/", "")]: parentCondition.schema
772
- }
773
- },
774
- {
775
- properties: {
776
- [childCondition.scope.replace("#/properties/", "")]: childCondition.schema
777
- }
778
- }
886
+ ...flattenConditionSchema(parentRule.condition.scope, parentRule.condition.schema),
887
+ ...flattenConditionSchema(childRule.condition.scope, childRule.condition.schema)
779
888
  ]
780
889
  }
781
890
  }
@@ -783,10 +892,14 @@ function combineRules(parentRule, childRule) {
783
892
  }
784
893
  function fieldNodeToControl(field, parentRule) {
785
894
  const displayNameAnnotation = field.annotations.find((a) => a.annotationKind === "displayName");
895
+ const placeholderAnnotation = field.annotations.find((a) => a.annotationKind === "placeholder");
786
896
  const control = {
787
897
  type: "Control",
788
898
  scope: fieldToScope(field.name),
789
899
  ...displayNameAnnotation !== void 0 && { label: displayNameAnnotation.value },
900
+ ...placeholderAnnotation !== void 0 && {
901
+ options: { placeholder: placeholderAnnotation.value }
902
+ },
790
903
  ...parentRule !== void 0 && { rule: parentRule }
791
904
  };
792
905
  return control;
@@ -849,6 +962,48 @@ function getSchemaExtension(schema, key) {
849
962
  return schema[key];
850
963
  }
851
964
 
965
+ // src/extensions/registry.ts
966
+ function createExtensionRegistry(extensions) {
967
+ const typeMap = /* @__PURE__ */ new Map();
968
+ const constraintMap = /* @__PURE__ */ new Map();
969
+ const annotationMap = /* @__PURE__ */ new Map();
970
+ for (const ext of extensions) {
971
+ if (ext.types !== void 0) {
972
+ for (const type of ext.types) {
973
+ const qualifiedId = `${ext.extensionId}/${type.typeName}`;
974
+ if (typeMap.has(qualifiedId)) {
975
+ throw new Error(`Duplicate custom type ID: "${qualifiedId}"`);
976
+ }
977
+ typeMap.set(qualifiedId, type);
978
+ }
979
+ }
980
+ if (ext.constraints !== void 0) {
981
+ for (const constraint of ext.constraints) {
982
+ const qualifiedId = `${ext.extensionId}/${constraint.constraintName}`;
983
+ if (constraintMap.has(qualifiedId)) {
984
+ throw new Error(`Duplicate custom constraint ID: "${qualifiedId}"`);
985
+ }
986
+ constraintMap.set(qualifiedId, constraint);
987
+ }
988
+ }
989
+ if (ext.annotations !== void 0) {
990
+ for (const annotation of ext.annotations) {
991
+ const qualifiedId = `${ext.extensionId}/${annotation.annotationName}`;
992
+ if (annotationMap.has(qualifiedId)) {
993
+ throw new Error(`Duplicate custom annotation ID: "${qualifiedId}"`);
994
+ }
995
+ annotationMap.set(qualifiedId, annotation);
996
+ }
997
+ }
998
+ }
999
+ return {
1000
+ extensions,
1001
+ findType: (typeId) => typeMap.get(typeId),
1002
+ findConstraint: (constraintId) => constraintMap.get(constraintId),
1003
+ findAnnotation: (annotationId) => annotationMap.get(annotationId)
1004
+ };
1005
+ }
1006
+
852
1007
  // src/json-schema/schema.ts
853
1008
  import { z as z3 } from "zod";
854
1009
  var jsonSchemaTypeSchema = z3.enum([
@@ -912,12 +1067,9 @@ var jsonSchema7Schema = z3.lazy(
912
1067
  );
913
1068
 
914
1069
  // src/validate/constraint-validator.ts
915
- function makeCode(ctx, category, number) {
916
- return `${ctx.vendorPrefix}-${category}-${String(number).padStart(3, "0")}`;
917
- }
918
1070
  function addContradiction(ctx, message, primary, related) {
919
1071
  ctx.diagnostics.push({
920
- code: makeCode(ctx, "CONTRADICTION", 1),
1072
+ code: "CONTRADICTING_CONSTRAINTS",
921
1073
  message,
922
1074
  severity: "error",
923
1075
  primaryLocation: primary,
@@ -926,7 +1078,7 @@ function addContradiction(ctx, message, primary, related) {
926
1078
  }
927
1079
  function addTypeMismatch(ctx, message, primary) {
928
1080
  ctx.diagnostics.push({
929
- code: makeCode(ctx, "TYPE_MISMATCH", 1),
1081
+ code: "TYPE_MISMATCH",
930
1082
  message,
931
1083
  severity: "error",
932
1084
  primaryLocation: primary,
@@ -935,13 +1087,31 @@ function addTypeMismatch(ctx, message, primary) {
935
1087
  }
936
1088
  function addUnknownExtension(ctx, message, primary) {
937
1089
  ctx.diagnostics.push({
938
- code: makeCode(ctx, "UNKNOWN_EXTENSION", 1),
1090
+ code: "UNKNOWN_EXTENSION",
939
1091
  message,
940
1092
  severity: "warning",
941
1093
  primaryLocation: primary,
942
1094
  relatedLocations: []
943
1095
  });
944
1096
  }
1097
+ function addUnknownPathTarget(ctx, message, primary) {
1098
+ ctx.diagnostics.push({
1099
+ code: "UNKNOWN_PATH_TARGET",
1100
+ message,
1101
+ severity: "error",
1102
+ primaryLocation: primary,
1103
+ relatedLocations: []
1104
+ });
1105
+ }
1106
+ function addConstraintBroadening(ctx, message, primary, related) {
1107
+ ctx.diagnostics.push({
1108
+ code: "CONSTRAINT_BROADENING",
1109
+ message,
1110
+ severity: "error",
1111
+ primaryLocation: primary,
1112
+ relatedLocations: [related]
1113
+ });
1114
+ }
945
1115
  function findNumeric(constraints, constraintKind) {
946
1116
  return constraints.find((c) => c.constraintKind === constraintKind);
947
1117
  }
@@ -953,6 +1123,165 @@ function findAllowedMembers(constraints) {
953
1123
  (c) => c.constraintKind === "allowedMembers"
954
1124
  );
955
1125
  }
1126
+ function findConstConstraints(constraints) {
1127
+ return constraints.filter(
1128
+ (c) => c.constraintKind === "const"
1129
+ );
1130
+ }
1131
+ function jsonValueEquals(left, right) {
1132
+ if (left === right) {
1133
+ return true;
1134
+ }
1135
+ if (Array.isArray(left) || Array.isArray(right)) {
1136
+ if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) {
1137
+ return false;
1138
+ }
1139
+ return left.every((item, index) => jsonValueEquals(item, right[index]));
1140
+ }
1141
+ if (isJsonObject(left) || isJsonObject(right)) {
1142
+ if (!isJsonObject(left) || !isJsonObject(right)) {
1143
+ return false;
1144
+ }
1145
+ const leftKeys = Object.keys(left).sort();
1146
+ const rightKeys = Object.keys(right).sort();
1147
+ if (leftKeys.length !== rightKeys.length) {
1148
+ return false;
1149
+ }
1150
+ return leftKeys.every((key, index) => {
1151
+ const rightKey = rightKeys[index];
1152
+ if (rightKey !== key) {
1153
+ return false;
1154
+ }
1155
+ const leftValue = left[key];
1156
+ const rightValue = right[rightKey];
1157
+ return leftValue !== void 0 && rightValue !== void 0 && jsonValueEquals(leftValue, rightValue);
1158
+ });
1159
+ }
1160
+ return false;
1161
+ }
1162
+ function isJsonObject(value) {
1163
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1164
+ }
1165
+ function isOrderedBoundConstraint(constraint) {
1166
+ 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";
1167
+ }
1168
+ function pathKey(constraint) {
1169
+ return constraint.path?.segments.join(".") ?? "";
1170
+ }
1171
+ function orderedBoundFamily(kind) {
1172
+ switch (kind) {
1173
+ case "minimum":
1174
+ case "exclusiveMinimum":
1175
+ return "numeric-lower";
1176
+ case "maximum":
1177
+ case "exclusiveMaximum":
1178
+ return "numeric-upper";
1179
+ case "minLength":
1180
+ return "minLength";
1181
+ case "minItems":
1182
+ return "minItems";
1183
+ case "maxLength":
1184
+ return "maxLength";
1185
+ case "maxItems":
1186
+ return "maxItems";
1187
+ default: {
1188
+ const _exhaustive = kind;
1189
+ return _exhaustive;
1190
+ }
1191
+ }
1192
+ }
1193
+ function isNumericLowerKind(kind) {
1194
+ return kind === "minimum" || kind === "exclusiveMinimum";
1195
+ }
1196
+ function isNumericUpperKind(kind) {
1197
+ return kind === "maximum" || kind === "exclusiveMaximum";
1198
+ }
1199
+ function describeConstraintTag(constraint) {
1200
+ return `@${constraint.constraintKind}`;
1201
+ }
1202
+ function compareConstraintStrength(current, previous) {
1203
+ const family = orderedBoundFamily(current.constraintKind);
1204
+ if (family === "numeric-lower") {
1205
+ if (!isNumericLowerKind(current.constraintKind) || !isNumericLowerKind(previous.constraintKind)) {
1206
+ throw new Error("numeric-lower family received non-numeric lower-bound constraint");
1207
+ }
1208
+ if (current.value !== previous.value) {
1209
+ return current.value > previous.value ? 1 : -1;
1210
+ }
1211
+ if (current.constraintKind === "exclusiveMinimum" && previous.constraintKind === "minimum") {
1212
+ return 1;
1213
+ }
1214
+ if (current.constraintKind === "minimum" && previous.constraintKind === "exclusiveMinimum") {
1215
+ return -1;
1216
+ }
1217
+ return 0;
1218
+ }
1219
+ if (family === "numeric-upper") {
1220
+ if (!isNumericUpperKind(current.constraintKind) || !isNumericUpperKind(previous.constraintKind)) {
1221
+ throw new Error("numeric-upper family received non-numeric upper-bound constraint");
1222
+ }
1223
+ if (current.value !== previous.value) {
1224
+ return current.value < previous.value ? 1 : -1;
1225
+ }
1226
+ if (current.constraintKind === "exclusiveMaximum" && previous.constraintKind === "maximum") {
1227
+ return 1;
1228
+ }
1229
+ if (current.constraintKind === "maximum" && previous.constraintKind === "exclusiveMaximum") {
1230
+ return -1;
1231
+ }
1232
+ return 0;
1233
+ }
1234
+ switch (family) {
1235
+ case "minLength":
1236
+ case "minItems":
1237
+ if (current.value === previous.value) {
1238
+ return 0;
1239
+ }
1240
+ return current.value > previous.value ? 1 : -1;
1241
+ case "maxLength":
1242
+ case "maxItems":
1243
+ if (current.value === previous.value) {
1244
+ return 0;
1245
+ }
1246
+ return current.value < previous.value ? 1 : -1;
1247
+ default: {
1248
+ const _exhaustive = family;
1249
+ return _exhaustive;
1250
+ }
1251
+ }
1252
+ }
1253
+ function checkConstraintBroadening(ctx, fieldName, constraints) {
1254
+ const strongestByKey = /* @__PURE__ */ new Map();
1255
+ for (const constraint of constraints) {
1256
+ if (!isOrderedBoundConstraint(constraint)) {
1257
+ continue;
1258
+ }
1259
+ const key = `${orderedBoundFamily(constraint.constraintKind)}:${pathKey(constraint)}`;
1260
+ const previous = strongestByKey.get(key);
1261
+ if (previous === void 0) {
1262
+ strongestByKey.set(key, constraint);
1263
+ continue;
1264
+ }
1265
+ const strength = compareConstraintStrength(constraint, previous);
1266
+ if (strength < 0) {
1267
+ const displayFieldName = formatPathTargetFieldName(
1268
+ fieldName,
1269
+ constraint.path?.segments ?? []
1270
+ );
1271
+ addConstraintBroadening(
1272
+ ctx,
1273
+ `Field "${displayFieldName}": ${describeConstraintTag(constraint)} (${String(constraint.value)}) is broader than earlier ${describeConstraintTag(previous)} (${String(previous.value)}). Constraints can only narrow.`,
1274
+ constraint.provenance,
1275
+ previous.provenance
1276
+ );
1277
+ continue;
1278
+ }
1279
+ if (strength <= 0) {
1280
+ continue;
1281
+ }
1282
+ strongestByKey.set(key, constraint);
1283
+ }
1284
+ }
956
1285
  function checkNumericContradictions(ctx, fieldName, constraints) {
957
1286
  const min = findNumeric(constraints, "minimum");
958
1287
  const max = findNumeric(constraints, "maximum");
@@ -1039,6 +1368,25 @@ function checkAllowedMembersContradiction(ctx, fieldName, constraints) {
1039
1368
  }
1040
1369
  }
1041
1370
  }
1371
+ function checkConstContradictions(ctx, fieldName, constraints) {
1372
+ const constConstraints = findConstConstraints(constraints);
1373
+ if (constConstraints.length < 2) return;
1374
+ const first = constConstraints[0];
1375
+ if (first === void 0) return;
1376
+ for (let i = 1; i < constConstraints.length; i++) {
1377
+ const current = constConstraints[i];
1378
+ if (current === void 0) continue;
1379
+ if (jsonValueEquals(first.value, current.value)) {
1380
+ continue;
1381
+ }
1382
+ addContradiction(
1383
+ ctx,
1384
+ `Field "${fieldName}": conflicting @const constraints require both ${JSON.stringify(first.value)} and ${JSON.stringify(current.value)}`,
1385
+ first.provenance,
1386
+ current.provenance
1387
+ );
1388
+ }
1389
+ }
1042
1390
  function typeLabel(type) {
1043
1391
  switch (type.kind) {
1044
1392
  case "primitive":
@@ -1049,6 +1397,8 @@ function typeLabel(type) {
1049
1397
  return "array";
1050
1398
  case "object":
1051
1399
  return "object";
1400
+ case "record":
1401
+ return "record";
1052
1402
  case "union":
1053
1403
  return "union";
1054
1404
  case "reference":
@@ -1063,85 +1413,173 @@ function typeLabel(type) {
1063
1413
  }
1064
1414
  }
1065
1415
  }
1066
- function checkTypeApplicability(ctx, fieldName, type, constraints) {
1067
- const isNumber = type.kind === "primitive" && type.primitiveKind === "number";
1068
- const isString = type.kind === "primitive" && type.primitiveKind === "string";
1069
- const isArray = type.kind === "array";
1070
- const isEnum = type.kind === "enum";
1071
- const label = typeLabel(type);
1072
- for (const constraint of constraints) {
1073
- if (constraint.path) {
1074
- const isTraversable = type.kind === "object" || type.kind === "array" || type.kind === "reference";
1075
- if (!isTraversable) {
1416
+ function dereferenceType(ctx, type) {
1417
+ let current = type;
1418
+ const seen = /* @__PURE__ */ new Set();
1419
+ while (current.kind === "reference") {
1420
+ if (seen.has(current.name)) {
1421
+ return current;
1422
+ }
1423
+ seen.add(current.name);
1424
+ const definition = ctx.typeRegistry[current.name];
1425
+ if (definition === void 0) {
1426
+ return current;
1427
+ }
1428
+ current = definition.type;
1429
+ }
1430
+ return current;
1431
+ }
1432
+ function resolvePathTargetType(ctx, type, segments) {
1433
+ const effectiveType = dereferenceType(ctx, type);
1434
+ if (segments.length === 0) {
1435
+ return { kind: "resolved", type: effectiveType };
1436
+ }
1437
+ if (effectiveType.kind === "array") {
1438
+ return resolvePathTargetType(ctx, effectiveType.items, segments);
1439
+ }
1440
+ if (effectiveType.kind === "object") {
1441
+ const [segment, ...rest] = segments;
1442
+ if (segment === void 0) {
1443
+ throw new Error("Invariant violation: object path traversal requires a segment");
1444
+ }
1445
+ const property = effectiveType.properties.find((prop) => prop.name === segment);
1446
+ if (property === void 0) {
1447
+ return { kind: "missing-property", segment };
1448
+ }
1449
+ return resolvePathTargetType(ctx, property.type, rest);
1450
+ }
1451
+ return { kind: "unresolvable", type: effectiveType };
1452
+ }
1453
+ function formatPathTargetFieldName(fieldName, path) {
1454
+ return path.length === 0 ? fieldName : `${fieldName}.${path.join(".")}`;
1455
+ }
1456
+ function checkConstraintOnType(ctx, fieldName, type, constraint) {
1457
+ const effectiveType = dereferenceType(ctx, type);
1458
+ const isNumber = effectiveType.kind === "primitive" && effectiveType.primitiveKind === "number";
1459
+ const isString = effectiveType.kind === "primitive" && effectiveType.primitiveKind === "string";
1460
+ const isArray = effectiveType.kind === "array";
1461
+ const isEnum = effectiveType.kind === "enum";
1462
+ const arrayItemType = effectiveType.kind === "array" ? dereferenceType(ctx, effectiveType.items) : void 0;
1463
+ const isStringArray = arrayItemType?.kind === "primitive" && arrayItemType.primitiveKind === "string";
1464
+ const label = typeLabel(effectiveType);
1465
+ const ck = constraint.constraintKind;
1466
+ switch (ck) {
1467
+ case "minimum":
1468
+ case "maximum":
1469
+ case "exclusiveMinimum":
1470
+ case "exclusiveMaximum":
1471
+ case "multipleOf": {
1472
+ if (!isNumber) {
1076
1473
  addTypeMismatch(
1077
1474
  ctx,
1078
- `Field "${fieldName}": path-targeted constraint "${constraint.constraintKind}" is invalid because type "${label}" cannot be traversed`,
1475
+ `Field "${fieldName}": constraint "${ck}" is only valid on number fields, but field type is "${label}"`,
1079
1476
  constraint.provenance
1080
1477
  );
1081
1478
  }
1082
- continue;
1479
+ break;
1083
1480
  }
1084
- const ck = constraint.constraintKind;
1085
- switch (ck) {
1086
- case "minimum":
1087
- case "maximum":
1088
- case "exclusiveMinimum":
1089
- case "exclusiveMaximum":
1090
- case "multipleOf": {
1091
- if (!isNumber) {
1092
- addTypeMismatch(
1093
- ctx,
1094
- `Field "${fieldName}": constraint "${ck}" is only valid on number fields, but field type is "${label}"`,
1095
- constraint.provenance
1096
- );
1097
- }
1098
- break;
1481
+ case "minLength":
1482
+ case "maxLength":
1483
+ case "pattern": {
1484
+ if (!isString && !isStringArray) {
1485
+ addTypeMismatch(
1486
+ ctx,
1487
+ `Field "${fieldName}": constraint "${ck}" is only valid on string fields or string array items, but field type is "${label}"`,
1488
+ constraint.provenance
1489
+ );
1099
1490
  }
1100
- case "minLength":
1101
- case "maxLength":
1102
- case "pattern": {
1103
- if (!isString) {
1104
- addTypeMismatch(
1105
- ctx,
1106
- `Field "${fieldName}": constraint "${ck}" is only valid on string fields, but field type is "${label}"`,
1107
- constraint.provenance
1108
- );
1109
- }
1110
- break;
1491
+ break;
1492
+ }
1493
+ case "minItems":
1494
+ case "maxItems":
1495
+ case "uniqueItems": {
1496
+ if (!isArray) {
1497
+ addTypeMismatch(
1498
+ ctx,
1499
+ `Field "${fieldName}": constraint "${ck}" is only valid on array fields, but field type is "${label}"`,
1500
+ constraint.provenance
1501
+ );
1111
1502
  }
1112
- case "minItems":
1113
- case "maxItems":
1114
- case "uniqueItems": {
1115
- if (!isArray) {
1116
- addTypeMismatch(
1117
- ctx,
1118
- `Field "${fieldName}": constraint "${ck}" is only valid on array fields, but field type is "${label}"`,
1119
- constraint.provenance
1120
- );
1121
- }
1503
+ break;
1504
+ }
1505
+ case "allowedMembers": {
1506
+ if (!isEnum) {
1507
+ addTypeMismatch(
1508
+ ctx,
1509
+ `Field "${fieldName}": constraint "allowedMembers" is only valid on enum fields, but field type is "${label}"`,
1510
+ constraint.provenance
1511
+ );
1512
+ }
1513
+ break;
1514
+ }
1515
+ case "const": {
1516
+ const isPrimitiveConstType = effectiveType.kind === "primitive" && ["string", "number", "boolean", "null"].includes(effectiveType.primitiveKind) || effectiveType.kind === "enum";
1517
+ if (!isPrimitiveConstType) {
1518
+ addTypeMismatch(
1519
+ ctx,
1520
+ `Field "${fieldName}": constraint "const" is only valid on primitive or enum fields, but field type is "${label}"`,
1521
+ constraint.provenance
1522
+ );
1122
1523
  break;
1123
1524
  }
1124
- case "allowedMembers": {
1125
- if (!isEnum) {
1525
+ if (effectiveType.kind === "primitive") {
1526
+ const valueType = constraint.value === null ? "null" : Array.isArray(constraint.value) ? "array" : typeof constraint.value;
1527
+ if (valueType !== effectiveType.primitiveKind) {
1126
1528
  addTypeMismatch(
1127
1529
  ctx,
1128
- `Field "${fieldName}": constraint "allowedMembers" is only valid on enum fields, but field type is "${label}"`,
1530
+ `Field "${fieldName}": @const value type "${valueType}" is incompatible with field type "${effectiveType.primitiveKind}"`,
1129
1531
  constraint.provenance
1130
1532
  );
1131
1533
  }
1132
1534
  break;
1133
1535
  }
1134
- case "custom": {
1135
- checkCustomConstraint(ctx, fieldName, type, constraint);
1136
- break;
1536
+ const memberValues = effectiveType.members.map((member) => member.value);
1537
+ if (!memberValues.some((member) => jsonValueEquals(member, constraint.value))) {
1538
+ addTypeMismatch(
1539
+ ctx,
1540
+ `Field "${fieldName}": @const value ${JSON.stringify(constraint.value)} is not one of the enum members`,
1541
+ constraint.provenance
1542
+ );
1137
1543
  }
1138
- default: {
1139
- const _exhaustive = constraint;
1140
- throw new Error(
1141
- `Unhandled constraint kind: ${_exhaustive.constraintKind}`
1544
+ break;
1545
+ }
1546
+ case "custom": {
1547
+ checkCustomConstraint(ctx, fieldName, effectiveType, constraint);
1548
+ break;
1549
+ }
1550
+ default: {
1551
+ const _exhaustive = constraint;
1552
+ throw new Error(
1553
+ `Unhandled constraint kind: ${_exhaustive.constraintKind}`
1554
+ );
1555
+ }
1556
+ }
1557
+ }
1558
+ function checkTypeApplicability(ctx, fieldName, type, constraints) {
1559
+ for (const constraint of constraints) {
1560
+ if (constraint.path) {
1561
+ const resolution = resolvePathTargetType(ctx, type, constraint.path.segments);
1562
+ const targetFieldName = formatPathTargetFieldName(fieldName, constraint.path.segments);
1563
+ if (resolution.kind === "missing-property") {
1564
+ addUnknownPathTarget(
1565
+ ctx,
1566
+ `Field "${targetFieldName}": path-targeted constraint "${constraint.constraintKind}" references unknown path segment "${resolution.segment}"`,
1567
+ constraint.provenance
1142
1568
  );
1569
+ continue;
1143
1570
  }
1571
+ if (resolution.kind === "unresolvable") {
1572
+ addTypeMismatch(
1573
+ ctx,
1574
+ `Field "${targetFieldName}": path-targeted constraint "${constraint.constraintKind}" is invalid because type "${typeLabel(resolution.type)}" cannot be traversed`,
1575
+ constraint.provenance
1576
+ );
1577
+ continue;
1578
+ }
1579
+ checkConstraintOnType(ctx, targetFieldName, resolution.type, constraint);
1580
+ continue;
1144
1581
  }
1582
+ checkConstraintOnType(ctx, fieldName, type, constraint);
1145
1583
  }
1146
1584
  }
1147
1585
  function checkCustomConstraint(ctx, fieldName, type, constraint) {
@@ -1185,6 +1623,8 @@ function validateConstraints(ctx, name, type, constraints) {
1185
1623
  checkNumericContradictions(ctx, name, constraints);
1186
1624
  checkLengthContradictions(ctx, name, constraints);
1187
1625
  checkAllowedMembersContradiction(ctx, name, constraints);
1626
+ checkConstContradictions(ctx, name, constraints);
1627
+ checkConstraintBroadening(ctx, name, constraints);
1188
1628
  checkTypeApplicability(ctx, name, type, constraints);
1189
1629
  }
1190
1630
  function validateElement(ctx, element) {
@@ -1211,8 +1651,8 @@ function validateElement(ctx, element) {
1211
1651
  function validateIR(ir, options) {
1212
1652
  const ctx = {
1213
1653
  diagnostics: [],
1214
- vendorPrefix: options?.vendorPrefix ?? "FORMSPEC",
1215
- extensionRegistry: options?.extensionRegistry
1654
+ extensionRegistry: options?.extensionRegistry,
1655
+ typeRegistry: ir.typeRegistry
1216
1656
  };
1217
1657
  for (const element of ir.elements) {
1218
1658
  validateElement(ctx, element);
@@ -1224,9 +1664,9 @@ function validateIR(ir, options) {
1224
1664
  }
1225
1665
 
1226
1666
  // src/browser.ts
1227
- function buildFormSchemas(form) {
1667
+ function buildFormSchemas(form, options) {
1228
1668
  return {
1229
- jsonSchema: generateJsonSchema(form),
1669
+ jsonSchema: generateJsonSchema(form, options),
1230
1670
  uiSchema: generateUiSchema(form)
1231
1671
  };
1232
1672
  }
@@ -1236,7 +1676,9 @@ export {
1236
1676
  categorizationSchema,
1237
1677
  categorySchema,
1238
1678
  controlSchema,
1679
+ createExtensionRegistry,
1239
1680
  generateJsonSchema,
1681
+ generateJsonSchemaFromIR,
1240
1682
  generateUiSchema,
1241
1683
  getSchemaExtension,
1242
1684
  groupLayoutSchema,