declare interface ArrayExpression extends BaseNode { type: 'ArrayExpression'; elements: (Expression | SpreadElement | null)[]; } declare interface ArrayPattern extends BaseNode { type: 'ArrayPattern'; elements: (Pattern | null)[]; typeAnnotation?: TSTypeAnnotation; } declare interface AssignmentPattern extends BaseNode { type: 'AssignmentPattern'; left: Pattern; right: Expression; } declare interface BaseNode { type: string; loc?: SourceLocation; range?: [number, number]; start?: number; end?: number; } declare interface BlockStatement extends BaseNode { type: 'BlockStatement'; body: Statement[]; } declare interface CallExpression extends BaseNode { type: 'CallExpression'; callee: Expression; arguments: Expression[]; typeParameters?: TSTypeParameterInstantiation; } declare interface ClassBody extends BaseNode { type: 'ClassBody'; body: ClassElement[]; } declare interface ClassDeclaration extends BaseNode { type: 'ClassDeclaration'; id: Identifier | null; superClass: Expression | null; superTypeParameters?: TSTypeParameterInstantiation; superTypeArguments?: TSTypeParameterInstantiation; body: ClassBody; decorators?: Decorator[]; } declare type ClassElement = PropertyDefinition | MethodDefinition_2; /** * Classify an identifier as money, a rate, or neither. * * The head noun is the last word, or the last word before a trailing qualifier * such as `Paid`. A rate word anywhere in the name wins outright: `taxRate` is * a rate even though `tax` is money, and that precedence is what stops the two * vocabularies from fighting over the same field. */ export declare function classifyNumericFieldName(name: string): NumericPrecisionKind | undefined; declare interface Decorator extends BaseNode { type: 'Decorator'; expression: Expression; } /** * Discover authored source files with the bounded policy shared by every * scanner entry point, including ManifestBuilder's preflight (#2275). */ export declare function discoverSourceFiles(options: SourceDiscoveryOptions): Promise; declare interface ExportDefaultDeclaration extends BaseNode { type: 'ExportDefaultDeclaration'; declaration: Statement | Expression; } declare interface ExportNamedDeclaration extends BaseNode { type: 'ExportNamedDeclaration'; declaration: Statement | null; specifiers: ExportSpecifier[]; } declare interface ExportSpecifier extends BaseNode { type: 'ExportSpecifier'; local: Identifier; exported: Identifier; } declare type Expression = Identifier | Literal | CallExpression | MemberExpression | ObjectExpression | ArrayExpression | NewExpression | UnaryExpression; /** * Describes a pre-loaded manifest from an installed SMRT package, used by * {@link InheritanceResolver} to resolve base classes that originate outside * the local project source. * * Build tooling (e.g. the SMRT CLI / vitest plugin) loads each installed * `@happyvertical/smrt-*` package's `manifest.json` and converts it into an * `ExternalManifest` before passing it to the scanner. * * @see {@link OxcScannerOptions.externalManifests} * @see {@link InheritanceResolver.addExternalManifest} */ export declare interface ExternalManifest { /** npm package name, e.g. `'@happyvertical/smrt-profiles'`. */ packageName: string; /** SemVer version string of the installed package. */ packageVersion: string; /** All class definitions exported by the package, keyed by class name. */ classes: Map; } /* Excluded from this release type: extractSmrtImports */ declare interface FieldDefinition { type: 'text' | 'decimal' | 'boolean' | 'integer' | 'datetime' | 'json' | 'foreignKey' | 'crossPackageRef' | 'oneToMany' | 'manyToMany' | 'meta'; required?: boolean; default?: unknown; min?: number; max?: number; maxLength?: number; minLength?: number; related?: string; description?: string; _meta?: Record; transient?: boolean; /** Sensitive value — excluded from public serialization + where filtering. */ sensitive?: boolean; /** Read-only over generated write surfaces — stripped from create/update bodies. */ readonly?: boolean; /** Permission slug required before the field is included in public reads. */ readPermission?: string; } /** * Result produced by {@link ManifestAdapter.inferFieldType} for a single field. * * In addition to the inferred `type`, carries the `source` of the inference * so callers can distinguish authoritative decorator-driven results from * heuristic guesses and provide better diagnostics. * * @see {@link InferredFieldType} for valid `type` values. * @see {@link ManifestAdapter.inferFieldType} for inference priority rules. */ export declare interface FieldTypeInference { /** Inferred SMRT type */ type: InferredFieldType; /** Related class for relationship types */ related?: string; /** Default value if extractable */ defaultValue?: unknown; /** Whether field is required */ required: boolean; /** Inference source for debugging */ source: 'helper' | 'decorator' | 'annotation' | 'heuristic' | 'default'; /** Underlying type for meta fields (e.g., 'string' inside Meta) */ underlyingType?: InferredFieldType; /** * Decorator-derived metadata that should be merged into the manifest * field's `_meta` object. Used by `@crossPackageRef`, `@manyToMany`, * `@meta` to carry options (`validate`, `through`, `indexed`, `idType`, * etc.) that don't fit on the top-level FieldDefinition. */ _meta?: Record; } /** * Result from scanning a single file */ export declare interface FileScanResult { /** Source file path */ filePath: string; /** Classes found in file */ classes: RawClassDefinition[]; /** Scan errors */ errors: ScanError[]; /** Parse time in milliseconds */ parseTimeMs: number; /** Type alias declarations found in file (name → resolved type string) */ typeAliases: Record; /** SMRT package imports found in file (package name → Set of imported class names) */ smrtImports?: Map>; } declare interface FunctionExpression extends BaseNode { type: 'FunctionExpression'; async: boolean; params: Pattern[]; returnType?: TSTypeAnnotation; body: BlockStatement; } declare interface Identifier extends BaseNode { type: 'Identifier'; name: string; typeAnnotation?: TSTypeAnnotation; optional?: boolean; } declare interface ImportDeclaration extends BaseNode { type: 'ImportDeclaration'; specifiers?: ImportSpecifierLike[]; source: Literal; } declare interface ImportDefaultSpecifier extends BaseNode { type: 'ImportDefaultSpecifier'; local: Identifier; } declare interface ImportNamespaceSpecifier extends BaseNode { type: 'ImportNamespaceSpecifier'; local: Identifier; } declare interface ImportSpecifier extends BaseNode { type: 'ImportSpecifier'; imported: Identifier; local: Identifier; } declare type ImportSpecifierLike = ImportSpecifier | ImportNamespaceSpecifier | ImportDefaultSpecifier; /** * The set of SMRT column types that the scanner can infer for a field. * * | Value | DB column type | Notes | * |---|---|---| * | `text` | `TEXT` / `VARCHAR` | Default for `string` and unknown types | * | `integer` | `INTEGER` | `number` with `= 0` initialiser | * | `decimal` | `DECIMAL` | `number` with `= 0.0` initialiser | * | `boolean` | `BOOLEAN` | `boolean` annotation or literal initialiser | * | `datetime` | `DATETIME` | `Date` annotation | * | `json` | `JSON` / `TEXT` | Arrays, `Record<>`, object types | * | `foreignKey` | `UUID` by default (FK column) | `@foreignKey(Class)` decorator | * | `crossPackageRef` | `UUID` by default (no FK constraint) | `@crossPackageRef('@pkg:Class')` decorator | * | `oneToMany` | — (virtual) | `@oneToMany(Class)` decorator | * | `manyToMany` | — (virtual) | `@manyToMany(Class)` decorator | * | `meta` | Stored in `_meta_data` | STI child field wrapped in `Meta` | * | `unknown` | — | Could not be determined | * * @see {@link FieldTypeInference} for the full inference result shape. */ export declare type InferredFieldType = 'text' | 'integer' | 'decimal' | 'boolean' | 'datetime' | 'json' | 'foreignKey' | 'crossPackageRef' | 'oneToMany' | 'manyToMany' | 'meta' | 'unknown'; /** * Resolves class inheritance chains and STI hierarchies from raw OXC scan output. * * After {@link OxcScanner} parses files, `InheritanceResolver` builds a * class map from the raw definitions and walks each class's `extends` chain * to produce fully-resolved {@link ResolvedClassDefinition} objects. * * Key responsibilities: * - Walking extends chains across local classes and external package manifests. * - Detecting which classes participate in STI (Single Table Inheritance). * - Merging fields from ancestor classes for STI subclasses (base fields first). * - Caching resolved chains to avoid repeated traversals. * * Framework base classes (`SmrtObject`, `SmrtClass`, `SmrtCollection`) are * recognized without needing to appear in source files. * * @example * ```typescript * import { InheritanceResolver } from '@happyvertical/smrt-scanner'; * * const resolver = new InheritanceResolver({ baseClasses: ['MyBaseClass'] }); * resolver.addClasses(rawClasses); * const resolved = resolver.resolveAll(); * ``` * * @see {@link OxcScanner} which owns and drives this resolver internally. */ export declare class InheritanceResolver { /** Map of className -> RawClassDefinition */ private classMap; /** External package manifests for cross-package resolution */ private externalManifests; /** Known base classes (user-provided) */ private knownBaseClasses; /** Cache of resolved inheritance chains */ private chainCache; /** * Create a new `InheritanceResolver`. * * @param options.baseClasses - Additional class names to treat as known * framework base classes (beyond the built-in `SmrtObject`, `SmrtClass`, * and `SmrtCollection`). * @param options.externalManifests - Pre-loaded external package manifests * keyed by package name, used for cross-package parent class resolution. */ constructor(options?: { baseClasses?: string[]; externalManifests?: Map; }); /** * Register raw class definitions from a scan pass. * * Adds each class to the internal class map by `className`. Calling this * clears the inheritance chain cache so subsequent calls to * {@link resolveAll} or {@link resolveInheritanceChain} reflect the new * classes. * * @param classes - Array of {@link RawClassDefinition} objects from * {@link ScanResults.classes}. */ addClasses(classes: RawClassDefinition[]): void; /** * Register an external package manifest for cross-package base class resolution. * * Clears the chain cache after registration so re-resolution picks up the * new definitions. * * @param manifest - External package manifest providing class definitions * that may appear as base classes in the local project. * * @see {@link ExternalManifest} */ addExternalManifest(manifest: ExternalManifest): void; /** * Resolve all registered classes and return fully-resolved definitions. * * A class is included in the output if it either: * 1. Has an `@smrt()` decorator, or * 2. Directly or transitively extends a framework base class * (`SmrtObject`, `SmrtClass`, `SmrtCollection`) — this captures * collection classes such as `class MeetingCollection extends * SmrtCollection` that do not carry `@smrt()` themselves. * * @returns An array of {@link ResolvedClassDefinition} — one entry per * eligible class, with inheritance chain, STI metadata, and merged fields * populated. * * @see {@link resolve} to resolve a single class definition. */ resolveAll(): ResolvedClassDefinition[]; /** * Check if a class extends a framework base class * (SmrtObject, SmrtClass, or SmrtCollection) */ private extendsFrameworkBase; /** * Resolve a single raw class definition into a fully-resolved definition. * * Computes the inheritance chain, determines the effective table strategy, * detects STI membership, and merges ancestor fields for STI classes. * * @param classDef - The raw class definition to resolve. * @returns A {@link ResolvedClassDefinition} with all inherited metadata * applied. The `packageName` field is left as `null` and must be set by * the caller (e.g. {@link ManifestAdapter}). * * @see {@link resolveAll} to resolve every registered class at once. */ resolve(classDef: RawClassDefinition): ResolvedClassDefinition; /** * Resolve the full inheritance chain for a named class, from the root base * class down to the named class itself. * * Results are memoised in an internal cache that is cleared whenever * {@link addClasses} or {@link addExternalManifest} is called. * * @param className - Name of the class to resolve. * @returns An ordered array of class names starting from the furthest * ancestor and ending with `className`. * * @example * ```typescript * // Given: class Article extends Content, class Content extends SmrtObject * resolver.resolveInheritanceChain('Article'); * // => ['SmrtObject', 'Content', 'Article'] * ``` */ resolveInheritanceChain(className: string): string[]; /** * Look up a class definition by name, searching in priority order: * 1. Local classes added via {@link addClasses}. * 2. External package manifests added via {@link addExternalManifest}. * 3. Built-in framework base classes (`SmrtObject`, `SmrtClass`, * `SmrtCollection`) — returns a minimal stub definition so chain walking * can terminate cleanly. * * @param className - Class name to look up. * @returns The {@link RawClassDefinition} if found, or `null` if the class * is unknown to the resolver. */ findClassDefinition(className: string): RawClassDefinition | null; /** * Find the STI root class in a resolved inheritance chain. * * Walks the chain from base to leaf and returns the name of the first class * whose `@smrt()` decorator explicitly declares `tableStrategy: 'sti'`. * * @param chain - Ordered inheritance chain (base → leaf) as returned by * {@link resolveInheritanceChain}. * @returns The class name of the STI root, or `null` if no class in the * chain uses `tableStrategy: 'sti'`. */ findSTIBase(chain: string[]): string | null; /** * Determine the effective table strategy (`'sti'` or `'cti'`) for a class. * * Resolution order: * 1. The class's own `@smrt({ tableStrategy })` declaration, if present. * 2. The nearest ancestor that declares `tableStrategy: 'sti'` — STI is * inherited automatically by all subclasses. * 3. Defaults to `'cti'` if no STI ancestor is found. * * @param classDef - Raw class definition whose strategy is being determined. * @param chain - Pre-resolved inheritance chain for `classDef` (base → leaf). * @returns `'sti'` or `'cti'`. */ determineTableStrategy(classDef: RawClassDefinition, chain: string[]): 'sti' | 'cti'; /** * Merge fields from all classes in an STI inheritance chain. * * Iterates from the root base class to the leaf class so that base class * fields appear first in the returned array. If a field name is declared in * both an ancestor and a descendant, the ancestor's definition takes * precedence (first-seen wins), preserving the base-class column layout. * * @param chain - Ordered inheritance chain (base → leaf) as returned by * {@link resolveInheritanceChain}. * @returns A deduplicated, ordered array of {@link RawFieldDefinition} * covering every field in the STI hierarchy. */ mergeFieldsForSTI(chain: string[]): RawFieldDefinition[]; /** * Return all known descendants of a class. * * Useful for STI schema generation where the base table must accommodate * columns from every subclass. * * @param className - The ancestor class name to search from. * @returns An array of class names (local classes only) whose resolved * inheritance chain includes `className`. Does not include `className` * itself. */ getDescendants(className: string): string[]; /** * Check whether a class participates in an STI hierarchy. * * @param className - Name of the class to check. * @returns `true` if any class in the resolved inheritance chain declares * `tableStrategy: 'sti'`, `false` otherwise. */ isSTIClass(className: string): boolean; /** * Return aggregate statistics about the classes registered with this resolver. * * @returns An object with: * - `totalClasses` — total number of classes in the class map. * - `smrtClasses` — classes that carry `@smrt()`. * - `stiClasses` — `@smrt()` classes in an STI hierarchy. * - `maxInheritanceDepth` — length of the deepest inheritance chain among * `@smrt()` classes. */ getStats(): { totalClasses: number; smrtClasses: number; stiClasses: number; maxInheritanceDepth: number; }; } /** * Report every persisted `number` field whose declared precision contradicts * its name — money declared decimal, or a rate declared integer. * * @param classes - Raw class definitions from `parseFile` / `OxcScanner`. * @param sourceText - Optional contents of the scanned file, used only to * recover declaration line numbers the AST does not carry. * @returns One finding per offending field, in declaration order. */ export declare function lintNumericPrecision(classes: RawClassDefinition[], sourceText?: string): NumericPrecisionFinding[]; declare interface Literal extends BaseNode { type: 'Literal'; value: string | number | boolean | null | RegExp | bigint; raw?: string; } /** * Converts OXC scanner output into the smrt-core `SmartObjectManifest` format * consumed by code generators, the Vitest plugin, and the SMRT CLI. * * The adapter handles field type inference (applying the `0` vs `0.0` integer / * decimal heuristic), decorator interpretation (`@foreignKey`, `@oneToMany`, * `@manyToMany`, `@field`), type alias resolution, STI `Meta` unwrapping, * static property capture (`uiSlots`, `adminRoutes`), and qualified name * generation for namespace isolation across packages. * * @example * ```typescript * import { OxcScanner, ManifestAdapter } from '@happyvertical/smrt-scanner'; * * const scanner = new OxcScanner({ cwd: process.cwd() }); * const { results, resolved } = await scanner.scanAndResolve(); * * const adapter = new ManifestAdapter(); * const manifest = adapter.toManifest(resolved, { * packageName: '@my-org/my-package', * packageVersion: '1.0.0', * typeAliases: results.typeAliases, * }); * ``` * * @see {@link OxcScanner} for producing the `ResolvedClassDefinition[]` input. * @see {@link ResolvedClassDefinition} for the shape of each input element. */ export declare class ManifestAdapter { private typeAliases; private _aliasDepth?; /** * Convert an array of resolved class definitions into a `SmartObjectManifest`. * * Each class is converted to a `SmartObjectDefinition` via * {@link toSmartObjectDefinition} and stored under its qualified name key * (e.g. `@my-org/my-package:MyClass`) when `packageName` is provided, or * under its lowercased class name otherwise. * * @param resolved - Resolved class definitions from {@link OxcScanner.resolve} * or {@link OxcScanner.scanAndResolve}. * @param options.packageName - npm package name used to generate qualified * class names for namespace isolation across multi-package projects. * @param options.packageVersion - Package version recorded in the manifest * metadata. * @param options.typeAliases - Map of type alias names to their resolved type * strings (from {@link ScanResults.typeAliases}). Used to resolve custom * types like `type Status = 'active' | 'inactive'` during field inference. * @returns A complete `SmartObjectManifest` ready for serialisation. * * @example * ```typescript * const manifest = adapter.toManifest(resolved, { * packageName: '@my-org/my-package', * packageVersion: '1.0.0', * typeAliases: results.typeAliases, * }); * fs.writeFileSync('manifest.json', JSON.stringify(manifest, null, 2)); * ``` */ toManifest(resolved: ResolvedClassDefinition[], options?: { packageName?: string; packageVersion?: string; typeAliases?: Record; }): SmartObjectManifest; /** * Convert a single resolved class definition to a `SmartObjectDefinition`. * * Handles: * - Static property capture (`uiSlots`, `adminRoutes`) with child-wins * semantics for overridden statics. * - Field conversion (non-static public fields only) via {@link convertField}. * - Method conversion (public instance/static methods) via {@link convertMethod}. * - Collection name pluralisation. * - Qualified name generation when `packageName` is supplied. * * @param classDef - A fully-resolved class definition. * @param options.packageName - Package name used to build the qualified class * name (`@pkg:ClassName`). * @param options.packageVersion - Package version (informational, stored in * the definition). * @returns A `SmartObjectDefinition` ready to be stored in a manifest. * * @see {@link toManifest} for the bulk conversion entry point. */ toSmartObjectDefinition(classDef: ResolvedClassDefinition, options?: { packageName?: string; packageVersion?: string; }): SmartObjectDefinition; /** * Framework internal fields that should NOT be included in manifests * These are SmrtObject internals used by the framework, not user-defined fields */ private static readonly FRAMEWORK_INTERNAL_FIELDS; /** * Convert a single raw field definition to a manifest `FieldDefinition`. * * Returns `null` for fields that should be omitted from the manifest: * - `private` or `protected` fields. * - Framework-internal fields (`_tableName`, `_db`, `_ai`, etc.). * * Delegates type inference to {@link inferFieldType} and applies additional * post-processing: * - Marks fields with `Function` type annotation as `transient`. * - Marks fields with `@field({ transient: true })` decorator as `transient`. * - Populates `_meta.underlyingType` for STI `Meta` fields. * * @param field - Raw field definition from a scanned class. * @returns A `FieldDefinition` for the manifest, or `null` if the field * should be excluded. * * @see {@link inferFieldType} for the type inference logic. */ convertField(field: RawFieldDefinition): FieldDefinition | null; /** * Infer the SMRT field type and required flag from a raw field definition. * * Inference is attempted in the following priority order: * 1. **Field helper call in initializer** — currently always returns `null` * (field helpers removed); reserved for future use. * 2. **Decorator** — `@foreignKey`, `@oneToMany`, `@manyToMany`, `@field({ type })`. * 3. **Type annotation** — `string` → `text`, `number` with `0` vs `0.0` * heuristic → `integer` / `decimal`, `boolean`, `Date` → `datetime`, * arrays → `json`, `Record<>` / `object` → `json`, union types with * `null`, inline string/number literal unions, `Meta` wrapper, * and type alias resolution (up to depth 5). * 4. **Numeric literal without annotation** — `version = 1` → `integer`. * 5. **Boolean literal without annotation** — `isRead = false` → `boolean`. * 6. **Default** — falls back to `text`. * * @param field - The raw field definition to analyse. * @returns A {@link FieldTypeInference} describing the inferred type, * required flag, default value, related class name (for relationships), * and the inference source for debugging. * * @see {@link FieldTypeInference} for the result shape. * @see {@link InferredFieldType} for valid type values. */ inferFieldType(field: RawFieldDefinition): FieldTypeInference; /** * Infer type from field helper call (removed) * * Field helpers have been removed in favor of decorators and TypeScript types: * - Use TypeScript types: name: string = '', price: number = 0.0 * - Use @field() decorator for constraints: @field({ required: true }) * - Use @foreignKey(), @oneToMany(), @manyToMany() decorators for relationships */ private inferFromHelper; /** * Infer type from field decorator */ private inferFromDecorator; private extractFieldDecoratorOptions; private parseReportFieldDecorator; private parseFieldDecoratorOptions; private normalizeFieldType; /** * Infer type from TypeScript type annotation */ private inferFromAnnotation; /** * Parse default value from initializer string */ private parseDefaultValue; /** * Convert a raw method definition to a manifest `MethodDefinition`. * * Returns `null` for `private` or `protected` methods, which are excluded * from the manifest. Parameters are mapped to the manifest parameter shape * and default values are parsed via `parseDefaultValue`. * * @param method - Raw method definition from a scanned class. * @returns A manifest-compatible `MethodDefinition`, or `null` if the method * should be excluded. */ convertMethod(method: RawMethodDefinition): MethodDefinition | null; /** * Simple pluralization for collection names. * * This produces the manifest's `collection` label only; the authoritative DDL * table name is derived independently by core (`classnameToTablename` → * the `pluralize` library), so this needs to stay self-consistent rather than * cover every irregular plural. Note the `y → ies` rule fires only after a * consonant, so vowel+y words pluralise correctly (`Day` → `days`, not * `daies`). */ private pluralize; } declare interface MemberExpression extends BaseNode { type: 'MemberExpression'; object: Expression; property: Expression; computed: boolean; } declare interface MethodDefinition { name: string; async: boolean; parameters: Array<{ name: string; type: string; optional: boolean; default?: unknown; }>; returnType: string; description?: string; isStatic: boolean; isPublic: boolean; } declare interface MethodDefinition_2 extends BaseNode { type: 'MethodDefinition'; key: Expression; value: FunctionExpression; kind: 'constructor' | 'method' | 'get' | 'set'; computed: boolean; static: boolean; accessibility?: 'public' | 'private' | 'protected'; } declare interface NewExpression extends BaseNode { type: 'NewExpression'; callee: Expression; arguments: Expression[]; } export declare function normalizeGlobSeparators(pattern: string, pathSeparator?: "\\" | "/"): string; /** One field whose declared precision contradicts its name. */ export declare interface NumericPrecisionFinding { /** `money` wants INTEGER minor units; `rate` wants DECIMAL. */ kind: NumericPrecisionKind; /** Declaring class, e.g. `Invoice`. */ className: string; /** Field name, e.g. `totalAmount`. */ fieldName: string; /** File the class was scanned from. */ filePath: string; /** * 1-based line of the field declaration, or `0` when it could not be * resolved. The OXC AST nodes this scanner consumes do not carry `loc`, so * the line is recovered from the source text when a caller passes it to * {@link lintNumericPrecision}. */ line: number; /** The initializer that triggered the finding. */ initializer: string; /** Human-readable explanation naming the rule. */ message: string; /** The accepted fixes. */ remedy: string; } /** Which rule a field falls under, if any. */ export declare type NumericPrecisionKind = 'money' | 'rate'; declare interface ObjectExpression extends BaseNode { type: 'ObjectExpression'; properties: (Property | SpreadElement)[]; } declare interface ObjectPattern extends BaseNode { type: 'ObjectPattern'; properties: (Property | RestElement)[]; typeAnnotation?: TSTypeAnnotation; } /** * Prunes that always apply, on top of whatever `exclude` a caller passes. * * `exclude` replaces {@link DEFAULT_EXCLUDE} wholesale, so every caller that * narrowed the excludes also silently reopened `node_modules` — and installed * dependencies are never a project's own `@smrt()` sources. Dot directories are * generated or tool state (`.git`, `.svelte-kit`, `.turbo`, `.vercel`, agent * scratch) and are pruned for the same reason: nothing authored lives there. * `**\/.*` keeps hidden FILES out of the result too, so turning `dot` on to * make these prunes work does not quietly widen what gets scanned. * * These are load-bearing for termination, not just for speed. See * {@link OxcScanner.discoverFiles}. */ /** * High-performance TypeScript scanner that discovers `@smrt()`-decorated * classes in a project's source files. * * Orchestrates the two-phase scan pipeline: * 1. **Phase 1 — Parse** (`scan()`): uses OXC (Rust) to parse TypeScript files * in parallel and extract raw class, field, method, and decorator metadata. * 2. **Phase 2 — Resolve** (`resolve()`): walks inheritance chains, detects STI * hierarchies, and merges fields from ancestor classes. * * The common path is {@link scanAndResolve} which runs both phases in sequence. * * @example * ```typescript * import { OxcScanner } from '@happyvertical/smrt-scanner'; * * const scanner = new OxcScanner({ * cwd: process.cwd(), * include: ['src/**\/*.ts'], * exclude: ['**\/*.test.ts'], * }); * * const { results, resolved } = await scanner.scanAndResolve(); * console.log(`Found ${resolved.length} SMRT classes in ${results.fileCount} files`); * ``` * * @see {@link OxcScannerOptions} for all available configuration options. * @see {@link scanDirectory} for a one-liner convenience wrapper. */ export declare class OxcScanner { private options; private resolver; private scanResults; /** * Create a new `OxcScanner` with the given options. * * All options are optional. By default the scanner targets every `.ts` and * `.tsx` file under `process.cwd()`, excluding `node_modules`, `dist`, * `build`, declaration files, and test files. * * @param options - Scanner configuration. See {@link OxcScannerOptions}. */ constructor(options?: OxcScannerOptions); /** * Phase 1 — Discover and parse TypeScript files using OXC. * * Uses `fast-glob` to enumerate matching files and then parses them in * parallel with OXC (Rust). The raw class definitions are registered with * the internal {@link InheritanceResolver} for use in the subsequent * {@link resolve} call. * * @returns A {@link ScanResults} object containing all classes found, any * parse errors, accumulated type aliases, SMRT import metadata, and * aggregate timing information. * * @example * ```typescript * const scanner = new OxcScanner({ cwd: '/project' }); * const results = await scanner.scan(); * console.log(`Parsed ${results.fileCount} files in ${results.totalParseTimeMs.toFixed(1)}ms`); * ``` */ scan(): Promise; /** * Phase 2 — Resolve inheritance chains for all scanned classes. * * Must be called after {@link scan}. Walks each class's extends chain, * detects STI hierarchies, merges ancestor fields for STI subclasses, and * marks framework base classes. * * @returns An array of {@link ResolvedClassDefinition} objects — one for * every class that either carries `@smrt()` or extends a framework base * class (`SmrtObject`, `SmrtClass`, `SmrtCollection`). * * @throws {Error} If called before {@link scan}. * * @see {@link scanAndResolve} to run both phases in one call. */ resolve(): ResolvedClassDefinition[]; /** * Run both scan phases in a single call. * * Equivalent to calling `await scanner.scan()` followed by * `scanner.resolve()`. This is the most common entry point for callers * that want the fully-resolved manifest-ready data in one step. * * @returns An object with: * - `results` — raw {@link ScanResults} from Phase 1. * - `resolved` — array of {@link ResolvedClassDefinition} from Phase 2. * * @example * ```typescript * const scanner = new OxcScanner({ cwd: '/project/src' }); * const { results, resolved } = await scanner.scanAndResolve(); * // resolved is ready to pass to ManifestAdapter.toManifest() * ``` * * @see {@link ManifestAdapter} to convert `resolved` into a manifest JSON. */ scanAndResolve(): Promise<{ results: ScanResults; resolved: ResolvedClassDefinition[]; }>; /** * Register an external package manifest for cross-package base class resolution. * * When a project class extends a class defined in an installed SMRT package, * the resolver needs access to that package's class definitions to walk the * full inheritance chain. Call this method with each external package's * {@link ExternalManifest} before calling {@link scan} or {@link resolve}. * * @param manifest - The external manifest to register, including `packageName`, * `packageVersion`, and a `classes` map keyed by class name. * * @see {@link ExternalManifest} */ addExternalManifest(manifest: ExternalManifest): void; /** * Scan all discovered files for @happyvertical/smrt-* imports. * Returns a map of package name → Set of imported class names. * * Used for tree-shaking: only external objects that are actually imported * in the project's source files will be included in the manifest. * * Must be called after scan() or as part of scanAndResolve(). * * @example * ```typescript * const scanner = new OxcScanner({ cwd: process.cwd() }); * await scanner.scan(); * const imports = scanner.scanSmrtImports(); * // Map { '@happyvertical/smrt-profiles' => Set { 'Person', 'Organization' } } * ``` */ scanSmrtImports(): Map>; /** * Return aggregate statistics about the last scan. * * Can be called after {@link scan} has completed. Returns counts useful for * diagnostics and the `--stats` CLI flag. * * @returns An object with: * - `totalClasses` — total class declarations seen (including non-SMRT). * - `smrtClasses` — classes with `@smrt()` decorator. * - `stiClasses` — SMRT classes participating in an STI hierarchy. * - `maxInheritanceDepth` — length of the deepest inheritance chain. * - `fileCount` — number of files scanned. * - `parseTimeMs` — total wall-clock parse time in milliseconds. */ getStats(): { totalClasses: number; smrtClasses: number; stiClasses: number; maxInheritanceDepth: number; fileCount: number; parseTimeMs: number; }; /** * Discover files to scan using fast-glob. * * Two settings here decide whether discovery terminates at all when the * scanner is pointed at an application root rather than a package `src/`: * * - `dot: true`. Without it a `**` in an ignore pattern cannot cross a * dot segment, so `**\/node_modules/**` prunes `node_modules` at the root * but NOT `.svelte-kit/…/node_modules` or any other `node_modules` under a * dot directory. Those subtrees were then walked in full and every entry * discarded — unbounded work that could never produce a match. * - `followSymbolicLinks: false`. pnpm materializes `node_modules` as a * symlink graph with cycles, so a link-following walk revisits the same * real directories once per path that reaches them. * * Together they were enough to exhaust a 4 GB heap on a consumer app that * installs the published packages (#2275). * * Patterns are rewritten relative to `cwd` first. fast-glob matches `ignore` * in whatever space the patterns use, so an absolute pattern would hand * `**\/.*\/**` the project's own ancestors — a checkout under `~/.worktrees` * or `~/.cache` would then match nothing at all, silently. */ private discoverFiles; /** * Parse a single file with timing */ private parseFileWithTiming; } /** * Configuration options for {@link OxcScanner}. * * All fields are optional; reasonable defaults are applied when omitted. * * @see {@link OxcScanner} */ export declare interface OxcScannerOptions { /** Glob patterns to include */ include?: string[]; /** Glob patterns to exclude */ exclude?: string[]; /** Base directory for scanning */ cwd?: string; /** Path to tsconfig.json for module resolution */ tsconfig?: string; /** Whether to follow imports to find base classes */ followImports?: boolean; /** Known base classes (avoid resolution) */ baseClasses?: string[]; /** Include private methods in output */ includePrivateMethods?: boolean; /** Include static methods in output */ includeStaticMethods?: boolean; /** External package manifests for base class resolution */ externalManifests?: Map; /** * Follow symbolic links while discovering files. Defaults to `false`. * * A package's own sources are real files, while a pnpm `node_modules` is a * symlink graph with cycles: every store entry links back out to its * siblings, so a link-following walk reaches the same real directory once per * path that leads to it and never terminates in practice. Discovery therefore * stays on real directories unless a caller explicitly opts back in. */ followSymbolicLinks?: boolean; } /** * Parse a single TypeScript file and extract SMRT class definitions. * * Reads the file from disk, runs oxc-parser on it, and returns all class * definitions found, any parse errors, accumulated type aliases, and * `@happyvertical/smrt-*` import metadata. * * @param filePath - Absolute path to the `.ts` or `.tsx` file to parse. * @returns A {@link FileScanResult} containing classes, errors, type aliases, * SMRT imports, and timing information for the file. * * @example * ```typescript * import { parseFile } from '@happyvertical/smrt-scanner'; * * const result = parseFile('/project/src/models/Product.ts'); * console.log(result.classes.map((c) => c.className)); * // ['Product', 'ProductCollection'] * ``` * * @see {@link parseSource} to parse a source string directly (e.g. in tests). */ export declare function parseFile(filePath: string): FileScanResult; /** * Parse TypeScript source text directly and extract SMRT class definitions. * * Identical to {@link parseFile} but accepts a source string instead of a * file path. Primarily used in tests and tooling that constructs source * programmatically. * * @param sourceText - Raw TypeScript source code to parse. * @param filename - Virtual filename used to determine the parser language * mode (`.ts`, `.tsx`, `.js`, `.jsx`) and to populate `filePath` fields in * the result. Defaults to `'test.ts'`. * @returns A {@link FileScanResult} containing classes, errors, type aliases, * and SMRT import metadata extracted from the source text. * * @example * ```typescript * import { parseSource } from '@happyvertical/smrt-scanner'; * * const src = ` * import { smrt } from '@happyvertical/smrt-core'; * @smrt() * export class Widget extends SmrtObject { * label: string = ''; * } * `; * const result = parseSource(src, 'Widget.ts'); * console.log(result.classes[0].className); // 'Widget' * ``` * * @see {@link parseFile} to parse a file from disk. */ export declare function parseSource(sourceText: string, filename?: string): FileScanResult; declare type Pattern = Identifier | AssignmentPattern | RestElement | ObjectPattern | ArrayPattern; declare interface Position { line: number; column: number; } declare interface Property extends BaseNode { type: 'Property'; key: Expression; value: Expression | Pattern; kind: 'init' | 'get' | 'set'; method: boolean; shorthand: boolean; computed: boolean; } declare interface PropertyDefinition extends BaseNode { type: 'PropertyDefinition'; key: Expression; value: Expression | null; computed: boolean; static: boolean; readonly?: boolean; optional?: boolean; accessibility?: 'public' | 'private' | 'protected'; typeAnnotation?: TSTypeAnnotation; decorators?: Decorator[]; } /** * Qualified class name format: "@package/name:ClassName" * Uniquely identifies classes across packages. */ declare type QualifiedClassName = `${string}:${string}`; /** * Type definitions for OXC-based SMRT scanner * * This module defines: * 1. Raw types - Intermediate representation from OXC parsing (Phase 1) * 2. Resolved types - After inheritance resolution (Phase 2) * 3. Re-exports of smrt-core types for compatibility */ /** * Raw class definition extracted from OXC AST * Contains only syntactic information, no semantic resolution */ export declare interface RawClassDefinition { /** Class name as declared in source */ className: string; /** Absolute path to source file */ filePath: string; /** Parent class name from extends clause (null if none) */ extendsClause: string | null; /** Generic type argument from extends (e.g., "Meeting" from SmrtCollection) */ extendsTypeArg: string | null; /** Parsed @smrt() decorator configuration object */ decoratorConfig: RawDecoratorConfig | null; /** Has @smrt() decorator */ hasSmartDecorator: boolean; /** Class properties/fields */ fields: RawFieldDefinition[]; /** Class methods */ methods: RawMethodDefinition[]; /** Start line in source file */ startLine: number; /** End line in source file */ endLine: number; } /** * Raw decorator information */ export declare interface RawDecorator { /** Decorator name (e.g., "field", "foreignKey") */ name: string; /** Raw arguments as strings */ arguments: string[]; } /** * Raw @smrt() decorator configuration */ export declare interface RawDecoratorConfig { /** Table strategy: 'sti' | 'cti' */ tableStrategy?: 'sti' | 'cti'; /** Storage type for the generated id primary key */ idType?: 'uuid' | 'text'; /** Code-owned feature toggle declarations */ features?: Record; }>; /** API configuration */ api?: { include?: string[]; exclude?: string[]; }; /** CLI configuration */ cli?: boolean | { include?: string[]; exclude?: string[]; skipApiCheck?: boolean; http?: boolean; }; /** MCP configuration */ mcp?: { include?: string[]; exclude?: string[]; }; /** Raw config object for unknown properties */ [key: string]: unknown; } /** * Raw field definition from OXC AST */ export declare interface RawFieldDefinition { /** Field name */ name: string; /** TypeScript type annotation as string (e.g., "string", "number", "Date") */ typeAnnotation: string | null; /** Raw initializer expression as string */ initializer: string | null; /** For numeric literals: whether it contains a decimal point */ hasDecimalPoint: boolean; /** For numeric literals: the actual numeric value */ numericValue: number | null; /** Decorators applied to this field */ decorators: RawDecorator[]; /** Whether field is optional (has ?) */ optional: boolean; /** Whether field is static */ isStatic: boolean; /** Whether field is readonly */ readonly: boolean; /** Whether field is private/protected */ accessibility: 'public' | 'private' | 'protected'; /** Start line in source */ line: number; } /** * Raw method definition from OXC AST */ export declare interface RawMethodDefinition { /** Method name */ name: string; /** Whether method is async */ async: boolean; /** Whether method is static */ isStatic: boolean; /** Accessibility modifier */ accessibility: 'public' | 'private' | 'protected'; /** Method parameters */ parameters: RawParameterDefinition[]; /** Return type annotation as string */ returnType: string | null; /** JSDoc description if present */ description: string | null; /** Start line in source */ line: number; } /** * Raw parameter definition */ export declare interface RawParameterDefinition { /** Parameter name */ name: string; /** Type annotation as string */ type: string | null; /** Whether parameter is optional */ optional: boolean; /** Default value as string */ defaultValue: string | null; } /** Preserve relative glob escapes; normalize separators only after rewriting. */ export declare function relativeGlobToCwd(pattern: string, cwd: string): string; /** * Resolved class definition with inheritance chain */ export declare interface ResolvedClassDefinition extends RawClassDefinition { /** Full inheritance chain from base to this class */ inheritanceChain: string[]; /** STI base class name (if part of STI hierarchy) */ stiBase: string | null; /** Effective table strategy (inherited or declared) */ effectiveTableStrategy: 'sti' | 'cti'; /** Whether this class uses STI (convenience boolean) */ isSTI: boolean; /** Whether this class is a framework base class */ isFrameworkBase: boolean; /** All fields including inherited (for STI) */ allFields: RawFieldDefinition[]; /** Package this class belongs to (if external) */ packageName: string | null; } declare interface RestElement extends BaseNode { type: 'RestElement'; argument: Pattern; typeAnnotation?: TSTypeAnnotation; } /** * Scan error */ export declare interface ScanError { /** Error message */ message: string; /** Source file */ filePath: string; /** Line number (1-based) */ line?: number; /** Column number (1-based) */ column?: number; /** Error severity */ severity: 'error' | 'warning'; } /** * Result from scanning multiple files */ export declare interface ScanResults { /** All scanned files */ files: FileScanResult[]; /** All classes found (flattened) */ classes: RawClassDefinition[]; /** All errors (flattened) */ errors: ScanError[]; /** Total parse time in milliseconds */ totalParseTimeMs: number; /** Number of files scanned */ fileCount: number; /** Accumulated type aliases across all files */ typeAliases: Record; /** Accumulated SMRT package imports across all files (package name → Set of imported class names) */ smrtImports?: Map>; } declare interface SmartObjectConfig { tableStrategy?: 'sti' | 'cti'; idType?: 'uuid' | 'text'; features?: Record; }>; api?: { include?: string[]; exclude?: string[]; }; cli?: boolean | { include?: string[]; exclude?: string[]; skipApiCheck?: boolean; http?: boolean; }; mcp?: { include?: string[]; exclude?: string[]; }; [key: string]: unknown; } declare interface SmartObjectDefinition { name: string; className: string; qualifiedName?: QualifiedClassName; collection: string; filePath: string; packageName?: string; packageVersion?: string; importPath?: string; modulePath?: string; exportName?: string; collectionExportName?: string; fields: Record; methods: Record; decoratorConfig: SmartObjectConfig; extends?: string; extendsTypeArg?: string; staticProperties?: Record; } declare interface SmartObjectManifest { version: string; /** Always {@link MANIFEST_TIMESTAMP}: build output must be reproducible. */ timestamp: number; packageName?: string; packageVersion?: string; objects: Record; moduleType?: string; smrtDependencies?: string[]; } export declare interface SourceDiscoveryOptions { cwd: string; include: string[]; exclude: string[]; followSymbolicLinks?: boolean; } declare interface SourceLocation { start: Position; end: Position; } /** * Cheap pre-filter so callers can skip parsing files that cannot produce a * finding. * * Conservative by construction: every condition here is textually implied by a * finding. A reported field lives in a class carrying `@smrt` or * `extends Smrt…`, is initialized to a number, and has a money or rate word at * one of the boundaries the head-noun splitter cuts on. It may still return * `true` for a file the AST pass then clears — that is the intended direction. * * @param source - Full file contents. * @returns `false` only when the file provably cannot produce a finding. */ export declare function sourceMayContainNumericPrecisionIssue(source: string): boolean; /** * Split an identifier into lowercase words on camelCase, PascalCase, digits, * and underscores. `totalAmountCents` → `['total', 'amount', 'cents']`. */ export declare function splitIdentifierWords(name: string): string[]; declare interface SpreadElement extends BaseNode { type: 'SpreadElement'; argument: Expression; } declare type Statement = ClassDeclaration | ExportNamedDeclaration | ExportDefaultDeclaration | ImportDeclaration | TSTypeAliasDeclaration | TSEnumDeclaration | VariableDeclaration; declare interface TSAnyKeyword extends BaseNode { type: 'TSAnyKeyword'; } declare interface TSArrayType extends BaseNode { type: 'TSArrayType'; elementType: TSType; } declare interface TSBooleanKeyword extends BaseNode { type: 'TSBooleanKeyword'; } declare interface TSEnumBody extends BaseNode { type: 'TSEnumBody'; members: TSEnumMember[]; } declare interface TSEnumDeclaration extends BaseNode { type: 'TSEnumDeclaration'; id: Identifier; body?: TSEnumBody; members?: TSEnumMember[]; } declare interface TSEnumMember extends BaseNode { type: 'TSEnumMember'; initializer?: Expression; } declare interface TSFunctionType extends BaseNode { type: 'TSFunctionType'; } declare interface TSIndexSignature extends BaseNode { type: 'TSIndexSignature'; parameters: Identifier[]; typeAnnotation?: TSTypeAnnotation; } declare interface TSLiteralType extends BaseNode { type: 'TSLiteralType'; literal?: Literal; } declare interface TSMethodSignature extends BaseNode { type: 'TSMethodSignature'; key: Expression; params: Pattern[]; returnType?: TSTypeAnnotation; } declare interface TSNullKeyword extends BaseNode { type: 'TSNullKeyword'; } declare interface TSNumberKeyword extends BaseNode { type: 'TSNumberKeyword'; } declare interface TSPropertySignature extends BaseNode { type: 'TSPropertySignature'; key: Expression; typeAnnotation?: TSTypeAnnotation; optional?: boolean; readonly?: boolean; } declare interface TSQualifiedName extends BaseNode { type: 'TSQualifiedName'; left: Identifier | TSQualifiedName; right: Identifier; } declare interface TSStringKeyword extends BaseNode { type: 'TSStringKeyword'; } declare type TSType = TSStringKeyword | TSNumberKeyword | TSBooleanKeyword | TSAnyKeyword | TSVoidKeyword | TSNullKeyword | TSUndefinedKeyword | TSTypeReference | TSArrayType | TSUnionType | TSTypeLiteral | TSLiteralType | TSFunctionType; declare interface TSTypeAliasDeclaration extends BaseNode { type: 'TSTypeAliasDeclaration'; id: Identifier; typeAnnotation: TSType; } declare interface TSTypeAnnotation extends BaseNode { type: 'TSTypeAnnotation'; typeAnnotation: TSType; } declare type TSTypeElement = TSPropertySignature | TSMethodSignature | TSIndexSignature; declare interface TSTypeLiteral extends BaseNode { type: 'TSTypeLiteral'; members: TSTypeElement[]; } declare interface TSTypeParameterInstantiation extends BaseNode { type: 'TSTypeParameterInstantiation'; params: TSType[]; } declare interface TSTypeReference extends BaseNode { type: 'TSTypeReference'; typeName: Identifier | TSQualifiedName; typeParameters?: TSTypeParameterInstantiation; typeArguments?: TSTypeParameterInstantiation; } declare interface TSUndefinedKeyword extends BaseNode { type: 'TSUndefinedKeyword'; } declare interface TSUnionType extends BaseNode { type: 'TSUnionType'; types: TSType[]; } declare interface TSVoidKeyword extends BaseNode { type: 'TSVoidKeyword'; } declare interface UnaryExpression extends BaseNode { type: 'UnaryExpression'; operator: string; argument: Expression; } declare interface VariableDeclaration extends BaseNode { type: 'VariableDeclaration'; kind: 'const' | 'let' | 'var'; declarations: VariableDeclarator[]; } declare interface VariableDeclarator extends BaseNode { type: 'VariableDeclarator'; id: Identifier | Pattern; init: Expression | null; } /** * Verify that `dist/manifest.json` contains every `@smrt()` object declared in * the package source. See module docs for the rationale and guarantees. */ export declare function verifyManifestCompleteness(options: VerifyManifestCompletenessOptions): Promise; export declare interface VerifyManifestCompletenessOptions { /** Absolute or relative path to the package directory to verify. */ packageDir: string; /** Source include globs (default mirrors the build). */ include?: string[]; /** Source exclude globs (default mirrors the build). */ exclude?: string[]; /** Package directory basenames to skip (default: framework infrastructure). */ skipPackages?: string[]; } export declare interface VerifyManifestCompletenessResult { status: VerifyManifestStatus; packageName?: string; manifestPath?: string; /** Qualified object keys present in source but missing from the manifest. */ missing: string[]; /** Number of objects the source is expected to contribute. */ expectedCount: number; /** Number of objects present in the published manifest. */ distCount: number; /** Human-readable explanation for `skipped` / `missing-manifest`. */ reason?: string; } /** * Manifest completeness verification. * * Re-scans a package's `src/` with the same OXC scanner the build uses and * asserts that every `@smrt()`-decorated object (and its collection) is present * in the package's published `dist/manifest.json`. This is a publish-time guard: * downstream schema migration is manifest-driven, so a stale manifest that omits * an object means consumers can never create its table via `smrt db:migrate`. * * Root cause it guards against (issue #1483): `@happyvertical/smrt-jobs@0.27.41` * shipped a `dist/manifest.json` generated before `SmrtWorker` existed. The * source, exports, and `__smrt-register__` path were all correct, but the * published manifest had only 4 of 6 objects, so `_smrt_workers` was never * created and `TaskRunner.start()` threw on every consumer that upgraded. * * The check compares object-key SETS only. The downstream manifest-enrichment * passes (schema/validation/agent generation) mutate object entries but never * add or remove object keys, so the scanner + adapter object set is the source * of truth for "which objects the published manifest must contain". The * comparison is `expected ⊆ dist`: enrichment passes that add keys to `dist` * (e.g. STI children) never trigger a false failure. * * Parse errors are handled before the set comparison: a syntactically broken * source file drops its objects from `expected` AND `dist` symmetrically, which * a naive `expected ⊆ dist` would wave through as `ok`. The scan's * `severity:'error'` entries therefore short-circuit to a distinct `scan-error` * status so a broken source can never masquerade as a complete manifest. * * @see https://github.com/happyvertical/smrt/issues/1483 */ export declare type VerifyManifestStatus = 'ok' | 'incomplete' | 'missing-manifest' | 'scan-error' | 'skipped'; export { }