@formspec/build 0.1.0-alpha.44 → 0.1.0-alpha.46

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 (44) hide show
  1. package/dist/analyzer/class-analyzer.d.ts.map +1 -1
  2. package/dist/analyzer/program.d.ts +4 -1
  3. package/dist/analyzer/program.d.ts.map +1 -1
  4. package/dist/analyzer/tsdoc-parser.d.ts.map +1 -1
  5. package/dist/browser.cjs +33 -7
  6. package/dist/browser.cjs.map +1 -1
  7. package/dist/browser.js +34 -10
  8. package/dist/browser.js.map +1 -1
  9. package/dist/build-alpha.d.ts +106 -5
  10. package/dist/build-beta.d.ts +106 -5
  11. package/dist/build-internal.d.ts +106 -5
  12. package/dist/build.d.ts +106 -5
  13. package/dist/cli.cjs +364 -116
  14. package/dist/cli.cjs.map +1 -1
  15. package/dist/cli.js +360 -115
  16. package/dist/cli.js.map +1 -1
  17. package/dist/extensions/index.d.ts +1 -1
  18. package/dist/extensions/index.d.ts.map +1 -1
  19. package/dist/extensions/registry.d.ts +64 -5
  20. package/dist/extensions/registry.d.ts.map +1 -1
  21. package/dist/extensions/symbol-registry.d.ts +33 -0
  22. package/dist/extensions/symbol-registry.d.ts.map +1 -0
  23. package/dist/generators/class-schema.d.ts +32 -0
  24. package/dist/generators/class-schema.d.ts.map +1 -1
  25. package/dist/generators/discovered-schema.d.ts.map +1 -1
  26. package/dist/index.cjs +350 -109
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.ts +1 -1
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +351 -112
  31. package/dist/index.js.map +1 -1
  32. package/dist/internals.cjs +121 -23
  33. package/dist/internals.cjs.map +1 -1
  34. package/dist/internals.d.ts +2 -2
  35. package/dist/internals.d.ts.map +1 -1
  36. package/dist/internals.js +121 -26
  37. package/dist/internals.js.map +1 -1
  38. package/dist/json-schema/generator.d.ts.map +1 -1
  39. package/dist/metadata/collision-guards.d.ts.map +1 -1
  40. package/dist/metadata/policy.d.ts.map +1 -1
  41. package/dist/metadata/resolve.d.ts.map +1 -1
  42. package/dist/ui-schema/ir-generator.d.ts.map +1 -1
  43. package/dist/validate/constraint-validator.d.ts.map +1 -1
  44. package/package.json +5 -5
