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

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 (65) hide show
  1. package/README.md +74 -128
  2. package/dist/__tests__/class-schema.test.d.ts +2 -0
  3. package/dist/__tests__/class-schema.test.d.ts.map +1 -0
  4. package/dist/__tests__/date-extension.integration.test.d.ts +2 -0
  5. package/dist/__tests__/date-extension.integration.test.d.ts.map +1 -0
  6. package/dist/__tests__/fixtures/class-schema-regressions.d.ts +83 -0
  7. package/dist/__tests__/fixtures/class-schema-regressions.d.ts.map +1 -0
  8. package/dist/__tests__/fixtures/example-date-extension.d.ts +12 -0
  9. package/dist/__tests__/fixtures/example-date-extension.d.ts.map +1 -0
  10. package/dist/__tests__/fixtures/example-numeric-extension.d.ts +20 -0
  11. package/dist/__tests__/fixtures/example-numeric-extension.d.ts.map +1 -0
  12. package/dist/__tests__/fixtures/extension-forms.d.ts +7 -0
  13. package/dist/__tests__/fixtures/extension-forms.d.ts.map +1 -0
  14. package/dist/__tests__/fixtures/mixed-authoring-shipping-address.d.ts +1 -0
  15. package/dist/__tests__/fixtures/mixed-authoring-shipping-address.d.ts.map +1 -1
  16. package/dist/__tests__/fixtures/named-primitive-aliases.d.ts +15 -0
  17. package/dist/__tests__/fixtures/named-primitive-aliases.d.ts.map +1 -0
  18. package/dist/__tests__/fixtures/nested-array-path-constraints.d.ts +14 -0
  19. package/dist/__tests__/fixtures/nested-array-path-constraints.d.ts.map +1 -0
  20. package/dist/__tests__/fixtures/sample-forms.d.ts +10 -0
  21. package/dist/__tests__/fixtures/sample-forms.d.ts.map +1 -1
  22. package/dist/__tests__/generate-schemas.test.d.ts +2 -0
  23. package/dist/__tests__/generate-schemas.test.d.ts.map +1 -0
  24. package/dist/__tests__/numeric-extension.integration.test.d.ts +2 -0
  25. package/dist/__tests__/numeric-extension.integration.test.d.ts.map +1 -0
  26. package/dist/__tests__/parity/parity.test.d.ts +6 -2
  27. package/dist/__tests__/parity/parity.test.d.ts.map +1 -1
  28. package/dist/__tests__/parity/utils.d.ts +9 -4
  29. package/dist/__tests__/parity/utils.d.ts.map +1 -1
  30. package/dist/analyzer/class-analyzer.d.ts +5 -4
  31. package/dist/analyzer/class-analyzer.d.ts.map +1 -1
  32. package/dist/analyzer/jsdoc-constraints.d.ts +3 -2
  33. package/dist/analyzer/jsdoc-constraints.d.ts.map +1 -1
  34. package/dist/analyzer/program.d.ts +15 -0
  35. package/dist/analyzer/program.d.ts.map +1 -1
  36. package/dist/analyzer/tsdoc-parser.d.ts +23 -2
  37. package/dist/analyzer/tsdoc-parser.d.ts.map +1 -1
  38. package/dist/browser.cjs +269 -11
  39. package/dist/browser.cjs.map +1 -1
  40. package/dist/browser.js +269 -11
  41. package/dist/browser.js.map +1 -1
  42. package/dist/build.d.ts +28 -2
  43. package/dist/canonicalize/chain-dsl-canonicalizer.d.ts.map +1 -1
  44. package/dist/canonicalize/tsdoc-canonicalizer.d.ts.map +1 -1
  45. package/dist/cli.cjs +1640 -282
  46. package/dist/cli.cjs.map +1 -1
  47. package/dist/cli.js +1638 -281
  48. package/dist/cli.js.map +1 -1
  49. package/dist/extensions/registry.d.ts +25 -1
  50. package/dist/extensions/registry.d.ts.map +1 -1
  51. package/dist/generators/class-schema.d.ts +4 -4
  52. package/dist/generators/class-schema.d.ts.map +1 -1
  53. package/dist/generators/method-schema.d.ts.map +1 -1
  54. package/dist/generators/mixed-authoring.d.ts.map +1 -1
  55. package/dist/index.cjs +1615 -271
  56. package/dist/index.cjs.map +1 -1
  57. package/dist/index.js +1615 -271
  58. package/dist/index.js.map +1 -1
  59. package/dist/internals.cjs +990 -236
  60. package/dist/internals.cjs.map +1 -1
  61. package/dist/internals.js +988 -234
  62. package/dist/internals.js.map +1 -1
  63. package/dist/json-schema/ir-generator.d.ts.map +1 -1
  64. package/dist/validate/constraint-validator.d.ts.map +1 -1
  65. package/package.json +3 -3
@@ -14,6 +14,7 @@
14
14
  */
15
15
  import * as ts from "typescript";
16
16
  import type { ConstraintNode, AnnotationNode } from "@formspec/core";
