@happyvertical/smrt-scanner 0.30.0

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.
package/dist/index.js ADDED
@@ -0,0 +1,163 @@
1
+ import { O as OxcScanner, M as ManifestAdapter } from "./chunks/scanner-3K_xuVXN.js";
2
+ import { I, e, p, a } from "./chunks/scanner-3K_xuVXN.js";
3
+ import { existsSync, readFileSync } from "node:fs";
4
+ import { resolve, basename } from "node:path";
5
+ const DEFAULT_INCLUDE = ["src/**/*.ts"];
6
+ const DEFAULT_EXCLUDE = [
7
+ "**/*.test.ts",
8
+ "**/*.spec.ts",
9
+ "**/__tests__/**",
10
+ "**/*.svelte",
11
+ "**/node_modules/**",
12
+ "**/dist/**",
13
+ "**/*.d.ts"
14
+ ];
15
+ const DEFAULT_SKIP_PACKAGES = [
16
+ "core",
17
+ "types",
18
+ "config",
19
+ "scanner",
20
+ "vitest",
21
+ "smrt-playground"
22
+ ];
23
+ function resolveManifestPath(packageDir, pkgJson) {
24
+ const exportsMap = pkgJson.exports ?? {};
25
+ const entry = exportsMap["./manifest"] ?? exportsMap["./manifest.json"];
26
+ let relativeTarget;
27
+ if (typeof entry === "string") {
28
+ relativeTarget = entry;
29
+ } else if (entry && typeof entry === "object") {
30
+ const conditional = entry;
31
+ const candidate = conditional.default ?? conditional.import ?? conditional.types;
32
+ if (typeof candidate === "string") {
33
+ relativeTarget = candidate;
34
+ }
35
+ }
36
+ if (!relativeTarget) {
37
+ return null;
38
+ }
39
+ return resolve(packageDir, relativeTarget);
40
+ }
41
+ async function verifyManifestCompleteness(options) {
42
+ const packageDir = resolve(options.packageDir);
43
+ const include = options.include ?? DEFAULT_INCLUDE;
44
+ const exclude = options.exclude ?? DEFAULT_EXCLUDE;
45
+ const skipPackages = options.skipPackages ?? DEFAULT_SKIP_PACKAGES;
46
+ const packageJsonPath = resolve(packageDir, "package.json");
47
+ if (!existsSync(packageJsonPath)) {
48
+ return {
49
+ status: "skipped",
50
+ missing: [],
51
+ expectedCount: 0,
52
+ distCount: 0,
53
+ reason: `no package.json at ${packageDir}`
54
+ };
55
+ }
56
+ const pkgJson = JSON.parse(
57
+ readFileSync(packageJsonPath, "utf-8")
58
+ );
59
+ const packageName = pkgJson.name;
60
+ if (skipPackages.includes(basename(packageDir))) {
61
+ return {
62
+ status: "skipped",
63
+ packageName,
64
+ missing: [],
65
+ expectedCount: 0,
66
+ distCount: 0,
67
+ reason: "framework infrastructure package (does not use scanner manifest)"
68
+ };
69
+ }
70
+ const manifestPath = resolveManifestPath(packageDir, pkgJson);
71
+ if (!manifestPath) {
72
+ return {
73
+ status: "skipped",
74
+ packageName,
75
+ missing: [],
76
+ expectedCount: 0,
77
+ distCount: 0,
78
+ reason: "package does not publish a ./manifest export"
79
+ };
80
+ }
81
+ const scanner = new OxcScanner({ cwd: packageDir, include, exclude });
82
+ const { results, resolved } = await scanner.scanAndResolve();
83
+ const scanErrors = results.errors.filter((e2) => e2.severity === "error");
84
+ if (scanErrors.length > 0) {
85
+ const detail = scanErrors.map((e2) => {
86
+ const where = e2.line ? `${e2.filePath}:${e2.line}` : e2.filePath;
87
+ return `${where} — ${e2.message}`;
88
+ }).join("; ");
89
+ return {
90
+ status: "scan-error",
91
+ packageName,
92
+ manifestPath,
93
+ missing: [],
94
+ expectedCount: 0,
95
+ distCount: 0,
96
+ reason: `source scan reported ${scanErrors.length} parse error(s): ${detail}`
97
+ };
98
+ }
99
+ const adapter = new ManifestAdapter();
100
+ const expected = adapter.toManifest(resolved, {
101
+ packageName,
102
+ packageVersion: pkgJson.version,
103
+ typeAliases: results.typeAliases
104
+ });
105
+ const expectedKeys = Object.keys(expected.objects);
106
+ if (expectedKeys.length === 0) {
107
+ return {
108
+ status: "skipped",
109
+ packageName,
110
+ manifestPath,
111
+ missing: [],
112
+ expectedCount: 0,
113
+ distCount: 0,
114
+ reason: "no @smrt() objects found in source"
115
+ };
116
+ }
117
+ if (!existsSync(manifestPath)) {
118
+ return {
119
+ status: "missing-manifest",
120
+ packageName,
121
+ manifestPath,
122
+ missing: expectedKeys,
123
+ expectedCount: expectedKeys.length,
124
+ distCount: 0,
125
+ reason: `published manifest not found at ${manifestPath}`
126
+ };
127
+ }
128
+ let distObjects = {};
129
+ try {
130
+ const distManifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
131
+ distObjects = distManifest && typeof distManifest.objects === "object" ? distManifest.objects : {};
132
+ } catch (error) {
133
+ return {
134
+ status: "missing-manifest",
135
+ packageName,
136
+ manifestPath,
137
+ missing: expectedKeys,
138
+ expectedCount: expectedKeys.length,
139
+ distCount: 0,
140
+ reason: `published manifest is not valid JSON: ${error.message}`
141
+ };
142
+ }
143
+ const distKeys = new Set(Object.keys(distObjects));
144
+ const missing = expectedKeys.filter((key) => !distKeys.has(key));
145
+ return {
146
+ status: missing.length > 0 ? "incomplete" : "ok",
147
+ packageName,
148
+ manifestPath,
149
+ missing,
150
+ expectedCount: expectedKeys.length,
151
+ distCount: distKeys.size
152
+ };
153
+ }
154
+ export {
155
+ I as InheritanceResolver,
156
+ ManifestAdapter,
157
+ OxcScanner,
158
+ e as extractSmrtImports,
159
+ p as parseFile,
160
+ a as parseSource,
161
+ verifyManifestCompleteness
162
+ };
163
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../src/verify-completeness.ts"],"sourcesContent":["/**\n * Manifest completeness verification.\n *\n * Re-scans a package's `src/` with the same OXC scanner the build uses and\n * asserts that every `@smrt()`-decorated object (and its collection) is present\n * in the package's published `dist/manifest.json`. This is a publish-time guard:\n * downstream schema migration is manifest-driven, so a stale manifest that omits\n * an object means consumers can never create its table via `smrt db:migrate`.\n *\n * Root cause it guards against (issue #1483): `@happyvertical/smrt-jobs@0.27.41`\n * shipped a `dist/manifest.json` generated before `SmrtWorker` existed. The\n * source, exports, and `__smrt-register__` path were all correct, but the\n * published manifest had only 4 of 6 objects, so `_smrt_workers` was never\n * created and `TaskRunner.start()` threw on every consumer that upgraded.\n *\n * The check compares object-key SETS only. The downstream manifest-enrichment\n * passes (schema/validation/agent generation) mutate object entries but never\n * add or remove object keys, so the scanner + adapter object set is the source\n * of truth for \"which objects the published manifest must contain\". The\n * comparison is `expected ⊆ dist`: enrichment passes that add keys to `dist`\n * (e.g. STI children) never trigger a false failure.\n *\n * Parse errors are handled before the set comparison: a syntactically broken\n * source file drops its objects from `expected` AND `dist` symmetrically, which\n * a naive `expected ⊆ dist` would wave through as `ok`. The scan's\n * `severity:'error'` entries therefore short-circuit to a distinct `scan-error`\n * status so a broken source can never masquerade as a complete manifest.\n *\n * @see https://github.com/happyvertical/smrt/issues/1483\n */\n\nimport { existsSync, readFileSync } from 'node:fs';\nimport { basename, resolve } from 'node:path';\nimport { ManifestAdapter } from './manifest-adapter.js';\nimport { OxcScanner } from './scanner.js';\n\n/**\n * Source globs for the completeness scan. The build (`vite.config.base.ts`)\n * passes `include: ['src/**\\/*.ts']` and excludes `*.test.ts` / `*.spec.ts` (the\n * plugin also drops `*.svelte`); we reproduce that so the expected object set\n * matches what the build would publish. We additionally exclude\n * `**\\/__tests__\\/**` so any `@smrt()` fixtures in non-test helper modules under\n * `src/__tests__/` (e.g. `src/__tests__/helpers/*.ts`) are never required in the\n * published manifest. These extra exclusions can only shrink `expected` relative\n * to the build, so they can never cause a false failure — the check is\n * `expected ⊆ dist`.\n */\nconst DEFAULT_INCLUDE = ['src/**/*.ts'];\nconst DEFAULT_EXCLUDE = [\n '**/*.test.ts',\n '**/*.spec.ts',\n '**/__tests__/**',\n '**/*.svelte',\n '**/node_modules/**',\n '**/dist/**',\n '**/*.d.ts',\n];\n\n/**\n * Framework-infrastructure packages whose published manifest is NOT produced by\n * the OXC scanner path (e.g. `@happyvertical/smrt-core` ships a curated static\n * manifest). Mirrors `skipSmrtPlugin` in `vite.config.base.ts`. Keyed by package\n * directory basename.\n */\nconst DEFAULT_SKIP_PACKAGES = [\n 'core',\n 'types',\n 'config',\n 'scanner',\n 'vitest',\n 'smrt-playground',\n];\n\nexport type VerifyManifestStatus =\n | 'ok'\n | 'incomplete'\n | 'missing-manifest'\n | 'scan-error'\n | 'skipped';\n\nexport interface VerifyManifestCompletenessOptions {\n /** Absolute or relative path to the package directory to verify. */\n packageDir: string;\n /** Source include globs (default mirrors the build). */\n include?: string[];\n /** Source exclude globs (default mirrors the build). */\n exclude?: string[];\n /** Package directory basenames to skip (default: framework infrastructure). */\n skipPackages?: string[];\n}\n\nexport interface VerifyManifestCompletenessResult {\n status: VerifyManifestStatus;\n packageName?: string;\n manifestPath?: string;\n /** Qualified object keys present in source but missing from the manifest. */\n missing: string[];\n /** Number of objects the source is expected to contribute. */\n expectedCount: number;\n /** Number of objects present in the published manifest. */\n distCount: number;\n /** Human-readable explanation for `skipped` / `missing-manifest`. */\n reason?: string;\n}\n\ninterface MinimalPackageJson {\n name?: string;\n version?: string;\n exports?: Record<string, unknown>;\n}\n\n/**\n * Resolve the `./manifest` (or `./manifest.json`) export target to an absolute\n * path. Returns `null` when the package does not publish a manifest.\n */\nfunction resolveManifestPath(\n packageDir: string,\n pkgJson: MinimalPackageJson,\n): string | null {\n const exportsMap = pkgJson.exports ?? {};\n const entry = exportsMap['./manifest'] ?? exportsMap['./manifest.json'];\n\n let relativeTarget: string | undefined;\n if (typeof entry === 'string') {\n relativeTarget = entry;\n } else if (entry && typeof entry === 'object') {\n const conditional = entry as Record<string, unknown>;\n const candidate =\n conditional.default ?? conditional.import ?? conditional.types;\n if (typeof candidate === 'string') {\n relativeTarget = candidate;\n }\n }\n\n if (!relativeTarget) {\n return null;\n }\n\n return resolve(packageDir, relativeTarget);\n}\n\n/**\n * Verify that `dist/manifest.json` contains every `@smrt()` object declared in\n * the package source. See module docs for the rationale and guarantees.\n */\nexport async function verifyManifestCompleteness(\n options: VerifyManifestCompletenessOptions,\n): Promise<VerifyManifestCompletenessResult> {\n const packageDir = resolve(options.packageDir);\n const include = options.include ?? DEFAULT_INCLUDE;\n const exclude = options.exclude ?? DEFAULT_EXCLUDE;\n const skipPackages = options.skipPackages ?? DEFAULT_SKIP_PACKAGES;\n\n const packageJsonPath = resolve(packageDir, 'package.json');\n if (!existsSync(packageJsonPath)) {\n return {\n status: 'skipped',\n missing: [],\n expectedCount: 0,\n distCount: 0,\n reason: `no package.json at ${packageDir}`,\n };\n }\n\n const pkgJson: MinimalPackageJson = JSON.parse(\n readFileSync(packageJsonPath, 'utf-8'),\n );\n const packageName = pkgJson.name;\n\n if (skipPackages.includes(basename(packageDir))) {\n return {\n status: 'skipped',\n packageName,\n missing: [],\n expectedCount: 0,\n distCount: 0,\n reason:\n 'framework infrastructure package (does not use scanner manifest)',\n };\n }\n\n const manifestPath = resolveManifestPath(packageDir, pkgJson);\n if (!manifestPath) {\n return {\n status: 'skipped',\n packageName,\n missing: [],\n expectedCount: 0,\n distCount: 0,\n reason: 'package does not publish a ./manifest export',\n };\n }\n\n // Derive the object set the source SHOULD produce, using the same scanner and\n // adapter the build uses (vite-plugin scanWithOxc).\n const scanner = new OxcScanner({ cwd: packageDir, include, exclude });\n const { results, resolved } = await scanner.scanAndResolve();\n\n // A source file with a syntax error parses to `errors:[...], body:[]`, so any\n // @smrt() class in it is dropped from BOTH the re-scanned `expected` set and\n // (having never built) the published `dist`. Because the completeness check is\n // `expected ⊆ dist`, the object vanishes symmetrically and the guard would\n // return `ok` — silently defeating the #1483 guarantee for the exact case\n // (broken source) that most needs catching. Surface scan errors as a distinct\n // non-`ok` status instead of computing `expected` from a partial scan.\n const scanErrors = results.errors.filter((e) => e.severity === 'error');\n if (scanErrors.length > 0) {\n const detail = scanErrors\n .map((e) => {\n const where = e.line ? `${e.filePath}:${e.line}` : e.filePath;\n return `${where} — ${e.message}`;\n })\n .join('; ');\n return {\n status: 'scan-error',\n packageName,\n manifestPath,\n missing: [],\n expectedCount: 0,\n distCount: 0,\n reason: `source scan reported ${scanErrors.length} parse error(s): ${detail}`,\n };\n }\n\n const adapter = new ManifestAdapter();\n const expected = adapter.toManifest(resolved, {\n packageName,\n packageVersion: pkgJson.version,\n typeAliases: results.typeAliases,\n });\n const expectedKeys = Object.keys(expected.objects);\n\n // Nothing to verify if the package declares no SMRT objects (e.g. a package\n // that publishes ./manifest but only ships base classes).\n if (expectedKeys.length === 0) {\n return {\n status: 'skipped',\n packageName,\n manifestPath,\n missing: [],\n expectedCount: 0,\n distCount: 0,\n reason: 'no @smrt() objects found in source',\n };\n }\n\n if (!existsSync(manifestPath)) {\n return {\n status: 'missing-manifest',\n packageName,\n manifestPath,\n missing: expectedKeys,\n expectedCount: expectedKeys.length,\n distCount: 0,\n reason: `published manifest not found at ${manifestPath}`,\n };\n }\n\n let distObjects: Record<string, unknown> = {};\n try {\n const distManifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));\n distObjects =\n distManifest && typeof distManifest.objects === 'object'\n ? distManifest.objects\n : {};\n } catch (error) {\n return {\n status: 'missing-manifest',\n packageName,\n manifestPath,\n missing: expectedKeys,\n expectedCount: expectedKeys.length,\n distCount: 0,\n reason: `published manifest is not valid JSON: ${(error as Error).message}`,\n };\n }\n\n const distKeys = new Set(Object.keys(distObjects));\n const missing = expectedKeys.filter((key) => !distKeys.has(key));\n\n return {\n status: missing.length > 0 ? 'incomplete' : 'ok',\n packageName,\n manifestPath,\n missing,\n expectedCount: expectedKeys.length,\n distCount: distKeys.size,\n };\n}\n"],"names":["e"],"mappings":"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"}
@@ -0,0 +1,323 @@
1
+ /**
2
+ * Describes a pre-loaded manifest from an installed SMRT package, used by
3
+ * {@link InheritanceResolver} to resolve base classes that originate outside
4
+ * the local project source.
5
+ *
6
+ * Build tooling (e.g. the SMRT CLI / vitest plugin) loads each installed
7
+ * `@happyvertical/smrt-*` package's `manifest.json` and converts it into an
8
+ * `ExternalManifest` before passing it to the scanner.
9
+ *
10
+ * @see {@link OxcScannerOptions.externalManifests}
11
+ * @see {@link InheritanceResolver.addExternalManifest}
12
+ */
13
+ export declare interface ExternalManifest {
14
+ /** npm package name, e.g. `'@happyvertical/smrt-profiles'`. */
15
+ packageName: string;
16
+ /** SemVer version string of the installed package. */
17
+ packageVersion: string;
18
+ /** All class definitions exported by the package, keyed by class name. */
19
+ classes: Map<string, RawClassDefinition>;
20
+ }
21
+
22
+ /**
23
+ * Result produced by {@link ManifestAdapter.inferFieldType} for a single field.
24
+ *
25
+ * In addition to the inferred `type`, carries the `source` of the inference
26
+ * so callers can distinguish authoritative decorator-driven results from
27
+ * heuristic guesses and provide better diagnostics.
28
+ *
29
+ * @see {@link InferredFieldType} for valid `type` values.
30
+ * @see {@link ManifestAdapter.inferFieldType} for inference priority rules.
31
+ */
32
+ export declare interface FieldTypeInference {
33
+ /** Inferred SMRT type */
34
+ type: InferredFieldType;
35
+ /** Related class for relationship types */
36
+ related?: string;
37
+ /** Default value if extractable */
38
+ defaultValue?: unknown;
39
+ /** Whether field is required */
40
+ required: boolean;
41
+ /** Inference source for debugging */
42
+ source: 'helper' | 'decorator' | 'annotation' | 'heuristic' | 'default';
43
+ /** Underlying type for meta fields (e.g., 'string' inside Meta<string>) */
44
+ underlyingType?: InferredFieldType;
45
+ /**
46
+ * Decorator-derived metadata that should be merged into the manifest
47
+ * field's `_meta` object. Used by `@crossPackageRef`, `@manyToMany`,
48
+ * `@meta` to carry options (`validate`, `through`, `indexed`, `idType`,
49
+ * etc.) that don't fit on the top-level FieldDefinition.
50
+ */
51
+ _meta?: Record<string, unknown>;
52
+ }
53
+
54
+ /**
55
+ * Result from scanning a single file
56
+ */
57
+ export declare interface FileScanResult {
58
+ /** Source file path */
59
+ filePath: string;
60
+ /** Classes found in file */
61
+ classes: RawClassDefinition[];
62
+ /** Scan errors */
63
+ errors: ScanError[];
64
+ /** Parse time in milliseconds */
65
+ parseTimeMs: number;
66
+ /** Type alias declarations found in file (name → resolved type string) */
67
+ typeAliases: Record<string, string>;
68
+ /** SMRT package imports found in file (package name → Set of imported class names) */
69
+ smrtImports?: Map<string, Set<string>>;
70
+ }
71
+
72
+ /**
73
+ * The set of SMRT column types that the scanner can infer for a field.
74
+ *
75
+ * | Value | DB column type | Notes |
76
+ * |---|---|---|
77
+ * | `text` | `TEXT` / `VARCHAR` | Default for `string` and unknown types |
78
+ * | `integer` | `INTEGER` | `number` with `= 0` initialiser |
79
+ * | `decimal` | `DECIMAL` | `number` with `= 0.0` initialiser |
80
+ * | `boolean` | `BOOLEAN` | `boolean` annotation or literal initialiser |
81
+ * | `datetime` | `DATETIME` | `Date` annotation |
82
+ * | `json` | `JSON` / `TEXT` | Arrays, `Record<>`, object types |
83
+ * | `foreignKey` | `UUID` by default (FK column) | `@foreignKey(Class)` decorator |
84
+ * | `crossPackageRef` | `UUID` by default (no FK constraint) | `@crossPackageRef('@pkg:Class')` decorator |
85
+ * | `oneToMany` | — (virtual) | `@oneToMany(Class)` decorator |
86
+ * | `manyToMany` | — (virtual) | `@manyToMany(Class)` decorator |
87
+ * | `meta` | Stored in `_meta_data` | STI child field wrapped in `Meta<T>` |
88
+ * | `unknown` | — | Could not be determined |
89
+ *
90
+ * @see {@link FieldTypeInference} for the full inference result shape.
91
+ */
92
+ export declare type InferredFieldType = 'text' | 'integer' | 'decimal' | 'boolean' | 'datetime' | 'json' | 'foreignKey' | 'crossPackageRef' | 'oneToMany' | 'manyToMany' | 'meta' | 'unknown';
93
+
94
+ /**
95
+ * Configuration options for {@link OxcScanner}.
96
+ *
97
+ * All fields are optional; reasonable defaults are applied when omitted.
98
+ *
99
+ * @see {@link OxcScanner}
100
+ */
101
+ export declare interface OxcScannerOptions {
102
+ /** Glob patterns to include */
103
+ include?: string[];
104
+ /** Glob patterns to exclude */
105
+ exclude?: string[];
106
+ /** Base directory for scanning */
107
+ cwd?: string;
108
+ /** Path to tsconfig.json for module resolution */
109
+ tsconfig?: string;
110
+ /** Whether to follow imports to find base classes */
111
+ followImports?: boolean;
112
+ /** Known base classes (avoid resolution) */
113
+ baseClasses?: string[];
114
+ /** Include private methods in output */
115
+ includePrivateMethods?: boolean;
116
+ /** Include static methods in output */
117
+ includeStaticMethods?: boolean;
118
+ /** External package manifests for base class resolution */
119
+ externalManifests?: Map<string, ExternalManifest>;
120
+ }
121
+
122
+ /**
123
+ * Type definitions for OXC-based SMRT scanner
124
+ *
125
+ * This module defines:
126
+ * 1. Raw types - Intermediate representation from OXC parsing (Phase 1)
127
+ * 2. Resolved types - After inheritance resolution (Phase 2)
128
+ * 3. Re-exports of smrt-core types for compatibility
129
+ */
130
+ /**
131
+ * Raw class definition extracted from OXC AST
132
+ * Contains only syntactic information, no semantic resolution
133
+ */
134
+ export declare interface RawClassDefinition {
135
+ /** Class name as declared in source */
136
+ className: string;
137
+ /** Absolute path to source file */
138
+ filePath: string;
139
+ /** Parent class name from extends clause (null if none) */
140
+ extendsClause: string | null;
141
+ /** Generic type argument from extends (e.g., "Meeting" from SmrtCollection<Meeting>) */
142
+ extendsTypeArg: string | null;
143
+ /** Parsed @smrt() decorator configuration object */
144
+ decoratorConfig: RawDecoratorConfig | null;
145
+ /** Has @smrt() decorator */
146
+ hasSmartDecorator: boolean;
147
+ /** Class properties/fields */
148
+ fields: RawFieldDefinition[];
149
+ /** Class methods */
150
+ methods: RawMethodDefinition[];
151
+ /** Start line in source file */
152
+ startLine: number;
153
+ /** End line in source file */
154
+ endLine: number;
155
+ }
156
+
157
+ /**
158
+ * Raw decorator information
159
+ */
160
+ export declare interface RawDecorator {
161
+ /** Decorator name (e.g., "field", "foreignKey") */
162
+ name: string;
163
+ /** Raw arguments as strings */
164
+ arguments: string[];
165
+ }
166
+
167
+ /**
168
+ * Raw @smrt() decorator configuration
169
+ */
170
+ export declare interface RawDecoratorConfig {
171
+ /** Table strategy: 'sti' | 'cti' */
172
+ tableStrategy?: 'sti' | 'cti';
173
+ /** Storage type for the generated id primary key */
174
+ idType?: 'uuid' | 'text';
175
+ /** Code-owned feature toggle declarations */
176
+ features?: Record<string, {
177
+ defaultEnabled: boolean;
178
+ label?: string;
179
+ description?: string;
180
+ metadata?: Record<string, unknown>;
181
+ }>;
182
+ /** API configuration */
183
+ api?: {
184
+ include?: string[];
185
+ exclude?: string[];
186
+ };
187
+ /** CLI configuration */
188
+ cli?: boolean | {
189
+ include?: string[];
190
+ exclude?: string[];
191
+ skipApiCheck?: boolean;
192
+ http?: boolean;
193
+ };
194
+ /** MCP configuration */
195
+ mcp?: {
196
+ include?: string[];
197
+ exclude?: string[];
198
+ };
199
+ /** Raw config object for unknown properties */
200
+ [key: string]: unknown;
201
+ }
202
+
203
+ /**
204
+ * Raw field definition from OXC AST
205
+ */
206
+ export declare interface RawFieldDefinition {
207
+ /** Field name */
208
+ name: string;
209
+ /** TypeScript type annotation as string (e.g., "string", "number", "Date") */
210
+ typeAnnotation: string | null;
211
+ /** Raw initializer expression as string */
212
+ initializer: string | null;
213
+ /** For numeric literals: whether it contains a decimal point */
214
+ hasDecimalPoint: boolean;
215
+ /** For numeric literals: the actual numeric value */
216
+ numericValue: number | null;
217
+ /** Decorators applied to this field */
218
+ decorators: RawDecorator[];
219
+ /** Whether field is optional (has ?) */
220
+ optional: boolean;
221
+ /** Whether field is static */
222
+ isStatic: boolean;
223
+ /** Whether field is readonly */
224
+ readonly: boolean;
225
+ /** Whether field is private/protected */
226
+ accessibility: 'public' | 'private' | 'protected';
227
+ /** Start line in source */
228
+ line: number;
229
+ }
230
+
231
+ /**
232
+ * Raw method definition from OXC AST
233
+ */
234
+ export declare interface RawMethodDefinition {
235
+ /** Method name */
236
+ name: string;
237
+ /** Whether method is async */
238
+ async: boolean;
239
+ /** Whether method is static */
240
+ isStatic: boolean;
241
+ /** Accessibility modifier */
242
+ accessibility: 'public' | 'private' | 'protected';
243
+ /** Method parameters */
244
+ parameters: RawParameterDefinition[];
245
+ /** Return type annotation as string */
246
+ returnType: string | null;
247
+ /** JSDoc description if present */
248
+ description: string | null;
249
+ /** Start line in source */
250
+ line: number;
251
+ }
252
+
253
+ /**
254
+ * Raw parameter definition
255
+ */
256
+ export declare interface RawParameterDefinition {
257
+ /** Parameter name */
258
+ name: string;
259
+ /** Type annotation as string */
260
+ type: string | null;
261
+ /** Whether parameter is optional */
262
+ optional: boolean;
263
+ /** Default value as string */
264
+ defaultValue: string | null;
265
+ }
266
+
267
+ /**
268
+ * Resolved class definition with inheritance chain
269
+ */
270
+ export declare interface ResolvedClassDefinition extends RawClassDefinition {
271
+ /** Full inheritance chain from base to this class */
272
+ inheritanceChain: string[];
273
+ /** STI base class name (if part of STI hierarchy) */
274
+ stiBase: string | null;
275
+ /** Effective table strategy (inherited or declared) */
276
+ effectiveTableStrategy: 'sti' | 'cti';
277
+ /** Whether this class uses STI (convenience boolean) */
278
+ isSTI: boolean;
279
+ /** Whether this class is a framework base class */
280
+ isFrameworkBase: boolean;
281
+ /** All fields including inherited (for STI) */
282
+ allFields: RawFieldDefinition[];
283
+ /** Package this class belongs to (if external) */
284
+ packageName: string | null;
285
+ }
286
+
287
+ /**
288
+ * Scan error
289
+ */
290
+ export declare interface ScanError {
291
+ /** Error message */
292
+ message: string;
293
+ /** Source file */
294
+ filePath: string;
295
+ /** Line number (1-based) */
296
+ line?: number;
297
+ /** Column number (1-based) */
298
+ column?: number;
299
+ /** Error severity */
300
+ severity: 'error' | 'warning';
301
+ }
302
+
303
+ /**
304
+ * Result from scanning multiple files
305
+ */
306
+ export declare interface ScanResults {
307
+ /** All scanned files */
308
+ files: FileScanResult[];
309
+ /** All classes found (flattened) */
310
+ classes: RawClassDefinition[];
311
+ /** All errors (flattened) */
312
+ errors: ScanError[];
313
+ /** Total parse time in milliseconds */
314
+ totalParseTimeMs: number;
315
+ /** Number of files scanned */
316
+ fileCount: number;
317
+ /** Accumulated type aliases across all files */
318
+ typeAliases: Record<string, string>;
319
+ /** Accumulated SMRT package imports across all files (package name → Set of imported class names) */
320
+ smrtImports?: Map<string, Set<string>>;
321
+ }
322
+
323
+ export { }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@happyvertical/smrt-scanner",
3
+ "version": "0.30.0",
4
+ "description": "High-performance TypeScript scanner using OXC for SMRT manifest generation",
5
+ "author": "HappyVertical",
6
+ "type": "module",
7
+ "main": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "bin": {
10
+ "smrt-scan": "./bin/smrt-scan.js"
11
+ },
12
+ "files": [
13
+ "CLAUDE.md",
14
+ "bin",
15
+ "dist",
16
+ "AGENTS.md"
17
+ ],
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "import": "./dist/index.js"
22
+ },
23
+ "./cli": {
24
+ "types": "./dist/cli.d.ts",
25
+ "import": "./dist/cli.js"
26
+ },
27
+ "./types": {
28
+ "types": "./dist/types.d.ts",
29
+ "import": "./dist/types.js"
30
+ }
31
+ },
32
+ "dependencies": {
33
+ "fast-glob": "3.3.3",
34
+ "minimatch": "10.1.1",
35
+ "oxc-parser": "^0.108.0",
36
+ "oxc-resolver": "^11.16.3"
37
+ },
38
+ "devDependencies": {
39
+ "@types/node": "25.0.9",
40
+ "tsx": "^4.21.0",
41
+ "vite": "7.3.1",
42
+ "vitest": "^4.0.17"
43
+ },
44
+ "publishConfig": {
45
+ "registry": "https://registry.npmjs.org",
46
+ "access": "public"
47
+ },
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "https://github.com/happyvertical/smrt.git",
51
+ "directory": "packages/scanner"
52
+ },
53
+ "scripts": {
54
+ "build": "vite build",
55
+ "build:watch": "vite build --watch",
56
+ "test": "vitest run",
57
+ "test:watch": "vitest",
58
+ "typecheck": "tsc --noEmit -p tsconfig.json",
59
+ "clean": "rm -rf dist"
60
+ }
61
+ }