@@ -317,7 +317,7 @@ declare interface ControlOptionConstraints {
317
317
  *
318
318
  * @public
319
319
  */
320
- export declare function createExtensionRegistry(extensions: readonly ExtensionDefinition[]): ExtensionRegistry;
320
+ export declare function createExtensionRegistry(extensions: readonly ExtensionDefinition[]): MutableExtensionRegistry;
321
321
 
322
322
  /**
323
323
  * Creates a supported static build context for a source file.
@@ -436,8 +436,33 @@ declare interface CustomTypeRegistration_2 {
436
436
  /**
437
437
  * Optional TypeScript surface names that should resolve to this custom type
438
438
  * during TSDoc/class analysis. Defaults to `typeName` when omitted.
439
+ * @deprecated Prefer `brand` for structural detection or type parameters
440
+ * on `defineCustomType<T>()` for symbol-based detection. String name
441
+ * matching will be removed in a future major version.
439
442
  */
440
443
  readonly tsTypeNames?: readonly string[];
444
+ /**
445
+ * Optional brand identifier for structural type detection.
446
+ *
447
+ * When provided, the type resolver checks `type.getProperties()` for a
448
+ * computed property whose name matches this identifier. This is more
449
+ * reliable than `tsTypeNames` for aliased branded types because it does not
450
+ * depend on the local type name.
451
+ *
452
+ * Brand detection is attempted after name-based resolution (`tsTypeNames`)
453
+ * as a structural fallback. If both match, name-based resolution wins.
454
+ *
455
+ * The value should match the identifier text of a `unique symbol` declaration
456
+ * used as a computed property key on the branded type. For example, if the
457
+ * type is `string & { readonly [__decimalBrand]: true }`, the brand is
458
+ * `"__decimalBrand"`.
459
+ *
460
+ * Brand identifiers are stored as plain strings in the extension registry, so
461
+ * they must be unique across the extensions loaded into the same build.
462
+ *
463
+ * Note: `"__integerBrand"` is reserved for the builtin Integer type.
464
+ */
465
+ readonly brand?: string;
441
466
  /**
442
467
  * Converts the custom type's payload into a JSON Schema fragment.
443
468
  *
@@ -716,10 +741,32 @@ export declare interface ExtensionRegistry {
716
741
  * This is used during TSDoc/class analysis to resolve extension-defined
717
742
  * custom types from source-level declarations.
718
743
  */
719
- findTypeByName(typeName: string): {
720
- readonly extensionId: string;
721
- readonly registration: CustomTypeRegistration;
722
- } | undefined;
744
+ findTypeByName(typeName: string): ExtensionTypeLookupResult | undefined;
745
+ /**
746
+ * Look up a custom type registration by a brand identifier.
747
+ *
748
+ * This is used during class analysis to resolve extension-defined custom types
749
+ * via structural brand detection (`unique symbol` computed property keys).
750
+ * Brand identifiers are stored as plain strings, so they must be unique
751
+ * across all extensions loaded into the registry.
752
+ *
753
+ * @param brand - The identifier text of the `unique symbol` brand variable.
754
+ */
755
+ findTypeByBrand(brand: string): ExtensionTypeLookupResult | undefined;
756
+ /**
757
+ * Look up a custom type by its TypeScript symbol identity.
758
+ *
759
+ * Built from `defineCustomType<T>()` type parameter extraction in the config file.
760
+ * This is the most precise detection path — it uses `ts.Symbol` identity, which is
761
+ * immune to import aliases and name collisions.
762
+ *
763
+ * Returns `undefined` until {@link MutableExtensionRegistry.setSymbolMap} has been
764
+ * called (i.e., before the TypeScript program is available), or when the symbol is
765
+ * not registered via a type parameter.
766
+ *
767
+ * @param symbol - The canonical TypeScript symbol to look up.
768
+ */
769
+ findTypeBySymbol(symbol: ts.Symbol): ExtensionTypeLookupResult | undefined;
723
770
  /**
724
771
  * Look up a custom constraint registration by its fully-qualified constraint ID.
725
772
  *
@@ -757,6 +804,22 @@ export declare interface ExtensionRegistry {
757
804
  */
758
805
  declare type ExtensionTypeKind = "primitive" | "enum" | "array" | "object" | "record" | "union" | "reference" | "dynamic" | "custom";
759
806
 
807
+ /**
808
+ * The result of a successful extension type lookup.
809
+ *
810
+ * Returned by {@link ExtensionRegistry.findTypeByName},
811
+ * {@link ExtensionRegistry.findTypeByBrand}, and
812
+ * {@link ExtensionRegistry.findTypeBySymbol}.
813
+ *
814
+ * @public
815
+ */
816
+ export declare interface ExtensionTypeLookupResult {
817
+ /** The fully-qualified extension ID (e.g., "x-stripe/monetary"). */
818
+ readonly extensionId: string;
819
+ /** The custom type registration matched by this lookup. */
820
+ readonly registration: CustomTypeRegistration;
821
+ }
822
+
760
823
  /**
761
824
  * Field configuration option constraints - control which field options are allowed.
762
825
  *
@@ -1846,6 +1909,28 @@ export declare interface MixedAuthoringSchemas {
1846
1909
  readonly uiSchema: UISchema;
1847
1910
  }
1848
1911
 
1912
+ /**
1913
+ * Mutable extension registry used internally by the build pipeline.
1914
+ *
1915
+ * Extends {@link ExtensionRegistry} with `setSymbolMap`, which must be called
1916
+ * after the TypeScript program is created. Consumer code should accept only
1917
+ * the read-only {@link ExtensionRegistry} interface.
1918
+ *
1919
+ * @public
1920
+ */
1921
+ export declare interface MutableExtensionRegistry extends ExtensionRegistry {
1922
+ /**
1923
+ * Sets the symbol map built from config AST analysis.
1924
+ *
1925
+ * Called after the TypeScript program is created and the config file is analyzed.
1926
+ * Prior to this call, {@link ExtensionRegistry.findTypeBySymbol} always returns
1927
+ * `undefined`.
1928
+ *
1929
+ * @param map - A map from canonical `ts.Symbol` to the matching registry entry.
1930
+ */
1931
+ setSymbolMap(map: Map<ts.Symbol, ExtensionTypeLookupResult>): void;
1932
+ }
1933
+
1849
1934
  export { NumberField }
1850
1935
 
1851
1936
  export { ObjectField }
@@ -2080,6 +2165,22 @@ export declare interface StaticSchemaGenerationOptions {
2080
2165
  readonly metadata?: MetadataPolicyInput | undefined;
2081
2166
  /** Discriminator-specific schema generation behavior. */
2082
2167
  readonly discriminator?: DiscriminatorResolutionOptions | undefined;
2168
+ /**
2169
+ * Absolute path to the FormSpec config file (e.g., `formspec.config.ts`).
2170
+ *
2171
+ * When provided alongside a `config` that includes extensions, the build
2172
+ * pipeline includes the config file in the TypeScript program and extracts
2173
+ * `defineCustomType<T>()` type parameters. This enables symbol-based custom
2174
+ * type detection — the most precise resolution path, immune to import aliases
2175
+ * and name collisions.
2176
+ *
2177
+ * Obtain this from `loadFormSpecConfig()`:
2178
+ * ```typescript
2179
+ * const { config, configPath } = await loadFormSpecConfig();
2180
+ * await generateSchemas({ filePath, typeName, config, configPath, errorReporting: "throw" });
2181
+ * ```
2182
+ */
2183
+ readonly configPath?: string | undefined;
2083
2184
  }
2084
2185
 
2085
2186
  export { TextField }
@@ -317,7 +317,7 @@ declare interface ControlOptionConstraints {
317
317
  *
318
318
  * @public
319
319
  */
320
- export declare function createExtensionRegistry(extensions: readonly ExtensionDefinition[]): ExtensionRegistry;
320
+ export declare function createExtensionRegistry(extensions: readonly ExtensionDefinition[]): MutableExtensionRegistry;
321
321
 
322
322
  /**
323
323
  * Creates a supported static build context for a source file.
@@ -436,8 +436,33 @@ declare interface CustomTypeRegistration_2 {
436
436
  /**
437
437
  * Optional TypeScript surface names that should resolve to this custom type
438
438
  * during TSDoc/class analysis. Defaults to `typeName` when omitted.
439
+ * @deprecated Prefer `brand` for structural detection or type parameters
440
+ * on `defineCustomType<T>()` for symbol-based detection. String name
441
+ * matching will be removed in a future major version.
439
442
  */
440
443
  readonly tsTypeNames?: readonly string[];
444
+ /**
445
+ * Optional brand identifier for structural type detection.
446
+ *
447
+ * When provided, the type resolver checks `type.getProperties()` for a
448
+ * computed property whose name matches this identifier. This is more
449
+ * reliable than `tsTypeNames` for aliased branded types because it does not
450
+ * depend on the local type name.
451
+ *
452
+ * Brand detection is attempted after name-based resolution (`tsTypeNames`)
453
+ * as a structural fallback. If both match, name-based resolution wins.
454
+ *
455
+ * The value should match the identifier text of a `unique symbol` declaration
456
+ * used as a computed property key on the branded type. For example, if the
457
+ * type is `string & { readonly [__decimalBrand]: true }`, the brand is
458
+ * `"__decimalBrand"`.
459
+ *
460
+ * Brand identifiers are stored as plain strings in the extension registry, so
461
+ * they must be unique across the extensions loaded into the same build.
462
+ *
463
+ * Note: `"__integerBrand"` is reserved for the builtin Integer type.
464
+ */
465
+ readonly brand?: string;
441
466
  /**
442
467
  * Converts the custom type's payload into a JSON Schema fragment.
443
468
  *
@@ -716,10 +741,32 @@ export declare interface ExtensionRegistry {
716
741
  * This is used during TSDoc/class analysis to resolve extension-defined
717
742
  * custom types from source-level declarations.
718
743
  */
719
- findTypeByName(typeName: string): {
720
- readonly extensionId: string;
721
- readonly registration: CustomTypeRegistration;
722
- } | undefined;
744
+ findTypeByName(typeName: string): ExtensionTypeLookupResult | undefined;
745
+ /**
746
+ * Look up a custom type registration by a brand identifier.
747
+ *
748
+ * This is used during class analysis to resolve extension-defined custom types
749
+ * via structural brand detection (`unique symbol` computed property keys).
750
+ * Brand identifiers are stored as plain strings, so they must be unique
751
+ * across all extensions loaded into the registry.
752
+ *
753
+ * @param brand - The identifier text of the `unique symbol` brand variable.
754
+ */
755
+ findTypeByBrand(brand: string): ExtensionTypeLookupResult | undefined;
756
+ /**
757
+ * Look up a custom type by its TypeScript symbol identity.
758
+ *
759
+ * Built from `defineCustomType<T>()` type parameter extraction in the config file.
760
+ * This is the most precise detection path — it uses `ts.Symbol` identity, which is
761
+ * immune to import aliases and name collisions.
762
+ *
763
+ * Returns `undefined` until {@link MutableExtensionRegistry.setSymbolMap} has been
764
+ * called (i.e., before the TypeScript program is available), or when the symbol is
765
+ * not registered via a type parameter.
766
+ *
767
+ * @param symbol - The canonical TypeScript symbol to look up.
768
+ */
769
+ findTypeBySymbol(symbol: ts.Symbol): ExtensionTypeLookupResult | undefined;
723
770
  /**
724
771
  * Look up a custom constraint registration by its fully-qualified constraint ID.
725
772
  *
@@ -757,6 +804,22 @@ export declare interface ExtensionRegistry {
757
804
  */
758
805
  declare type ExtensionTypeKind = "primitive" | "enum" | "array" | "object" | "record" | "union" | "reference" | "dynamic" | "custom";
759
806
 
807
+ /**
808
+ * The result of a successful extension type lookup.
809
+ *
810
+ * Returned by {@link ExtensionRegistry.findTypeByName},
811
+ * {@link ExtensionRegistry.findTypeByBrand}, and
812
+ * {@link ExtensionRegistry.findTypeBySymbol}.
813
+ *
814
+ * @public
815
+ */
816
+ export declare interface ExtensionTypeLookupResult {
817
+ /** The fully-qualified extension ID (e.g., "x-stripe/monetary"). */
818
+ readonly extensionId: string;
819
+ /** The custom type registration matched by this lookup. */
820
+ readonly registration: CustomTypeRegistration;
821
+ }
822
+
760
823
  /**
761
824
  * Field configuration option constraints - control which field options are allowed.
762
825
  *
@@ -1846,6 +1909,28 @@ export declare interface MixedAuthoringSchemas {
1846
1909
  readonly uiSchema: UISchema;
1847
1910
  }
1848
1911
 
1912
+ /**
1913
+ * Mutable extension registry used internally by the build pipeline.
1914
+ *
1915
+ * Extends {@link ExtensionRegistry} with `setSymbolMap`, which must be called
1916
+ * after the TypeScript program is created. Consumer code should accept only
1917
+ * the read-only {@link ExtensionRegistry} interface.
1918
+ *
1919
+ * @public
1920
+ */
1921
+ export declare interface MutableExtensionRegistry extends ExtensionRegistry {
1922
+ /**
1923
+ * Sets the symbol map built from config AST analysis.
1924
+ *
1925
+ * Called after the TypeScript program is created and the config file is analyzed.
1926
+ * Prior to this call, {@link ExtensionRegistry.findTypeBySymbol} always returns
1927
+ * `undefined`.
1928
+ *
1929
+ * @param map - A map from canonical `ts.Symbol` to the matching registry entry.
1930
+ */
1931
+ setSymbolMap(map: Map<ts.Symbol, ExtensionTypeLookupResult>): void;
1932
+ }
1933
+
1849
1934
  export { NumberField }
1850
1935
 
1851
1936
  export { ObjectField }
@@ -2080,6 +2165,22 @@ export declare interface StaticSchemaGenerationOptions {
2080
2165
  readonly metadata?: MetadataPolicyInput | undefined;
2081
2166
  /** Discriminator-specific schema generation behavior. */
2082
2167
  readonly discriminator?: DiscriminatorResolutionOptions | undefined;
2168
+ /**
2169
+ * Absolute path to the FormSpec config file (e.g., `formspec.config.ts`).
2170
+ *
2171
+ * When provided alongside a `config` that includes extensions, the build
2172
+ * pipeline includes the config file in the TypeScript program and extracts
2173
+ * `defineCustomType<T>()` type parameters. This enables symbol-based custom
2174
+ * type detection — the most precise resolution path, immune to import aliases
2175
+ * and name collisions.
2176
+ *
2177
+ * Obtain this from `loadFormSpecConfig()`:
2178
+ * ```typescript
2179
+ * const { config, configPath } = await loadFormSpecConfig();
2180
+ * await generateSchemas({ filePath, typeName, config, configPath, errorReporting: "throw" });
2181
+ * ```
2182
+ */
2183
+ readonly configPath?: string | undefined;
2083
2184
  }
2084
2185
 
2085
2186
  export { TextField }
@@ -317,7 +317,7 @@ declare interface ControlOptionConstraints {
317
317
  *
318
318
  * @public
319
319
  */
320
- export declare function createExtensionRegistry(extensions: readonly ExtensionDefinition[]): ExtensionRegistry;
320
+ export declare function createExtensionRegistry(extensions: readonly ExtensionDefinition[]): MutableExtensionRegistry;
321
321
 
322
322
  /**
323
323
  * Creates a supported static build context for a source file.
@@ -436,8 +436,33 @@ declare interface CustomTypeRegistration_2 {
436
436
  /**
437
437
  * Optional TypeScript surface names that should resolve to this custom type
438
438
  * during TSDoc/class analysis. Defaults to `typeName` when omitted.
439
+ * @deprecated Prefer `brand` for structural detection or type parameters
440
+ * on `defineCustomType<T>()` for symbol-based detection. String name
441
+ * matching will be removed in a future major version.
439
442
  */
440
443
  readonly tsTypeNames?: readonly string[];
444
+ /**
445
+ * Optional brand identifier for structural type detection.
446
+ *
447
+ * When provided, the type resolver checks `type.getProperties()` for a
448
+ * computed property whose name matches this identifier. This is more
449
+ * reliable than `tsTypeNames` for aliased branded types because it does not
450
+ * depend on the local type name.
451
+ *
452
+ * Brand detection is attempted after name-based resolution (`tsTypeNames`)
453
+ * as a structural fallback. If both match, name-based resolution wins.
454
+ *
455
+ * The value should match the identifier text of a `unique symbol` declaration
456
+ * used as a computed property key on the branded type. For example, if the
457
+ * type is `string & { readonly [__decimalBrand]: true }`, the brand is
458
+ * `"__decimalBrand"`.
459
+ *
460
+ * Brand identifiers are stored as plain strings in the extension registry, so
461
+ * they must be unique across the extensions loaded into the same build.
462
+ *
463
+ * Note: `"__integerBrand"` is reserved for the builtin Integer type.
464
+ */
465
+ readonly brand?: string;
441
466
  /**
442
467
  * Converts the custom type's payload into a JSON Schema fragment.
443
468
  *
@@ -716,10 +741,32 @@ export declare interface ExtensionRegistry {
716
741
  * This is used during TSDoc/class analysis to resolve extension-defined
717
742
  * custom types from source-level declarations.
718
743
  */
719
- findTypeByName(typeName: string): {
720
- readonly extensionId: string;
721
- readonly registration: CustomTypeRegistration;
722
- } | undefined;
744
+ findTypeByName(typeName: string): ExtensionTypeLookupResult | undefined;
745
+ /**
746
+ * Look up a custom type registration by a brand identifier.
747
+ *
748
+ * This is used during class analysis to resolve extension-defined custom types
749
+ * via structural brand detection (`unique symbol` computed property keys).
750
+ * Brand identifiers are stored as plain strings, so they must be unique
751
+ * across all extensions loaded into the registry.
752
+ *
753
+ * @param brand - The identifier text of the `unique symbol` brand variable.
754
+ */
755
+ findTypeByBrand(brand: string): ExtensionTypeLookupResult | undefined;
756
+ /**
757
+ * Look up a custom type by its TypeScript symbol identity.
758
+ *
759
+ * Built from `defineCustomType<T>()` type parameter extraction in the config file.
760
+ * This is the most precise detection path — it uses `ts.Symbol` identity, which is
761
+ * immune to import aliases and name collisions.
762
+ *
763
+ * Returns `undefined` until {@link MutableExtensionRegistry.setSymbolMap} has been
764
+ * called (i.e., before the TypeScript program is available), or when the symbol is
765
+ * not registered via a type parameter.
766
+ *
767
+ * @param symbol - The canonical TypeScript symbol to look up.
768
+ */
769
+ findTypeBySymbol(symbol: ts.Symbol): ExtensionTypeLookupResult | undefined;
723
770
  /**
724
771
  * Look up a custom constraint registration by its fully-qualified constraint ID.
725
772
  *
@@ -757,6 +804,22 @@ export declare interface ExtensionRegistry {
757
804
  */
758
805
  declare type ExtensionTypeKind = "primitive" | "enum" | "array" | "object" | "record" | "union" | "reference" | "dynamic" | "custom";
759
806
 
807
+ /**
808
+ * The result of a successful extension type lookup.
809
+ *
810
+ * Returned by {@link ExtensionRegistry.findTypeByName},
811
+ * {@link ExtensionRegistry.findTypeByBrand}, and
812
+ * {@link ExtensionRegistry.findTypeBySymbol}.
813
+ *
814
+ * @public
815
+ */
816
+ export declare interface ExtensionTypeLookupResult {
817
+ /** The fully-qualified extension ID (e.g., "x-stripe/monetary"). */
818
+ readonly extensionId: string;
819
+ /** The custom type registration matched by this lookup. */
820
+ readonly registration: CustomTypeRegistration;
821
+ }
822
+
760
823
  /**
761
824
  * Field configuration option constraints - control which field options are allowed.
762
825
  *
@@ -1846,6 +1909,28 @@ export declare interface MixedAuthoringSchemas {
1846
1909
  readonly uiSchema: UISchema;
1847
1910
  }
1848
1911
 
1912
+ /**
1913
+ * Mutable extension registry used internally by the build pipeline.
1914
+ *
1915
+ * Extends {@link ExtensionRegistry} with `setSymbolMap`, which must be called
1916
+ * after the TypeScript program is created. Consumer code should accept only
1917
+ * the read-only {@link ExtensionRegistry} interface.
1918
+ *
1919
+ * @public
1920
+ */
1921
+ export declare interface MutableExtensionRegistry extends ExtensionRegistry {
1922
+ /**
1923
+ * Sets the symbol map built from config AST analysis.
1924
+ *
1925
+ * Called after the TypeScript program is created and the config file is analyzed.
1926
+ * Prior to this call, {@link ExtensionRegistry.findTypeBySymbol} always returns
1927
+ * `undefined`.
1928
+ *
1929
+ * @param map - A map from canonical `ts.Symbol` to the matching registry entry.
1930
+ */
1931
+ setSymbolMap(map: Map<ts.Symbol, ExtensionTypeLookupResult>): void;
1932
+ }
1933
+
1849
1934
  export { NumberField }
1850
1935
 
1851
1936
  export { ObjectField }
@@ -2080,6 +2165,22 @@ export declare interface StaticSchemaGenerationOptions {
2080
2165
  readonly metadata?: MetadataPolicyInput | undefined;
2081
2166
  /** Discriminator-specific schema generation behavior. */
2082
2167
  readonly discriminator?: DiscriminatorResolutionOptions | undefined;
2168
+ /**
2169
+ * Absolute path to the FormSpec config file (e.g., `formspec.config.ts`).
2170
+ *
2171
+ * When provided alongside a `config` that includes extensions, the build
2172
+ * pipeline includes the config file in the TypeScript program and extracts
2173
+ * `defineCustomType<T>()` type parameters. This enables symbol-based custom
2174
+ * type detection — the most precise resolution path, immune to import aliases
2175
+ * and name collisions.
2176
+ *
2177
+ * Obtain this from `loadFormSpecConfig()`:
2178
+ * ```typescript
2179
+ * const { config, configPath } = await loadFormSpecConfig();
2180
+ * await generateSchemas({ filePath, typeName, config, configPath, errorReporting: "throw" });
2181
+ * ```
2182
+ */
2183
+ readonly configPath?: string | undefined;
2083
2184
  }
2084
2185
 
2085
2186
  export { TextField }
package/dist/build.d.ts CHANGED
@@ -317,7 +317,7 @@ declare interface ControlOptionConstraints {
317
317
  *
318
318
  * @public
319
319
  */
320
- export declare function createExtensionRegistry(extensions: readonly ExtensionDefinition[]): ExtensionRegistry;
320
+ export declare function createExtensionRegistry(extensions: readonly ExtensionDefinition[]): MutableExtensionRegistry;
321
321
 
322
322
  /**
323
323
  * Creates a supported static build context for a source file.
@@ -436,8 +436,33 @@ declare interface CustomTypeRegistration_2 {
436
436
  /**
437
437
  * Optional TypeScript surface names that should resolve to this custom type
438
438
  * during TSDoc/class analysis. Defaults to `typeName` when omitted.
439
+ * @deprecated Prefer `brand` for structural detection or type parameters
440
+ * on `defineCustomType<T>()` for symbol-based detection. String name
441
+ * matching will be removed in a future major version.
439
442
  */
440
443
  readonly tsTypeNames?: readonly string[];
444
+ /**
445
+ * Optional brand identifier for structural type detection.
446
+ *
447
+ * When provided, the type resolver checks `type.getProperties()` for a
448
+ * computed property whose name matches this identifier. This is more
449
+ * reliable than `tsTypeNames` for aliased branded types because it does not
450
+ * depend on the local type name.
451
+ *
452
+ * Brand detection is attempted after name-based resolution (`tsTypeNames`)
453
+ * as a structural fallback. If both match, name-based resolution wins.
454
+ *
455
+ * The value should match the identifier text of a `unique symbol` declaration
456
+ * used as a computed property key on the branded type. For example, if the
457
+ * type is `string & { readonly [__decimalBrand]: true }`, the brand is
458
+ * `"__decimalBrand"`.
459
+ *
460
+ * Brand identifiers are stored as plain strings in the extension registry, so
461
+ * they must be unique across the extensions loaded into the same build.
462
+ *
463
+ * Note: `"__integerBrand"` is reserved for the builtin Integer type.
464
+ */
465
+ readonly brand?: string;
441
466
  /**
442
467
  * Converts the custom type's payload into a JSON Schema fragment.
443
468
  *
@@ -716,10 +741,32 @@ export declare interface ExtensionRegistry {
716
741
  * This is used during TSDoc/class analysis to resolve extension-defined
717
742
  * custom types from source-level declarations.
718
743
  */
719
- findTypeByName(typeName: string): {
720
- readonly extensionId: string;
721
- readonly registration: CustomTypeRegistration;
722
- } | undefined;
744
+ findTypeByName(typeName: string): ExtensionTypeLookupResult | undefined;
745
+ /**
746
+ * Look up a custom type registration by a brand identifier.
747
+ *
748
+ * This is used during class analysis to resolve extension-defined custom types
749
+ * via structural brand detection (`unique symbol` computed property keys).
750
+ * Brand identifiers are stored as plain strings, so they must be unique
751
+ * across all extensions loaded into the registry.
752
+ *
753
+ * @param brand - The identifier text of the `unique symbol` brand variable.
754
+ */
755
+ findTypeByBrand(brand: string): ExtensionTypeLookupResult | undefined;
756
+ /**
757
+ * Look up a custom type by its TypeScript symbol identity.
758
+ *
759
+ * Built from `defineCustomType<T>()` type parameter extraction in the config file.
760
+ * This is the most precise detection path — it uses `ts.Symbol` identity, which is
761
+ * immune to import aliases and name collisions.
762
+ *
763
+ * Returns `undefined` until {@link MutableExtensionRegistry.setSymbolMap} has been
764
+ * called (i.e., before the TypeScript program is available), or when the symbol is
765
+ * not registered via a type parameter.
766
+ *
767
+ * @param symbol - The canonical TypeScript symbol to look up.
768
+ */
769
+ findTypeBySymbol(symbol: ts.Symbol): ExtensionTypeLookupResult | undefined;
723
770
  /**
724
771
  * Look up a custom constraint registration by its fully-qualified constraint ID.
725
772
  *
@@ -757,6 +804,22 @@ export declare interface ExtensionRegistry {
757
804
  */
758
805
  declare type ExtensionTypeKind = "primitive" | "enum" | "array" | "object" | "record" | "union" | "reference" | "dynamic" | "custom";
759
806
 
807
+ /**
808
+ * The result of a successful extension type lookup.
809
+ *
810
+ * Returned by {@link ExtensionRegistry.findTypeByName},
811
+ * {@link ExtensionRegistry.findTypeByBrand}, and
812
+ * {@link ExtensionRegistry.findTypeBySymbol}.
813
+ *
814
+ * @public
815
+ */
816
+ export declare interface ExtensionTypeLookupResult {
817
+ /** The fully-qualified extension ID (e.g., "x-stripe/monetary"). */
818
+ readonly extensionId: string;
819
+ /** The custom type registration matched by this lookup. */
820
+ readonly registration: CustomTypeRegistration;
821
+ }
822
+
760
823
  /**
761
824
  * Field configuration option constraints - control which field options are allowed.
762
825
  *
@@ -1846,6 +1909,28 @@ export declare interface MixedAuthoringSchemas {
1846
1909
  readonly uiSchema: UISchema;
1847
1910
  }
1848
1911
 
1912
+ /**
1913
+ * Mutable extension registry used internally by the build pipeline.
1914
+ *
1915
+ * Extends {@link ExtensionRegistry} with `setSymbolMap`, which must be called
1916
+ * after the TypeScript program is created. Consumer code should accept only
1917
+ * the read-only {@link ExtensionRegistry} interface.
1918
+ *
1919
+ * @public
1920
+ */
1921
+ export declare interface MutableExtensionRegistry extends ExtensionRegistry {
1922
+ /**
1923
+ * Sets the symbol map built from config AST analysis.
1924
+ *
1925
+ * Called after the TypeScript program is created and the config file is analyzed.
1926
+ * Prior to this call, {@link ExtensionRegistry.findTypeBySymbol} always returns
1927
+ * `undefined`.
1928
+ *
1929
+ * @param map - A map from canonical `ts.Symbol` to the matching registry entry.
1930
+ */
1931
+ setSymbolMap(map: Map<ts.Symbol, ExtensionTypeLookupResult>): void;
1932
+ }
1933
+
1849
1934
  export { NumberField }
1850
1935
 
1851
1936
  export { ObjectField }
@@ -2080,6 +2165,22 @@ export declare interface StaticSchemaGenerationOptions {
2080
2165
  readonly metadata?: MetadataPolicyInput | undefined;
2081
2166
  /** Discriminator-specific schema generation behavior. */
2082
2167
  readonly discriminator?: DiscriminatorResolutionOptions | undefined;
2168
+ /**
2169
+ * Absolute path to the FormSpec config file (e.g., `formspec.config.ts`).
2170
+ *
2171
+ * When provided alongside a `config` that includes extensions, the build
2172
+ * pipeline includes the config file in the TypeScript program and extracts
2173
+ * `defineCustomType<T>()` type parameters. This enables symbol-based custom
2174
+ * type detection — the most precise resolution path, immune to import aliases
2175
+ * and name collisions.
2176
+ *
2177
+ * Obtain this from `loadFormSpecConfig()`:
2178
+ * ```typescript
2179
+ * const { config, configPath } = await loadFormSpecConfig();
2180
+ * await generateSchemas({ filePath, typeName, config, configPath, errorReporting: "throw" });
2181
+ * ```
2182
+ */
2183
+ readonly configPath?: string | undefined;
2083
2184
  }
2084
2185
 
2085
2186
  export { TextField }