17
+ import { type ParseTSDocOptions } from "./tsdoc-parser.js";
17
18
  /**
18
19
  * Extracts constraints from JSDoc comments on a TypeScript AST node and returns
19
20
  * canonical {@link ConstraintNode} objects.
@@ -25,7 +26,7 @@ import type { ConstraintNode, AnnotationNode } from "@formspec/core";
25
26
  * @param file - Absolute path to the source file for provenance
26
27
  * @returns Canonical constraint nodes for each valid constraint tag
27
28
  */
28
- export declare function extractJSDocConstraintNodes(node: ts.Node, file?: string): ConstraintNode[];
29
+ export declare function extractJSDocConstraintNodes(node: ts.Node, file?: string, options?: ParseTSDocOptions): ConstraintNode[];
29
30
  /**
30
31
  * Extracts canonical annotation tags from a node and returns
31
32
  * {@link AnnotationNode} objects.
@@ -34,7 +35,7 @@ export declare function extractJSDocConstraintNodes(node: ts.Node, file?: string
34
35
  * @param file - Absolute path to the source file for provenance
35
36
  * @returns Canonical annotation nodes
36
37
  */
37
- export declare function extractJSDocAnnotationNodes(node: ts.Node, file?: string): AnnotationNode[];
38
+ export declare function extractJSDocAnnotationNodes(node: ts.Node, file?: string, options?: ParseTSDocOptions): AnnotationNode[];
38
39
  /**
39
40
  * Checks if a node has a TSDoc `@deprecated` tag.
40
41
  *
@@ -1 +1 @@
1
- {"version":3,"file":"jsdoc-constraints.d.ts","sourceRoot":"","sources":["../../src/analyzer/jsdoc-constraints.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,MAAM,YAAY,CAAC;AACjC,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAa,MAAM,gBAAgB,CAAC;AAOhF;;;;;;;;;;GAUG;AACH,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,IAAI,SAAK,GAAG,cAAc,EAAE,CAGtF;AAED;;;;;;;GAOG;AACH,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,IAAI,SAAK,GAAG,cAAc,EAAE,CAGtF;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,GAAG,OAAO,CAEvD;AAED;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAC3C,WAAW,EAAE,EAAE,CAAC,UAAU,GAAG,SAAS,EACtC,IAAI,SAAK,GACR,cAAc,GAAG,IAAI,CAwCvB"}
1
+ {"version":3,"file":"jsdoc-constraints.d.ts","sourceRoot":"","sources":["../../src/analyzer/jsdoc-constraints.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,MAAM,YAAY,CAAC;AACjC,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAa,MAAM,gBAAgB,CAAC;AAChF,OAAO,EAAyC,KAAK,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAMlG;;;;;;;;;;GAUG;AACH,wBAAgB,2BAA2B,CACzC,IAAI,EAAE,EAAE,CAAC,IAAI,EACb,IAAI,SAAK,EACT,OAAO,CAAC,EAAE,iBAAiB,GAC1B,cAAc,EAAE,CAGlB;AAED;;;;;;;GAOG;AACH,wBAAgB,2BAA2B,CACzC,IAAI,EAAE,EAAE,CAAC,IAAI,EACb,IAAI,SAAK,EACT,OAAO,CAAC,EAAE,iBAAiB,GAC1B,cAAc,EAAE,CAGlB;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,GAAG,OAAO,CAEvD;AAED;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAC3C,WAAW,EAAE,EAAE,CAAC,UAAU,GAAG,SAAS,EACtC,IAAI,SAAK,GACR,cAAc,GAAG,IAAI,CAwCvB"}
@@ -5,6 +5,8 @@
5
5
  * using the project's tsconfig.json for compiler options.
6
6
  */
7
7
  import * as ts from "typescript";
8
+ import { type IRClassAnalysis } from "./class-analyzer.js";
9
+ import type { ExtensionRegistry } from "../extensions/index.js";
8
10
  /**
9
11
  * Result of creating a TypeScript program for analysis.
10
12
  */
@@ -50,4 +52,17 @@ export declare function findInterfaceByName(sourceFile: ts.SourceFile, interface
50
52
  * @returns The type alias declaration node, or null if not found
51
53
  */
52
54
  export declare function findTypeAliasByName(sourceFile: ts.SourceFile, aliasName: string): ts.TypeAliasDeclaration | null;
55
+ /**
56
+ * Analyzes a named type (class, interface, or type alias) from a TypeScript
57
+ * source file and returns an `IRClassAnalysis`.
58
+ *
59
+ * Tries each declaration kind in order: class → interface → type alias.
60
+ * Throws if the name is not found or if the type alias analysis fails.
61
+ *
62
+ * @param filePath - Absolute or relative path to the TypeScript source file (resolved internally)
63
+ * @param typeName - Name of the class, interface, or type alias to analyze
64
+ * @param extensionRegistry - Optional extension registry for custom type handling
65
+ * @returns IR analysis result
66
+ */
67
+ export declare function analyzeNamedTypeToIR(filePath: string, typeName: string, extensionRegistry?: ExtensionRegistry): IRClassAnalysis;
53
68
  //# sourceMappingURL=program.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"program.d.ts","sourceRoot":"","sources":["../../src/analyzer/program.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,MAAM,YAAY,CAAC;AAGjC;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,6BAA6B;IAC7B,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC;IACpB,uCAAuC;IACvC,OAAO,EAAE,EAAE,CAAC,WAAW,CAAC;IACxB,qCAAqC;IACrC,UAAU,EAAE,EAAE,CAAC,UAAU,CAAC;CAC3B;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,cAAc,CA6DrE;AA6BD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,UAAU,EAAE,EAAE,CAAC,UAAU,EACzB,SAAS,EAAE,MAAM,GAChB,EAAE,CAAC,gBAAgB,GAAG,IAAI,CAE5B;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,EAAE,CAAC,UAAU,EACzB,aAAa,EAAE,MAAM,GACpB,EAAE,CAAC,oBAAoB,GAAG,IAAI,CAEhC;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,EAAE,CAAC,UAAU,EACzB,SAAS,EAAE,MAAM,GAChB,EAAE,CAAC,oBAAoB,GAAG,IAAI,CAEhC"}
1
+ {"version":3,"file":"program.d.ts","sourceRoot":"","sources":["../../src/analyzer/program.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,MAAM,YAAY,CAAC;AAEjC,OAAO,EAIL,KAAK,eAAe,EACrB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAEhE;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,6BAA6B;IAC7B,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC;IACpB,uCAAuC;IACvC,OAAO,EAAE,EAAE,CAAC,WAAW,CAAC;IACxB,qCAAqC;IACrC,UAAU,EAAE,EAAE,CAAC,UAAU,CAAC;CAC3B;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,cAAc,CA6DrE;AA6BD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,UAAU,EAAE,EAAE,CAAC,UAAU,EACzB,SAAS,EAAE,MAAM,GAChB,EAAE,CAAC,gBAAgB,GAAG,IAAI,CAE5B;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,EAAE,CAAC,UAAU,EACzB,aAAa,EAAE,MAAM,GACpB,EAAE,CAAC,oBAAoB,GAAG,IAAI,CAEhC;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,EAAE,CAAC,UAAU,EACzB,SAAS,EAAE,MAAM,GAChB,EAAE,CAAC,oBAAoB,GAAG,IAAI,CAEhC;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,iBAAiB,CAAC,EAAE,iBAAiB,GACpC,eAAe,CAyBjB"}
@@ -20,6 +20,11 @@
20
20
  *
21
21
  * The `@deprecated` tag is a standard TSDoc block tag, parsed structurally.
22
22
  *
23
+ * Description precedence is:
24
+ * 1. Explicit `@description`
25
+ * 2. Explicit `@remarks`
26
+ * 3. Implicit summary text (free text in the comment block)
27
+ *
23
28
  * **Fallback strategy**: TSDoc treats `{` / `}` as inline tag delimiters and
24
29
  * `@` as a tag prefix, so content containing these characters (e.g. JSON
25
30
  * objects in `@EnumOptions`, regex patterns with `@` in `@Pattern`) gets
@@ -28,7 +33,8 @@
28
33
  * verbatim.
29
34
  */
30
35
  import * as ts from "typescript";
31
- import { type ConstraintNode, type AnnotationNode, type PathTarget } from "@formspec/core";
36
+ import { type ConstraintNode, type AnnotationNode, type PathTarget, type TypeNode } from "@formspec/core";
37
+ import type { ExtensionRegistry } from "../extensions/index.js";
32
38
  /**
33
39
  * Result of parsing a single JSDoc comment attached to a TS AST node.
34
40
  */
@@ -38,6 +44,21 @@ export interface TSDocParseResult {
38
44
  /** Annotation IR nodes extracted from canonical TSDoc block tags. */
39
45
  readonly annotations: readonly AnnotationNode[];
40
46
  }
47
+ /**
48
+ * Optional extension-aware parsing inputs for TSDoc extraction.
49
+ */
50
+ export interface ParseTSDocOptions {
51
+ /**
52
+ * Extension registry used to resolve custom tags and custom-type-specific
53
+ * broadening of built-in constraint tags.
54
+ */
55
+ readonly extensionRegistry?: ExtensionRegistry;
56
+ /**
57
+ * Effective field/type node for the declaration being parsed. Required when
58
+ * built-in tags may broaden onto a custom type.
59
+ */
60
+ readonly fieldType?: TypeNode;
61
+ }
41
62
  /**
42
63
  * Display-name metadata extracted from a node's JSDoc tags.
43
64
  *
@@ -62,7 +83,7 @@ export interface DisplayNameMetadata {
62
83
  * @param file - Absolute source file path for provenance
63
84
  * @returns Parsed constraint and annotation nodes
64
85
  */
65
- export declare function parseTSDocTags(node: ts.Node, file?: string): TSDocParseResult;
86
+ export declare function parseTSDocTags(node: ts.Node, file?: string, options?: ParseTSDocOptions): TSDocParseResult;
66
87
  /**
67
88
  * Checks if a TS AST node has a `@deprecated` tag using the TSDoc parser.
68
89
  *
@@ -1 +1 @@
1
- {"version":3,"file":"tsdoc-parser.d.ts","sourceRoot":"","sources":["../../src/analyzer/tsdoc-parser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,KAAK,EAAE,MAAM,YAAY,CAAC;AAYjC,OAAO,EAIL,KAAK,cAAc,EACnB,KAAK,cAAc,EAInB,KAAK,UAAU,EAEhB,MAAM,gBAAgB,CAAC;AAyFxB;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,4DAA4D;IAC5D,QAAQ,CAAC,WAAW,EAAE,SAAS,cAAc,EAAE,CAAC;IAChD,qEAAqE;IACrE,QAAQ,CAAC,WAAW,EAAE,SAAS,cAAc,EAAE,CAAC;CACjD;AAED;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,kBAAkB,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC1D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,IAAI,SAAK,GAAG,gBAAgB,CAiKzE;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,GAAG,OAAO,CAsB5D;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,GAAG,mBAAmB,CA2B7E;AAMD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,GACX;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAQpD"}
1
+ {"version":3,"file":"tsdoc-parser.d.ts","sourceRoot":"","sources":["../../src/analyzer/tsdoc-parser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,KAAK,EAAE,MAAM,YAAY,CAAC;AAYjC,OAAO,EAIL,KAAK,cAAc,EACnB,KAAK,cAAc,EAInB,KAAK,UAAU,EAEf,KAAK,QAAQ,EACd,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AA+GhE;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,4DAA4D;IAC5D,QAAQ,CAAC,WAAW,EAAE,SAAS,cAAc,EAAE,CAAC;IAChD,qEAAqE;IACrE,QAAQ,CAAC,WAAW,EAAE,SAAS,cAAc,EAAE,CAAC;CACjD;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;IAC/C;;;OAGG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,QAAQ,CAAC;CAC/B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,kBAAkB,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC1D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,EAAE,CAAC,IAAI,EACb,IAAI,SAAK,EACT,OAAO,CAAC,EAAE,iBAAiB,GAC1B,gBAAgB,CAiLlB;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,GAAG,OAAO,CAsB5D;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,GAAG,mBAAmB,CA2B7E;AAMD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,GACX;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAQpD"}
package/dist/browser.cjs CHANGED
@@ -70,6 +70,7 @@ function canonicalizeChainDSL(form) {
70
70
  kind: "form-ir",
71
71
  irVersion: import_core.IR_VERSION,
72
72
  elements: canonicalizeElements(form.elements),
73
+ rootAnnotations: [],
73
74
  typeRegistry: {},
74
75
  provenance: CHAIN_DSL_PROVENANCE
75
76
  };
@@ -380,6 +381,9 @@ function generateJsonSchemaFromIR(ir, options) {
380
381
  const ctx = makeContext(options);
381
382
  for (const [name, typeDef] of Object.entries(ir.typeRegistry)) {
382
383
  ctx.defs[name] = generateTypeNode(typeDef.type, ctx);
384
+ if (typeDef.constraints && typeDef.constraints.length > 0) {
385
+ applyConstraints(ctx.defs[name], typeDef.constraints, ctx);
386
+ }
383
387
  if (typeDef.annotations && typeDef.annotations.length > 0) {
384
388
  applyAnnotations(ctx.defs[name], typeDef.annotations, ctx);
385
389
  }
@@ -548,7 +552,9 @@ function generateTypeNode(type, ctx) {
548
552
  }
549
553
  }
550
554
  function generatePrimitiveType(type) {
551
- return { type: type.primitiveKind };
555
+ return {
556
+ type: type.primitiveKind === "integer" || type.primitiveKind === "bigint" ? "integer" : type.primitiveKind
557
+ };
552
558
  }
553
559
  function generateEnumType(type) {
554
560
  const hasDisplayNames = type.members.some((m) => m.displayName !== void 0);
@@ -721,7 +727,7 @@ function applyAnnotations(schema, annotations, ctx) {
721
727
  case "deprecated":
722
728
  schema.deprecated = true;
723
729
  if (annotation.message !== void 0 && annotation.message !== "") {
724
- schema["x-formspec-deprecation-description"] = annotation.message;
730
+ schema[`${ctx.vendorPrefix}-deprecation-description`] = annotation.message;
725
731
  }
726
732
  break;
727
733
  case "placeholder":
@@ -754,7 +760,12 @@ function applyCustomConstraint(schema, constraint, ctx) {
754
760
  `Cannot generate JSON Schema for custom constraint "${constraint.constraintId}" without a matching extension registration`
755
761
  );
756
762
  }
757
- Object.assign(schema, registration.toJsonSchema(constraint.payload, ctx.vendorPrefix));
763
+ assignVendorPrefixedExtensionKeywords(
764
+ schema,
765
+ registration.toJsonSchema(constraint.payload, ctx.vendorPrefix),
766
+ ctx.vendorPrefix,
767
+ `custom constraint "${constraint.constraintId}"`
768
+ );
758
769
  }
759
770
  function applyCustomAnnotation(schema, annotation, ctx) {
760
771
  const registration = ctx.extensionRegistry?.findAnnotation(annotation.annotationId);
@@ -766,7 +777,22 @@ function applyCustomAnnotation(schema, annotation, ctx) {
766
777
  if (registration.toJsonSchema === void 0) {
767
778
  return;
768
779
  }
769
- Object.assign(schema, registration.toJsonSchema(annotation.value, ctx.vendorPrefix));
780
+ assignVendorPrefixedExtensionKeywords(
781
+ schema,
782
+ registration.toJsonSchema(annotation.value, ctx.vendorPrefix),
783
+ ctx.vendorPrefix,
784
+ `custom annotation "${annotation.annotationId}"`
785
+ );
786
+ }
787
+ function assignVendorPrefixedExtensionKeywords(schema, extensionSchema, vendorPrefix, source) {
788
+ for (const [key, value] of Object.entries(extensionSchema)) {
789
+ if (!key.startsWith(`${vendorPrefix}-`)) {
790
+ throw new Error(
791
+ `Cannot apply ${source}: extension hooks may only emit "${vendorPrefix}-*" JSON Schema keywords`
792
+ );
793
+ }
794
+ schema[key] = value;
795
+ }
770
796
  }
771
797
 
772
798
  // src/json-schema/generator.ts
@@ -1015,7 +1041,10 @@ function getSchemaExtension(schema, key) {
1015
1041
  // src/extensions/registry.ts
1016
1042
  function createExtensionRegistry(extensions) {
1017
1043
  const typeMap = /* @__PURE__ */ new Map();
1044
+ const typeNameMap = /* @__PURE__ */ new Map();
1018
1045
  const constraintMap = /* @__PURE__ */ new Map();
1046
+ const constraintTagMap = /* @__PURE__ */ new Map();
1047
+ const builtinBroadeningMap = /* @__PURE__ */ new Map();
1019
1048
  const annotationMap = /* @__PURE__ */ new Map();
1020
1049
  for (const ext of extensions) {
1021
1050
  if (ext.types !== void 0) {
@@ -1025,6 +1054,27 @@ function createExtensionRegistry(extensions) {
1025
1054
  throw new Error(`Duplicate custom type ID: "${qualifiedId}"`);
1026
1055
  }
1027
1056
  typeMap.set(qualifiedId, type);
1057
+ for (const sourceTypeName of type.tsTypeNames ?? [type.typeName]) {
1058
+ if (typeNameMap.has(sourceTypeName)) {
1059
+ throw new Error(`Duplicate custom type source name: "${sourceTypeName}"`);
1060
+ }
1061
+ typeNameMap.set(sourceTypeName, {
1062
+ extensionId: ext.extensionId,
1063
+ registration: type
1064
+ });
1065
+ }
1066
+ if (type.builtinConstraintBroadenings !== void 0) {
1067
+ for (const broadening of type.builtinConstraintBroadenings) {
1068
+ const key = `${qualifiedId}:${broadening.tagName}`;
1069
+ if (builtinBroadeningMap.has(key)) {
1070
+ throw new Error(`Duplicate built-in constraint broadening: "${key}"`);
1071
+ }
1072
+ builtinBroadeningMap.set(key, {
1073
+ extensionId: ext.extensionId,
1074
+ registration: broadening
1075
+ });
1076
+ }
1077
+ }
1028
1078
  }
1029
1079
  }
1030
1080
  if (ext.constraints !== void 0) {
@@ -1036,6 +1086,17 @@ function createExtensionRegistry(extensions) {
1036
1086
  constraintMap.set(qualifiedId, constraint);
1037
1087
  }
1038
1088
  }
1089
+ if (ext.constraintTags !== void 0) {
1090
+ for (const tag of ext.constraintTags) {
1091
+ if (constraintTagMap.has(tag.tagName)) {
1092
+ throw new Error(`Duplicate custom constraint tag: "@${tag.tagName}"`);
1093
+ }
1094
+ constraintTagMap.set(tag.tagName, {
1095
+ extensionId: ext.extensionId,
1096
+ registration: tag
1097
+ });
1098
+ }
1099
+ }
1039
1100
  if (ext.annotations !== void 0) {
1040
1101
  for (const annotation of ext.annotations) {
1041
1102
  const qualifiedId = `${ext.extensionId}/${annotation.annotationName}`;
@@ -1049,7 +1110,10 @@ function createExtensionRegistry(extensions) {
1049
1110
  return {
1050
1111
  extensions,
1051
1112
  findType: (typeId) => typeMap.get(typeId),
1113
+ findTypeByName: (typeName) => typeNameMap.get(typeName),
1052
1114
  findConstraint: (constraintId) => constraintMap.get(constraintId),
1115
+ findConstraintTag: (tagName) => constraintTagMap.get(tagName),
1116
+ findBuiltinConstraintBroadening: (typeId, tagName) => builtinBroadeningMap.get(`${typeId}:${tagName}`),
1053
1117
  findAnnotation: (annotationId) => annotationMap.get(annotationId)
1054
1118
  };
1055
1119
  }
@@ -1117,6 +1181,7 @@ var jsonSchema7Schema = import_zod3.z.lazy(
1117
1181
  );
1118
1182
 
1119
1183
  // src/validate/constraint-validator.ts
1184
+ var import_core3 = require("@formspec/core");
1120
1185
  function addContradiction(ctx, message, primary, related) {
1121
1186
  ctx.diagnostics.push({
1122
1187
  code: "CONTRADICTING_CONSTRAINTS",
@@ -1162,6 +1227,13 @@ function addConstraintBroadening(ctx, message, primary, related) {
1162
1227
  relatedLocations: [related]
1163
1228
  });
1164
1229
  }
1230
+ function getExtensionIdFromConstraintId(constraintId) {
1231
+ const separator = constraintId.lastIndexOf("/");
1232
+ if (separator <= 0) {
1233
+ return null;
1234
+ }
1235
+ return constraintId.slice(0, separator);
1236
+ }
1165
1237
  function findNumeric(constraints, constraintKind) {
1166
1238
  return constraints.find((c) => c.constraintKind === constraintKind);
1167
1239
  }
@@ -1332,6 +1404,112 @@ function checkConstraintBroadening(ctx, fieldName, constraints) {
1332
1404
  strongestByKey.set(key, constraint);
1333
1405
  }
1334
1406
  }
1407
+ function compareCustomConstraintStrength(current, previous) {
1408
+ const order = current.comparePayloads(current.constraint.payload, previous.constraint.payload);
1409
+ const equalPayloadTiebreaker = order === 0 ? compareSemanticInclusivity(current.role.inclusive, previous.role.inclusive) : order;
1410
+ switch (current.role.bound) {
1411
+ case "lower":
1412
+ return equalPayloadTiebreaker;
1413
+ case "upper":
1414
+ return equalPayloadTiebreaker === 0 ? 0 : -equalPayloadTiebreaker;
1415
+ case "exact":
1416
+ return order === 0 ? 0 : Number.NaN;
1417
+ default: {
1418
+ const _exhaustive = current.role.bound;
1419
+ return _exhaustive;
1420
+ }
1421
+ }
1422
+ }
1423
+ function compareSemanticInclusivity(currentInclusive, previousInclusive) {
1424
+ if (currentInclusive === previousInclusive) {
1425
+ return 0;
1426
+ }
1427
+ return currentInclusive ? -1 : 1;
1428
+ }
1429
+ function customConstraintsContradict(lower, upper) {
1430
+ const order = lower.comparePayloads(lower.constraint.payload, upper.constraint.payload);
1431
+ if (order > 0) {
1432
+ return true;
1433
+ }
1434
+ if (order < 0) {
1435
+ return false;
1436
+ }
1437
+ return !lower.role.inclusive || !upper.role.inclusive;
1438
+ }
1439
+ function describeCustomConstraintTag(constraint) {
1440
+ return constraint.provenance.tagName ?? constraint.constraintId;
1441
+ }
1442
+ function checkCustomConstraintSemantics(ctx, fieldName, constraints) {
1443
+ if (ctx.extensionRegistry === void 0) {
1444
+ return;
1445
+ }
1446
+ const strongestByKey = /* @__PURE__ */ new Map();
1447
+ const lowerByFamily = /* @__PURE__ */ new Map();
1448
+ const upperByFamily = /* @__PURE__ */ new Map();
1449
+ for (const constraint of constraints) {
1450
+ if (constraint.constraintKind !== "custom") {
1451
+ continue;
1452
+ }
1453
+ const registration = ctx.extensionRegistry.findConstraint(constraint.constraintId);
1454
+ if (registration?.comparePayloads === void 0 || registration.semanticRole === void 0) {
1455
+ continue;
1456
+ }
1457
+ const entry = {
1458
+ constraint,
1459
+ comparePayloads: registration.comparePayloads,
1460
+ role: registration.semanticRole
1461
+ };
1462
+ const familyKey = `${registration.semanticRole.family}:${pathKey(constraint)}`;
1463
+ const boundKey = `${familyKey}:${registration.semanticRole.bound}`;
1464
+ const previous = strongestByKey.get(boundKey);
1465
+ if (previous !== void 0) {
1466
+ const strength = compareCustomConstraintStrength(entry, previous);
1467
+ if (Number.isNaN(strength)) {
1468
+ addContradiction(
1469
+ ctx,
1470
+ `Field "${formatPathTargetFieldName(fieldName, constraint.path?.segments ?? [])}": ${describeCustomConstraintTag(constraint)} conflicts with ${describeCustomConstraintTag(previous.constraint)}`,
1471
+ constraint.provenance,
1472
+ previous.constraint.provenance
1473
+ );
1474
+ continue;
1475
+ }
1476
+ if (strength < 0) {
1477
+ addConstraintBroadening(
1478
+ ctx,
1479
+ `Field "${formatPathTargetFieldName(fieldName, constraint.path?.segments ?? [])}": ${describeCustomConstraintTag(constraint)} is broader than earlier ${describeCustomConstraintTag(previous.constraint)}. Constraints can only narrow.`,
1480
+ constraint.provenance,
1481
+ previous.constraint.provenance
1482
+ );
1483
+ continue;
1484
+ }
1485
+ if (strength > 0) {
1486
+ strongestByKey.set(boundKey, entry);
1487
+ }
1488
+ } else {
1489
+ strongestByKey.set(boundKey, entry);
1490
+ }
1491
+ if (registration.semanticRole.bound === "lower") {
1492
+ lowerByFamily.set(familyKey, strongestByKey.get(boundKey) ?? entry);
1493
+ } else if (registration.semanticRole.bound === "upper") {
1494
+ upperByFamily.set(familyKey, strongestByKey.get(boundKey) ?? entry);
1495
+ }
1496
+ }
1497
+ for (const [familyKey, lower] of lowerByFamily) {
1498
+ const upper = upperByFamily.get(familyKey);
1499
+ if (upper === void 0) {
1500
+ continue;
1501
+ }
1502
+ if (!customConstraintsContradict(lower, upper)) {
1503
+ continue;
1504
+ }
1505
+ addContradiction(
1506
+ ctx,
1507
+ `Field "${formatPathTargetFieldName(fieldName, lower.constraint.path?.segments ?? [])}": ${describeCustomConstraintTag(lower.constraint)} contradicts ${describeCustomConstraintTag(upper.constraint)}`,
1508
+ lower.constraint.provenance,
1509
+ upper.constraint.provenance
1510
+ );
1511
+ }
1512
+ }
1335
1513
  function checkNumericContradictions(ctx, fieldName, constraints) {
1336
1514
  const min = findNumeric(constraints, "minimum");
1337
1515
  const max = findNumeric(constraints, "maximum");
@@ -1479,6 +1657,26 @@ function dereferenceType(ctx, type) {
1479
1657
  }
1480
1658
  return current;
1481
1659
  }
1660
+ function collectReferencedTypeConstraints(ctx, type) {
1661
+ const collected = [];
1662
+ let current = type;
1663
+ const seen = /* @__PURE__ */ new Set();
1664
+ while (current.kind === "reference") {
1665
+ if (seen.has(current.name)) {
1666
+ break;
1667
+ }
1668
+ seen.add(current.name);
1669
+ const definition = ctx.typeRegistry[current.name];
1670
+ if (definition === void 0) {
1671
+ break;
1672
+ }
1673
+ if (definition.constraints !== void 0) {
1674
+ collected.push(...definition.constraints);
1675
+ }
1676
+ current = definition.type;
1677
+ }
1678
+ return collected;
1679
+ }
1482
1680
  function resolvePathTargetType(ctx, type, segments) {
1483
1681
  const effectiveType = dereferenceType(ctx, type);
1484
1682
  if (segments.length === 0) {
@@ -1500,12 +1698,33 @@ function resolvePathTargetType(ctx, type, segments) {
1500
1698
  }
1501
1699
  return { kind: "unresolvable", type: effectiveType };
1502
1700
  }
1701
+ function isNullType(type) {
1702
+ return type.kind === "primitive" && type.primitiveKind === "null";
1703
+ }
1704
+ function collectCustomConstraintCandidateTypes(ctx, type) {
1705
+ const effectiveType = dereferenceType(ctx, type);
1706
+ const candidates = [effectiveType];
1707
+ if (effectiveType.kind === "array") {
1708
+ candidates.push(...collectCustomConstraintCandidateTypes(ctx, effectiveType.items));
1709
+ }
1710
+ if (effectiveType.kind === "union") {
1711
+ const memberTypes = effectiveType.members.map((member) => dereferenceType(ctx, member));
1712
+ const nonNullMembers = memberTypes.filter((member) => !isNullType(member));
1713
+ if (nonNullMembers.length === 1 && nonNullMembers.length < memberTypes.length) {
1714
+ const [nullableMember] = nonNullMembers;
1715
+ if (nullableMember !== void 0) {
1716
+ candidates.push(...collectCustomConstraintCandidateTypes(ctx, nullableMember));
1717
+ }
1718
+ }
1719
+ }
1720
+ return candidates;
1721
+ }
1503
1722
  function formatPathTargetFieldName(fieldName, path) {
1504
1723
  return path.length === 0 ? fieldName : `${fieldName}.${path.join(".")}`;
1505
1724
  }
1506
1725
  function checkConstraintOnType(ctx, fieldName, type, constraint) {
1507
1726
  const effectiveType = dereferenceType(ctx, type);
1508
- const isNumber = effectiveType.kind === "primitive" && effectiveType.primitiveKind === "number";
1727
+ const isNumber = effectiveType.kind === "primitive" && ["number", "integer", "bigint"].includes(effectiveType.primitiveKind);
1509
1728
  const isString = effectiveType.kind === "primitive" && effectiveType.primitiveKind === "string";
1510
1729
  const isArray = effectiveType.kind === "array";
1511
1730
  const isEnum = effectiveType.kind === "enum";
@@ -1563,7 +1782,9 @@ function checkConstraintOnType(ctx, fieldName, type, constraint) {
1563
1782
  break;
1564
1783
  }
1565
1784
  case "const": {
1566
- const isPrimitiveConstType = effectiveType.kind === "primitive" && ["string", "number", "boolean", "null"].includes(effectiveType.primitiveKind) || effectiveType.kind === "enum";
1785
+ const isPrimitiveConstType = effectiveType.kind === "primitive" && ["string", "number", "integer", "bigint", "boolean", "null"].includes(
1786
+ effectiveType.primitiveKind
1787
+ ) || effectiveType.kind === "enum";
1567
1788
  if (!isPrimitiveConstType) {
1568
1789
  addTypeMismatch(
1569
1790
  ctx,
@@ -1574,7 +1795,8 @@ function checkConstraintOnType(ctx, fieldName, type, constraint) {
1574
1795
  }
1575
1796
  if (effectiveType.kind === "primitive") {
1576
1797
  const valueType = constraint.value === null ? "null" : Array.isArray(constraint.value) ? "array" : typeof constraint.value;
1577
- if (valueType !== effectiveType.primitiveKind) {
1798
+ const expectedValueType = effectiveType.primitiveKind === "integer" || effectiveType.primitiveKind === "bigint" ? "number" : effectiveType.primitiveKind;
1799
+ if (valueType !== expectedValueType) {
1578
1800
  addTypeMismatch(
1579
1801
  ctx,
1580
1802
  `Field "${fieldName}": @const value type "${valueType}" is incompatible with field type "${effectiveType.primitiveKind}"`,
@@ -1643,8 +1865,37 @@ function checkCustomConstraint(ctx, fieldName, type, constraint) {
1643
1865
  );
1644
1866
  return;
1645
1867
  }
1646
- if (registration.applicableTypes === null) return;
1647
- if (!registration.applicableTypes.includes(type.kind)) {
1868
+ const candidateTypes = collectCustomConstraintCandidateTypes(ctx, type);
1869
+ const normalizedTagName = constraint.provenance.tagName === void 0 ? void 0 : (0, import_core3.normalizeConstraintTagName)(constraint.provenance.tagName.replace(/^@/, ""));
1870
+ if (normalizedTagName !== void 0) {
1871
+ const tagRegistration = ctx.extensionRegistry.findConstraintTag(normalizedTagName);
1872
+ const extensionId = getExtensionIdFromConstraintId(constraint.constraintId);
1873
+ if (extensionId !== null && tagRegistration?.extensionId === extensionId && tagRegistration.registration.constraintName === registration.constraintName && !candidateTypes.some(
1874
+ (candidateType) => tagRegistration.registration.isApplicableToType?.(candidateType) !== false
1875
+ )) {
1876
+ addTypeMismatch(
1877
+ ctx,
1878
+ `Field "${fieldName}": custom constraint "${constraint.constraintId}" is not applicable to type "${typeLabel(type)}"`,
1879
+ constraint.provenance
1880
+ );
1881
+ return;
1882
+ }
1883
+ }
1884
+ if (registration.applicableTypes === null) {
1885
+ if (!candidateTypes.some((candidateType) => registration.isApplicableToType?.(candidateType) !== false)) {
1886
+ addTypeMismatch(
1887
+ ctx,
1888
+ `Field "${fieldName}": custom constraint "${constraint.constraintId}" is not applicable to type "${typeLabel(type)}"`,
1889
+ constraint.provenance
1890
+ );
1891
+ }
1892
+ return;
1893
+ }
1894
+ const applicableTypes = registration.applicableTypes;
1895
+ const matchesApplicableType = candidateTypes.some(
1896
+ (candidateType) => applicableTypes.includes(candidateType.kind) && registration.isApplicableToType?.(candidateType) !== false
1897
+ );
1898
+ if (!matchesApplicableType) {
1648
1899
  addTypeMismatch(
1649
1900
  ctx,
1650
1901
  `Field "${fieldName}": custom constraint "${constraint.constraintId}" is not applicable to type "${typeLabel(type)}"`,
@@ -1653,7 +1904,10 @@ function checkCustomConstraint(ctx, fieldName, type, constraint) {
1653
1904
  }
1654
1905
  }
1655
1906
  function validateFieldNode(ctx, field) {
1656
- validateConstraints(ctx, field.name, field.type, field.constraints);
1907
+ validateConstraints(ctx, field.name, field.type, [
1908
+ ...collectReferencedTypeConstraints(ctx, field.type),
1909
+ ...field.constraints
1910
+ ]);
1657
1911
  if (field.type.kind === "object") {
1658
1912
  for (const prop of field.type.properties) {
1659
1913
  validateObjectProperty(ctx, field.name, prop);
@@ -1662,7 +1916,10 @@ function validateFieldNode(ctx, field) {
1662
1916
  }
1663
1917
  function validateObjectProperty(ctx, parentName, prop) {
1664
1918
  const qualifiedName = `${parentName}.${prop.name}`;
1665
- validateConstraints(ctx, qualifiedName, prop.type, prop.constraints);
1919
+ validateConstraints(ctx, qualifiedName, prop.type, [
1920
+ ...collectReferencedTypeConstraints(ctx, prop.type),
1921
+ ...prop.constraints
1922
+ ]);
1666
1923
  if (prop.type.kind === "object") {
1667
1924
  for (const nestedProp of prop.type.properties) {
1668
1925
  validateObjectProperty(ctx, qualifiedName, nestedProp);
@@ -1675,6 +1932,7 @@ function validateConstraints(ctx, name, type, constraints) {
1675
1932
  checkAllowedMembersContradiction(ctx, name, constraints);
1676
1933
  checkConstContradictions(ctx, name, constraints);
1677
1934
  checkConstraintBroadening(ctx, name, constraints);
1935
+ checkCustomConstraintSemantics(ctx, name, constraints);
1678
1936
  checkTypeApplicability(ctx, name, type, constraints);
1679
1937
  }
1680
1938
  function validateElement(ctx, element) {