@happyvertical/smrt-scanner 0.37.1 → 0.37.3

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.
@@ -0,0 +1,1819 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { parseSync } from "oxc-parser";
3
+ import "node:path";
4
+ import fg from "fast-glob";
5
+ //#region src/inheritance-resolver.ts
6
+ var FRAMEWORK_BASE_CLASSES = /* @__PURE__ */ new Set([
7
+ "SmrtObject",
8
+ "SmrtClass",
9
+ "SmrtCollection",
10
+ "SmrtJunction",
11
+ "SmrtHierarchical",
12
+ "SmrtPolymorphicAssociation",
13
+ "SmrtReport",
14
+ "SmrtReportCollection"
15
+ ]);
16
+ var InheritanceResolver = class {
17
+ /** Map of className -> RawClassDefinition */
18
+ classMap = /* @__PURE__ */ new Map();
19
+ /** External package manifests for cross-package resolution */
20
+ externalManifests = /* @__PURE__ */ new Map();
21
+ /** Known base classes (user-provided) */
22
+ knownBaseClasses;
23
+ /** Cache of resolved inheritance chains */
24
+ chainCache = /* @__PURE__ */ new Map();
25
+ /**
26
+ * Create a new `InheritanceResolver`.
27
+ *
28
+ * @param options.baseClasses - Additional class names to treat as known
29
+ * framework base classes (beyond the built-in `SmrtObject`, `SmrtClass`,
30
+ * and `SmrtCollection`).
31
+ * @param options.externalManifests - Pre-loaded external package manifests
32
+ * keyed by package name, used for cross-package parent class resolution.
33
+ */
34
+ constructor(options = {}) {
35
+ this.knownBaseClasses = /* @__PURE__ */ new Set([...FRAMEWORK_BASE_CLASSES, ...options.baseClasses || []]);
36
+ this.externalManifests = options.externalManifests || /* @__PURE__ */ new Map();
37
+ }
38
+ /**
39
+ * Register raw class definitions from a scan pass.
40
+ *
41
+ * Adds each class to the internal class map by `className`. Calling this
42
+ * clears the inheritance chain cache so subsequent calls to
43
+ * {@link resolveAll} or {@link resolveInheritanceChain} reflect the new
44
+ * classes.
45
+ *
46
+ * @param classes - Array of {@link RawClassDefinition} objects from
47
+ * {@link ScanResults.classes}.
48
+ */
49
+ addClasses(classes) {
50
+ for (const classDef of classes) this.classMap.set(classDef.className, classDef);
51
+ this.chainCache.clear();
52
+ }
53
+ /**
54
+ * Register an external package manifest for cross-package base class resolution.
55
+ *
56
+ * Clears the chain cache after registration so re-resolution picks up the
57
+ * new definitions.
58
+ *
59
+ * @param manifest - External package manifest providing class definitions
60
+ * that may appear as base classes in the local project.
61
+ *
62
+ * @see {@link ExternalManifest}
63
+ */
64
+ addExternalManifest(manifest) {
65
+ this.externalManifests.set(manifest.packageName, manifest);
66
+ this.chainCache.clear();
67
+ }
68
+ /**
69
+ * Resolve all registered classes and return fully-resolved definitions.
70
+ *
71
+ * A class is included in the output if it either:
72
+ * 1. Has an `@smrt()` decorator, or
73
+ * 2. Directly or transitively extends a framework base class
74
+ * (`SmrtObject`, `SmrtClass`, `SmrtCollection`) — this captures
75
+ * collection classes such as `class MeetingCollection extends
76
+ * SmrtCollection<Meeting>` that do not carry `@smrt()` themselves.
77
+ *
78
+ * @returns An array of {@link ResolvedClassDefinition} — one entry per
79
+ * eligible class, with inheritance chain, STI metadata, and merged fields
80
+ * populated.
81
+ *
82
+ * @see {@link resolve} to resolve a single class definition.
83
+ */
84
+ resolveAll() {
85
+ const resolved = [];
86
+ for (const classDef of this.classMap.values()) {
87
+ const extendsFrameworkBase = this.extendsFrameworkBase(classDef);
88
+ if (!classDef.hasSmartDecorator && !extendsFrameworkBase) continue;
89
+ const resolvedClass = this.resolve(classDef);
90
+ resolved.push(resolvedClass);
91
+ }
92
+ return resolved;
93
+ }
94
+ /**
95
+ * Check if a class extends a framework base class
96
+ * (SmrtObject, SmrtClass, or SmrtCollection)
97
+ */
98
+ extendsFrameworkBase(classDef) {
99
+ if (classDef.extendsClause && this.knownBaseClasses.has(classDef.extendsClause)) return true;
100
+ return this.resolveInheritanceChain(classDef.className).some((className) => this.knownBaseClasses.has(className));
101
+ }
102
+ /**
103
+ * Resolve a single raw class definition into a fully-resolved definition.
104
+ *
105
+ * Computes the inheritance chain, determines the effective table strategy,
106
+ * detects STI membership, and merges ancestor fields for STI classes.
107
+ *
108
+ * @param classDef - The raw class definition to resolve.
109
+ * @returns A {@link ResolvedClassDefinition} with all inherited metadata
110
+ * applied. The `packageName` field is left as `null` and must be set by
111
+ * the caller (e.g. {@link ManifestAdapter}).
112
+ *
113
+ * @see {@link resolveAll} to resolve every registered class at once.
114
+ */
115
+ resolve(classDef) {
116
+ const inheritanceChain = this.resolveInheritanceChain(classDef.className);
117
+ const stiBase = this.findSTIBase(inheritanceChain);
118
+ const effectiveTableStrategy = this.determineTableStrategy(classDef, inheritanceChain);
119
+ const isFrameworkBase = this.knownBaseClasses.has(classDef.className);
120
+ const isSTI = effectiveTableStrategy === "sti";
121
+ const allFields = isSTI ? this.mergeFieldsForSTI(inheritanceChain) : classDef.fields;
122
+ return {
123
+ ...classDef,
124
+ inheritanceChain,
125
+ stiBase,
126
+ effectiveTableStrategy,
127
+ isSTI,
128
+ isFrameworkBase,
129
+ allFields,
130
+ packageName: null
131
+ };
132
+ }
133
+ /**
134
+ * Resolve the full inheritance chain for a named class, from the root base
135
+ * class down to the named class itself.
136
+ *
137
+ * Results are memoised in an internal cache that is cleared whenever
138
+ * {@link addClasses} or {@link addExternalManifest} is called.
139
+ *
140
+ * @param className - Name of the class to resolve.
141
+ * @returns An ordered array of class names starting from the furthest
142
+ * ancestor and ending with `className`.
143
+ *
144
+ * @example
145
+ * ```typescript
146
+ * // Given: class Article extends Content, class Content extends SmrtObject
147
+ * resolver.resolveInheritanceChain('Article');
148
+ * // => ['SmrtObject', 'Content', 'Article']
149
+ * ```
150
+ */
151
+ resolveInheritanceChain(className) {
152
+ const cached = this.chainCache.get(className);
153
+ if (cached) return cached;
154
+ const chain = [];
155
+ const visited = /* @__PURE__ */ new Set();
156
+ let current = className;
157
+ while (current && !visited.has(current)) {
158
+ visited.add(current);
159
+ chain.unshift(current);
160
+ if (this.knownBaseClasses.has(current)) break;
161
+ current = this.findClassDefinition(current)?.extendsClause || null;
162
+ }
163
+ this.chainCache.set(className, chain);
164
+ return chain;
165
+ }
166
+ /**
167
+ * Look up a class definition by name, searching in priority order:
168
+ * 1. Local classes added via {@link addClasses}.
169
+ * 2. External package manifests added via {@link addExternalManifest}.
170
+ * 3. Built-in framework base classes (`SmrtObject`, `SmrtClass`,
171
+ * `SmrtCollection`) — returns a minimal stub definition so chain walking
172
+ * can terminate cleanly.
173
+ *
174
+ * @param className - Class name to look up.
175
+ * @returns The {@link RawClassDefinition} if found, or `null` if the class
176
+ * is unknown to the resolver.
177
+ */
178
+ findClassDefinition(className) {
179
+ const local = this.classMap.get(className);
180
+ if (local) return local;
181
+ for (const manifest of this.externalManifests.values()) {
182
+ const external = manifest.classes.get(className);
183
+ if (external) return external;
184
+ }
185
+ if (this.knownBaseClasses.has(className)) return {
186
+ className,
187
+ filePath: "",
188
+ extendsClause: null,
189
+ extendsTypeArg: null,
190
+ decoratorConfig: null,
191
+ hasSmartDecorator: false,
192
+ fields: [],
193
+ methods: [],
194
+ startLine: 0,
195
+ endLine: 0
196
+ };
197
+ return null;
198
+ }
199
+ /**
200
+ * Find the STI root class in a resolved inheritance chain.
201
+ *
202
+ * Walks the chain from base to leaf and returns the name of the first class
203
+ * whose `@smrt()` decorator explicitly declares `tableStrategy: 'sti'`.
204
+ *
205
+ * @param chain - Ordered inheritance chain (base → leaf) as returned by
206
+ * {@link resolveInheritanceChain}.
207
+ * @returns The class name of the STI root, or `null` if no class in the
208
+ * chain uses `tableStrategy: 'sti'`.
209
+ */
210
+ findSTIBase(chain) {
211
+ for (const className of chain) if (this.findClassDefinition(className)?.decoratorConfig?.tableStrategy === "sti") return className;
212
+ return null;
213
+ }
214
+ /**
215
+ * Determine the effective table strategy (`'sti'` or `'cti'`) for a class.
216
+ *
217
+ * Resolution order:
218
+ * 1. The class's own `@smrt({ tableStrategy })` declaration, if present.
219
+ * 2. The nearest ancestor that declares `tableStrategy: 'sti'` — STI is
220
+ * inherited automatically by all subclasses.
221
+ * 3. Defaults to `'cti'` if no STI ancestor is found.
222
+ *
223
+ * @param classDef - Raw class definition whose strategy is being determined.
224
+ * @param chain - Pre-resolved inheritance chain for `classDef` (base → leaf).
225
+ * @returns `'sti'` or `'cti'`.
226
+ */
227
+ determineTableStrategy(classDef, chain) {
228
+ if (classDef.decoratorConfig?.tableStrategy) return classDef.decoratorConfig.tableStrategy;
229
+ for (const className of chain) {
230
+ if (className === classDef.className) continue;
231
+ if (this.findClassDefinition(className)?.decoratorConfig?.tableStrategy === "sti") return "sti";
232
+ }
233
+ return "cti";
234
+ }
235
+ /**
236
+ * Merge fields from all classes in an STI inheritance chain.
237
+ *
238
+ * Iterates from the root base class to the leaf class so that base class
239
+ * fields appear first in the returned array. If a field name is declared in
240
+ * both an ancestor and a descendant, the ancestor's definition takes
241
+ * precedence (first-seen wins), preserving the base-class column layout.
242
+ *
243
+ * @param chain - Ordered inheritance chain (base → leaf) as returned by
244
+ * {@link resolveInheritanceChain}.
245
+ * @returns A deduplicated, ordered array of {@link RawFieldDefinition}
246
+ * covering every field in the STI hierarchy.
247
+ */
248
+ mergeFieldsForSTI(chain) {
249
+ const allFields = [];
250
+ const seenNames = /* @__PURE__ */ new Set();
251
+ for (const className of chain) {
252
+ const classDef = this.findClassDefinition(className);
253
+ if (!classDef) continue;
254
+ for (const field of classDef.fields) {
255
+ if (seenNames.has(field.name)) continue;
256
+ seenNames.add(field.name);
257
+ allFields.push(field);
258
+ }
259
+ }
260
+ return allFields;
261
+ }
262
+ /**
263
+ * Return all known descendants of a class.
264
+ *
265
+ * Useful for STI schema generation where the base table must accommodate
266
+ * columns from every subclass.
267
+ *
268
+ * @param className - The ancestor class name to search from.
269
+ * @returns An array of class names (local classes only) whose resolved
270
+ * inheritance chain includes `className`. Does not include `className`
271
+ * itself.
272
+ */
273
+ getDescendants(className) {
274
+ const descendants = [];
275
+ for (const [name] of this.classMap) {
276
+ if (name === className) continue;
277
+ if (this.resolveInheritanceChain(name).includes(className)) descendants.push(name);
278
+ }
279
+ return descendants;
280
+ }
281
+ /**
282
+ * Check whether a class participates in an STI hierarchy.
283
+ *
284
+ * @param className - Name of the class to check.
285
+ * @returns `true` if any class in the resolved inheritance chain declares
286
+ * `tableStrategy: 'sti'`, `false` otherwise.
287
+ */
288
+ isSTIClass(className) {
289
+ const chain = this.resolveInheritanceChain(className);
290
+ return this.findSTIBase(chain) !== null;
291
+ }
292
+ /**
293
+ * Return aggregate statistics about the classes registered with this resolver.
294
+ *
295
+ * @returns An object with:
296
+ * - `totalClasses` — total number of classes in the class map.
297
+ * - `smrtClasses` — classes that carry `@smrt()`.
298
+ * - `stiClasses` — `@smrt()` classes in an STI hierarchy.
299
+ * - `maxInheritanceDepth` — length of the deepest inheritance chain among
300
+ * `@smrt()` classes.
301
+ */
302
+ getStats() {
303
+ let smrtClasses = 0;
304
+ let stiClasses = 0;
305
+ let maxInheritanceDepth = 0;
306
+ for (const classDef of this.classMap.values()) if (classDef.hasSmartDecorator) {
307
+ smrtClasses++;
308
+ const chain = this.resolveInheritanceChain(classDef.className);
309
+ maxInheritanceDepth = Math.max(maxInheritanceDepth, chain.length);
310
+ if (this.findSTIBase(chain)) stiClasses++;
311
+ }
312
+ return {
313
+ totalClasses: this.classMap.size,
314
+ smrtClasses,
315
+ stiClasses,
316
+ maxInheritanceDepth
317
+ };
318
+ }
319
+ };
320
+ //#endregion
321
+ //#region src/oxc-parser.ts
322
+ function getLangFromFilename(filename) {
323
+ if (filename.endsWith(".tsx")) return "tsx";
324
+ if (filename.endsWith(".ts")) return "ts";
325
+ if (filename.endsWith(".jsx")) return "jsx";
326
+ return "js";
327
+ }
328
+ function getLineColumn(sourceText, offset) {
329
+ if (offset < 0 || offset > sourceText.length) return;
330
+ let line = 1;
331
+ let lastNewlinePos = -1;
332
+ for (let i = 0; i < offset; i++) if (sourceText[i] === "\n") {
333
+ line++;
334
+ lastNewlinePos = i;
335
+ }
336
+ return {
337
+ line,
338
+ column: offset - lastNewlinePos
339
+ };
340
+ }
341
+ function getRange(node) {
342
+ if (node.range) return node.range;
343
+ if (node.start !== void 0 && node.end !== void 0) return [node.start, node.end];
344
+ return null;
345
+ }
346
+ function sliceSource(node, sourceText) {
347
+ const range = getRange(node);
348
+ return range ? sourceText.slice(range[0], range[1]) : null;
349
+ }
350
+ function parseFile(filePath) {
351
+ const startTime = performance.now();
352
+ const errors = [];
353
+ const classes = [];
354
+ let typeAliases = {};
355
+ let smrtImports;
356
+ try {
357
+ const sourceText = readFileSync(filePath, "utf-8");
358
+ const result = parseSync(filePath, sourceText, {
359
+ lang: getLangFromFilename(filePath),
360
+ preserveParens: false
361
+ });
362
+ if (result.errors && result.errors.length > 0) for (const error of result.errors) {
363
+ const loc = error.labels?.[0] ? getLineColumn(sourceText, error.labels[0].start) : void 0;
364
+ errors.push({
365
+ message: error.message || "Parse error",
366
+ filePath,
367
+ line: loc?.line,
368
+ column: loc?.column,
369
+ severity: error.severity === "Error" ? "error" : "warning"
370
+ });
371
+ }
372
+ const program = result.program;
373
+ if (program?.body) {
374
+ const importAliases = extractImportAliases(program.body);
375
+ typeAliases = extractTypeAliases(program.body);
376
+ smrtImports = extractSmrtImports(program.body);
377
+ for (const node of program.body) {
378
+ const extracted = extractClassFromNode(node, filePath, sourceText, importAliases);
379
+ if (extracted) classes.push(extracted);
380
+ }
381
+ }
382
+ } catch (error) {
383
+ errors.push({
384
+ message: error instanceof Error ? error.message : String(error),
385
+ filePath,
386
+ severity: "error"
387
+ });
388
+ }
389
+ const result2 = {
390
+ filePath,
391
+ classes,
392
+ errors,
393
+ parseTimeMs: performance.now() - startTime,
394
+ typeAliases
395
+ };
396
+ if (smrtImports && smrtImports.size > 0) result2.smrtImports = smrtImports;
397
+ return result2;
398
+ }
399
+ function parseSource(sourceText, filename = "test.ts") {
400
+ const startTime = performance.now();
401
+ const errors = [];
402
+ const classes = [];
403
+ let typeAliases = {};
404
+ let smrtImports;
405
+ try {
406
+ const result = parseSync(filename, sourceText, {
407
+ lang: getLangFromFilename(filename),
408
+ preserveParens: false
409
+ });
410
+ if (result.errors && result.errors.length > 0) for (const error of result.errors) {
411
+ const loc = error.labels?.[0] ? getLineColumn(sourceText, error.labels[0].start) : void 0;
412
+ errors.push({
413
+ message: error.message || "Parse error",
414
+ filePath: filename,
415
+ line: loc?.line,
416
+ column: loc?.column,
417
+ severity: error.severity === "Error" ? "error" : "warning"
418
+ });
419
+ }
420
+ const program = result.program;
421
+ if (program?.body) {
422
+ const importAliases = extractImportAliases(program.body);
423
+ typeAliases = extractTypeAliases(program.body);
424
+ smrtImports = extractSmrtImports(program.body);
425
+ for (const node of program.body) {
426
+ const extracted = extractClassFromNode(node, filename, sourceText, importAliases);
427
+ if (extracted) classes.push(extracted);
428
+ }
429
+ }
430
+ } catch (error) {
431
+ errors.push({
432
+ message: error instanceof Error ? error.message : String(error),
433
+ filePath: filename,
434
+ severity: "error"
435
+ });
436
+ }
437
+ const result2 = {
438
+ filePath: filename,
439
+ classes,
440
+ errors,
441
+ parseTimeMs: performance.now() - startTime,
442
+ typeAliases
443
+ };
444
+ if (smrtImports && smrtImports.size > 0) result2.smrtImports = smrtImports;
445
+ return result2;
446
+ }
447
+ var FORBIDDEN_OBJECT_KEYS = /* @__PURE__ */ new Set([
448
+ "__proto__",
449
+ "constructor",
450
+ "prototype"
451
+ ]);
452
+ function isSafeObjectKey(key) {
453
+ return !FORBIDDEN_OBJECT_KEYS.has(key);
454
+ }
455
+ function extractImportAliases(body) {
456
+ const aliases = /* @__PURE__ */ new Map();
457
+ for (const node of body) if (node.type === "ImportDeclaration" && node.specifiers) {
458
+ for (const spec of node.specifiers) if (spec.type === "ImportSpecifier" && spec.imported && spec.local) {
459
+ const original = spec.imported.name;
460
+ const local = spec.local.name;
461
+ if (original !== local) aliases.set(local, original);
462
+ }
463
+ }
464
+ return aliases;
465
+ }
466
+ function extractSmrtImports(body) {
467
+ const imports = /* @__PURE__ */ new Map();
468
+ for (const node of body) {
469
+ if (node.type !== "ImportDeclaration") continue;
470
+ const source = node.source;
471
+ if (!source || typeof source.value !== "string") continue;
472
+ const moduleName = source.value;
473
+ if (!moduleName.startsWith("@happyvertical/smrt-")) continue;
474
+ let classSet = imports.get(moduleName);
475
+ if (!classSet) {
476
+ classSet = /* @__PURE__ */ new Set();
477
+ imports.set(moduleName, classSet);
478
+ }
479
+ if (!node.specifiers || node.specifiers.length === 0) {
480
+ classSet.add("*");
481
+ continue;
482
+ }
483
+ for (const spec of node.specifiers) if (spec.type === "ImportSpecifier" && spec.imported && spec.local) {
484
+ const importedName = spec.imported.name;
485
+ if (/^[A-Z][A-Za-z0-9]*$/.test(importedName)) classSet.add(importedName);
486
+ } else if (spec.type === "ImportNamespaceSpecifier") classSet.add("*");
487
+ else if (spec.type === "ImportDefaultSpecifier" && spec.local) {
488
+ const defaultName = spec.local.name;
489
+ if (/^[A-Z][A-Za-z0-9]*$/.test(defaultName)) classSet.add(defaultName);
490
+ }
491
+ }
492
+ return imports;
493
+ }
494
+ function extractTypeAliases(body) {
495
+ const aliases = {};
496
+ for (const node of body) {
497
+ if (node.type === "TSTypeAliasDeclaration") {
498
+ const name = node.id?.name;
499
+ const resolved = node.typeAnnotation ? extractTypeName(node.typeAnnotation) : null;
500
+ if (name && resolved && isSafeObjectKey(name)) aliases[name] = resolved;
501
+ }
502
+ if (node.type === "ExportNamedDeclaration" && node.declaration?.type === "TSTypeAliasDeclaration") {
503
+ const decl = node.declaration;
504
+ const name = decl.id?.name;
505
+ const resolved = decl.typeAnnotation ? extractTypeName(decl.typeAnnotation) : null;
506
+ if (name && resolved && isSafeObjectKey(name)) aliases[name] = resolved;
507
+ }
508
+ const enumDecl = node.type === "TSEnumDeclaration" ? node : node.type === "ExportNamedDeclaration" && node.declaration?.type === "TSEnumDeclaration" ? node.declaration : null;
509
+ if (enumDecl) {
510
+ const name = enumDecl.id?.name;
511
+ const members = enumDecl.body?.members ?? enumDecl.members;
512
+ if (name && isSafeObjectKey(name) && members && members.length > 0) {
513
+ const values = members.map((m) => {
514
+ if (m.initializer?.type === "Literal") {
515
+ const val = m.initializer.value;
516
+ if (typeof val === "string") return `'${val}'`;
517
+ if (typeof val === "number") return String(val);
518
+ }
519
+ return null;
520
+ }).filter((v) => v !== null);
521
+ if (values.length > 0) {
522
+ if (values.every((v) => v.startsWith("'"))) aliases[name] = values.join(" | ");
523
+ }
524
+ }
525
+ }
526
+ }
527
+ return aliases;
528
+ }
529
+ function extractClassFromNode(node, filePath, sourceText, importAliases) {
530
+ if (node.type === "ExportNamedDeclaration" && node.declaration) return extractClassFromNode(node.declaration, filePath, sourceText, importAliases);
531
+ if (node.type === "ExportDefaultDeclaration" && node.declaration) return extractClassFromNode(node.declaration, filePath, sourceText, importAliases);
532
+ if (node.type === "ClassDeclaration") return extractClassDeclaration(node, filePath, sourceText, importAliases);
533
+ return null;
534
+ }
535
+ function extractClassDeclaration(node, filePath, sourceText, importAliases) {
536
+ const className = node.id?.name || "AnonymousClass";
537
+ const decorators = node.decorators || [];
538
+ const smrtDecorator = decorators.find((d) => isSmrtDecorator(d));
539
+ const reportDecorator = decorators.find((d) => isNamedDecorator(d, "report"));
540
+ const tenantScopedDecorator = decorators.find((d) => isNamedDecorator(d, "TenantScoped"));
541
+ const hasSmartDecorator = !!smrtDecorator;
542
+ const smrtConfig = smrtDecorator ? extractDecoratorConfig(smrtDecorator, sourceText) : null;
543
+ const decoratorConfig = tenantScopedDecorator || reportDecorator ? {
544
+ ...smrtConfig ?? {},
545
+ ...reportDecorator ? { report: extractDecoratorConfig(reportDecorator, sourceText) } : {},
546
+ ...tenantScopedDecorator ? { tenantScoped: extractDecoratorConfig(tenantScopedDecorator, sourceText) } : {}
547
+ } : smrtConfig;
548
+ const { extendsClause, extendsTypeArg } = extractExtendsClause(node, importAliases);
549
+ const fields = [];
550
+ const methods = [];
551
+ for (const member of node.body.body) if (member.type === "PropertyDefinition") {
552
+ const field = extractPropertyDefinition(member, sourceText);
553
+ if (field) fields.push(field);
554
+ } else if (member.type === "MethodDefinition") {
555
+ const method = extractMethodDefinition(member, sourceText);
556
+ if (method) methods.push(method);
557
+ }
558
+ return {
559
+ className,
560
+ filePath,
561
+ extendsClause,
562
+ extendsTypeArg,
563
+ decoratorConfig,
564
+ hasSmartDecorator,
565
+ fields,
566
+ methods,
567
+ startLine: node.loc?.start.line || 1,
568
+ endLine: node.loc?.end.line || 1
569
+ };
570
+ }
571
+ function isSmrtDecorator(decorator) {
572
+ return isNamedDecorator(decorator, "smrt");
573
+ }
574
+ function isNamedDecorator(decorator, name) {
575
+ const expr = decorator.expression;
576
+ if (expr.type === "CallExpression") {
577
+ const callee = expr.callee;
578
+ if (callee.type === "Identifier" && callee.name === name) return true;
579
+ }
580
+ if (expr.type === "Identifier" && expr.name === name) return true;
581
+ return false;
582
+ }
583
+ function extractDecoratorConfig(decorator, sourceText) {
584
+ const expr = decorator.expression;
585
+ if (expr.type === "CallExpression" && expr.arguments.length > 0) {
586
+ const arg = expr.arguments[0];
587
+ if (arg.type === "ObjectExpression") return extractObjectLiteral(arg, sourceText);
588
+ }
589
+ return {};
590
+ }
591
+ function extractObjectLiteral(node, sourceText) {
592
+ const result = {};
593
+ for (const prop of node.properties) if (prop.type === "Property" && !prop.computed) {
594
+ const key = getPropertyKey(prop.key);
595
+ if (key && isSafeObjectKey(key)) result[key] = extractValue(prop.value, sourceText);
596
+ }
597
+ return result;
598
+ }
599
+ function getPropertyKey(node) {
600
+ if (node.type === "Identifier") return node.name;
601
+ if (node.type === "Literal" && typeof node.value === "string") return node.value;
602
+ return null;
603
+ }
604
+ function extractValue(node, sourceText) {
605
+ switch (node.type) {
606
+ case "Literal": return node.value;
607
+ case "Identifier":
608
+ if (node.name === "undefined") return void 0;
609
+ if (node.name === "null") return null;
610
+ if (node.name === "true") return true;
611
+ if (node.name === "false") return false;
612
+ return node.name;
613
+ case "ArrayExpression": return node.elements.filter((el) => el !== null && typeof el === "object" && "type" in el && el.type !== "SpreadElement").map((el) => extractValue(el, sourceText));
614
+ case "ObjectExpression": return extractObjectLiteral(node, sourceText);
615
+ case "UnaryExpression":
616
+ if (node.operator === "-" && node.argument?.type === "Literal") {
617
+ const value = node.argument.value;
618
+ if (typeof value === "number") return -value;
619
+ }
620
+ break;
621
+ case "CallExpression":
622
+ case "NewExpression": {
623
+ const src = sliceSource(node, sourceText);
624
+ if (src) return src;
625
+ break;
626
+ }
627
+ }
628
+ const rawSrc = sliceSource(node, sourceText);
629
+ if (rawSrc) return rawSrc;
630
+ }
631
+ function extractExtendsClause(node, importAliases) {
632
+ if (!node.superClass) return {
633
+ extendsClause: null,
634
+ extendsTypeArg: null
635
+ };
636
+ let extendsClause = null;
637
+ let extendsTypeArg = null;
638
+ if (node.superClass.type === "Identifier") extendsClause = node.superClass.name;
639
+ else if (node.superClass.type === "MemberExpression") extendsClause = getMemberExpressionString(node.superClass);
640
+ if (extendsClause && importAliases.has(extendsClause)) {
641
+ const aliasedExtends = importAliases.get(extendsClause);
642
+ if (aliasedExtends) extendsClause = aliasedExtends;
643
+ }
644
+ const params = node.superTypeArguments?.params || node.superTypeParameters?.params;
645
+ if (params && params.length > 0) {
646
+ const typeParam = params[0];
647
+ extendsTypeArg = extractTypeName(typeParam);
648
+ }
649
+ return {
650
+ extendsClause,
651
+ extendsTypeArg
652
+ };
653
+ }
654
+ function getMemberExpressionString(node) {
655
+ const parts = [];
656
+ let current = node;
657
+ while (current.type === "MemberExpression") {
658
+ if (current.property.type === "Identifier") parts.unshift(current.property.name);
659
+ current = current.object;
660
+ }
661
+ if (current.type === "Identifier") parts.unshift(current.name);
662
+ return parts.join(".");
663
+ }
664
+ function reconstructCallExpression(node, sourceText) {
665
+ const src = sliceSource(node, sourceText);
666
+ if (src) return src;
667
+ let callee = "";
668
+ if (node.callee.type === "Identifier") callee = node.callee.name;
669
+ else if (node.callee.type === "MemberExpression") callee = getMemberExpressionString(node.callee);
670
+ else return null;
671
+ const args = [];
672
+ for (const arg of node.arguments) {
673
+ const argSrc = sliceSource(arg, sourceText);
674
+ if (argSrc) args.push(argSrc);
675
+ else if (arg.type === "Identifier") args.push(arg.name);
676
+ else if (arg.type === "Literal") args.push(arg.raw || String(arg.value));
677
+ else if (arg.type === "ObjectExpression") {
678
+ const objStr = reconstructObjectExpression(arg, sourceText);
679
+ if (objStr) args.push(objStr);
680
+ } else args.push("...");
681
+ }
682
+ return `${callee}(${args.join(", ")})`;
683
+ }
684
+ function reconstructObjectExpression(node, sourceText) {
685
+ const src = sliceSource(node, sourceText);
686
+ if (src) return src;
687
+ const props = [];
688
+ for (const prop of node.properties) {
689
+ if (prop.type === "SpreadElement") continue;
690
+ if (prop.type === "Property") {
691
+ let key = "";
692
+ if (prop.key.type === "Identifier") key = prop.key.name;
693
+ else if (prop.key.type === "Literal") key = String(prop.key.value);
694
+ if (!key) continue;
695
+ let value = "";
696
+ const valSrc = sliceSource(prop.value, sourceText);
697
+ if (valSrc) value = valSrc;
698
+ else if (prop.value.type === "ObjectExpression") value = reconstructObjectExpression(prop.value, sourceText) || "";
699
+ else if (prop.value.type === "ArrayExpression") value = reconstructArrayExpression(prop.value, sourceText) || "";
700
+ else if (prop.value.type === "Identifier") value = prop.value.name;
701
+ else if (prop.value.type === "Literal") value = prop.value.raw || String(prop.value.value);
702
+ if (value) props.push(`${key}: ${value}`);
703
+ }
704
+ }
705
+ return `{ ${props.join(", ")} }`;
706
+ }
707
+ function reconstructArrayExpression(node, sourceText) {
708
+ const src = sliceSource(node, sourceText);
709
+ if (src) return src;
710
+ const elements = [];
711
+ for (const el of node.elements) {
712
+ if (!el) continue;
713
+ if (el.type === "SpreadElement") elements.push("...");
714
+ else {
715
+ const elSrc = sliceSource(el, sourceText);
716
+ if (elSrc) elements.push(elSrc);
717
+ else if (el.type === "Identifier") elements.push(el.name);
718
+ else if (el.type === "Literal") elements.push(el.raw || String(el.value));
719
+ else if (el.type === "ObjectExpression") {
720
+ const objStr = reconstructObjectExpression(el, sourceText);
721
+ if (objStr) elements.push(objStr);
722
+ }
723
+ }
724
+ }
725
+ return `[${elements.join(", ")}]`;
726
+ }
727
+ function extractTypeName(type) {
728
+ switch (type.type) {
729
+ case "TSTypeReference": {
730
+ let baseName = null;
731
+ if (type.typeName.type === "Identifier") baseName = type.typeName.name;
732
+ else if (type.typeName.type === "TSQualifiedName") baseName = getQualifiedName(type.typeName);
733
+ const typeParams = type.typeArguments?.params || type.typeParameters?.params;
734
+ if (baseName && typeParams?.length) {
735
+ const typeArgs = typeParams.map((p) => extractTypeName(p)).filter(Boolean);
736
+ if (typeArgs.length > 0) return `${baseName}<${typeArgs.join(", ")}>`;
737
+ }
738
+ return baseName;
739
+ }
740
+ case "TSStringKeyword": return "string";
741
+ case "TSNumberKeyword": return "number";
742
+ case "TSBooleanKeyword": return "boolean";
743
+ case "TSAnyKeyword": return "any";
744
+ case "TSVoidKeyword": return "void";
745
+ case "TSNullKeyword": return "null";
746
+ case "TSUndefinedKeyword": return "undefined";
747
+ case "TSLiteralType": {
748
+ const literal = type.literal;
749
+ if (!literal) return null;
750
+ if (typeof literal.value === "string") return `'${literal.value}'`;
751
+ if (typeof literal.value === "number") return String(literal.value);
752
+ if (typeof literal.value === "boolean") return String(literal.value);
753
+ return null;
754
+ }
755
+ case "TSArrayType": {
756
+ const elementType = extractTypeName(type.elementType);
757
+ return elementType ? `${elementType}[]` : null;
758
+ }
759
+ case "TSUnionType": return type.types.map((t) => extractTypeName(t)).filter(Boolean).join(" | ");
760
+ case "TSTypeLiteral": return "object";
761
+ case "TSFunctionType": return "Function";
762
+ default: return null;
763
+ }
764
+ }
765
+ function getQualifiedName(node) {
766
+ const parts = [];
767
+ let current = node;
768
+ while (current.type === "TSQualifiedName") {
769
+ parts.unshift(current.right.name);
770
+ current = current.left;
771
+ }
772
+ if (current.type === "Identifier") parts.unshift(current.name);
773
+ return parts.join(".");
774
+ }
775
+ function extractPropertyDefinition(node, sourceText) {
776
+ if (node.computed) return null;
777
+ const name = getPropertyKey(node.key);
778
+ if (!name) return null;
779
+ if (!isSafeObjectKey(name)) return null;
780
+ const typeAnnotation = node.typeAnnotation ? extractTypeName(node.typeAnnotation.typeAnnotation) : null;
781
+ let initializer = null;
782
+ let hasDecimalPoint = false;
783
+ let numericValue = null;
784
+ if (node.value) {
785
+ if (node.value.type === "UnaryExpression" && node.value.operator === "-" && node.value.argument?.type === "Literal" && typeof node.value.argument.value === "number") {
786
+ numericValue = -node.value.argument.value;
787
+ if (node.value.argument.raw) hasDecimalPoint = node.value.argument.raw.includes(".");
788
+ } else if (node.value.type === "Literal" && typeof node.value.value === "number") {
789
+ numericValue = node.value.value;
790
+ if (node.value.raw) hasDecimalPoint = node.value.raw.includes(".");
791
+ }
792
+ const valueSrc = sliceSource(node.value, sourceText);
793
+ if (valueSrc) initializer = valueSrc;
794
+ else if (node.value.type === "Literal" && node.value.raw) initializer = node.value.raw;
795
+ else if (node.value.type === "Literal") {
796
+ const val = node.value.value;
797
+ if (typeof val === "string") initializer = `'${val}'`;
798
+ else if (val !== null && val !== void 0) initializer = String(val);
799
+ } else if (node.value.type === "CallExpression" || node.value.type === "NewExpression") initializer = reconstructCallExpression(node.value, sourceText);
800
+ else if (node.value.type === "ArrayExpression") initializer = reconstructArrayExpression(node.value, sourceText);
801
+ else if (node.value.type === "ObjectExpression") initializer = reconstructObjectExpression(node.value, sourceText);
802
+ }
803
+ const decorators = [];
804
+ if (node.decorators) for (const dec of node.decorators) {
805
+ const extracted = extractFieldDecorator(dec, sourceText);
806
+ if (extracted) decorators.push(extracted);
807
+ }
808
+ return {
809
+ name,
810
+ typeAnnotation,
811
+ initializer,
812
+ hasDecimalPoint,
813
+ numericValue,
814
+ decorators,
815
+ optional: node.optional || false,
816
+ isStatic: node.static || false,
817
+ readonly: node.readonly || false,
818
+ accessibility: node.accessibility || "public",
819
+ line: node.loc?.start.line || 0
820
+ };
821
+ }
822
+ function extractFieldDecorator(decorator, sourceText) {
823
+ const expr = decorator.expression;
824
+ let name = null;
825
+ const args = [];
826
+ if (expr.type === "CallExpression") {
827
+ if (expr.callee.type === "Identifier") name = expr.callee.name;
828
+ for (const arg of expr.arguments) {
829
+ const argSrc = sliceSource(arg, sourceText);
830
+ if (argSrc) args.push(argSrc);
831
+ }
832
+ } else if (expr.type === "Identifier") name = expr.name;
833
+ if (!name) return null;
834
+ return {
835
+ name,
836
+ arguments: args
837
+ };
838
+ }
839
+ function extractMethodDefinition(node, sourceText) {
840
+ if (node.kind !== "method") return null;
841
+ const name = getPropertyKey(node.key);
842
+ if (!name) return null;
843
+ const func = node.value;
844
+ const parameters = [];
845
+ for (const param of func.params) {
846
+ const extracted = extractParameter(param, sourceText);
847
+ if (extracted) parameters.push(extracted);
848
+ }
849
+ const returnType = func.returnType ? extractTypeName(func.returnType.typeAnnotation) : null;
850
+ return {
851
+ name,
852
+ async: func.async,
853
+ isStatic: node.static,
854
+ accessibility: node.accessibility || "public",
855
+ parameters,
856
+ returnType,
857
+ description: null,
858
+ line: node.loc?.start.line || 0
859
+ };
860
+ }
861
+ function extractParameter(param, sourceText) {
862
+ if (param.type === "AssignmentPattern") {
863
+ const left = param.left;
864
+ if (left.type === "Identifier") return {
865
+ name: left.name,
866
+ type: left.typeAnnotation ? extractTypeName(left.typeAnnotation.typeAnnotation) : null,
867
+ optional: true,
868
+ defaultValue: sliceSource(param.right, sourceText)
869
+ };
870
+ if (left.type === "ObjectPattern" || left.type === "ArrayPattern") return {
871
+ name: "options",
872
+ type: left.typeAnnotation ? extractTypeName(left.typeAnnotation.typeAnnotation) : "any",
873
+ optional: true,
874
+ defaultValue: sliceSource(param.right, sourceText)
875
+ };
876
+ return null;
877
+ }
878
+ if (param.type === "RestElement") {
879
+ const arg = param.argument;
880
+ if (arg.type === "Identifier") return {
881
+ name: `...${arg.name}`,
882
+ type: param.typeAnnotation ? extractTypeName(param.typeAnnotation.typeAnnotation) : null,
883
+ optional: true,
884
+ defaultValue: null
885
+ };
886
+ return null;
887
+ }
888
+ if (param.type === "Identifier") return {
889
+ name: param.name,
890
+ type: param.typeAnnotation ? extractTypeName(param.typeAnnotation.typeAnnotation) : null,
891
+ optional: param.optional || false,
892
+ defaultValue: null
893
+ };
894
+ if (param.type === "ObjectPattern") return {
895
+ name: "options",
896
+ type: param.typeAnnotation ? extractTypeName(param.typeAnnotation.typeAnnotation) : "any",
897
+ optional: false,
898
+ defaultValue: null
899
+ };
900
+ return null;
901
+ }
902
+ //#endregion
903
+ //#region src/manifest-adapter.ts
904
+ function sanitizeParsed(value, seen = /* @__PURE__ */ new WeakSet()) {
905
+ if (value === null || typeof value !== "object") return value;
906
+ const isArray = Array.isArray(value);
907
+ const proto = Object.getPrototypeOf(value);
908
+ if (!isArray && proto !== Object.prototype && proto !== null) return value;
909
+ if (seen.has(value)) return void 0;
910
+ seen.add(value);
911
+ if (isArray) return value.map((item) => sanitizeParsed(item, seen));
912
+ const clean = {};
913
+ for (const key of Object.keys(value)) {
914
+ if (!isSafeObjectKey(key)) continue;
915
+ clean[key] = sanitizeParsed(value[key], seen);
916
+ }
917
+ return clean;
918
+ }
919
+ function parseLiteralInitializer(source) {
920
+ const trimmed = source?.trim();
921
+ if (!trimmed || !trimmed.startsWith("{") && !trimmed.startsWith("[")) return null;
922
+ try {
923
+ return sanitizeParsed(new Function(`return (${source})`)());
924
+ } catch {
925
+ return null;
926
+ }
927
+ }
928
+ function stripQuotes(value) {
929
+ if (!value) return value;
930
+ const match = value.match(/^(['"`])(.+)\1$/);
931
+ return match ? match[2] : value;
932
+ }
933
+ function createQualifiedName(packageName, className) {
934
+ return `${packageName}:${className}`;
935
+ }
936
+ var ManifestAdapter = class ManifestAdapter {
937
+ typeAliases = {};
938
+ _aliasDepth;
939
+ /**
940
+ * Convert an array of resolved class definitions into a `SmartObjectManifest`.
941
+ *
942
+ * Each class is converted to a `SmartObjectDefinition` via
943
+ * {@link toSmartObjectDefinition} and stored under its qualified name key
944
+ * (e.g. `@my-org/my-package:MyClass`) when `packageName` is provided, or
945
+ * under its lowercased class name otherwise.
946
+ *
947
+ * @param resolved - Resolved class definitions from {@link OxcScanner.resolve}
948
+ * or {@link OxcScanner.scanAndResolve}.
949
+ * @param options.packageName - npm package name used to generate qualified
950
+ * class names for namespace isolation across multi-package projects.
951
+ * @param options.packageVersion - Package version recorded in the manifest
952
+ * metadata.
953
+ * @param options.typeAliases - Map of type alias names to their resolved type
954
+ * strings (from {@link ScanResults.typeAliases}). Used to resolve custom
955
+ * types like `type Status = 'active' | 'inactive'` during field inference.
956
+ * @returns A complete `SmartObjectManifest` ready for serialisation.
957
+ *
958
+ * @example
959
+ * ```typescript
960
+ * const manifest = adapter.toManifest(resolved, {
961
+ * packageName: '@my-org/my-package',
962
+ * packageVersion: '1.0.0',
963
+ * typeAliases: results.typeAliases,
964
+ * });
965
+ * fs.writeFileSync('manifest.json', JSON.stringify(manifest, null, 2));
966
+ * ```
967
+ */
968
+ toManifest(resolved, options = {}) {
969
+ this.typeAliases = options.typeAliases || {};
970
+ const objects = {};
971
+ for (const classDef of resolved) {
972
+ const definition = this.toSmartObjectDefinition(classDef, options);
973
+ const manifestKey = definition.qualifiedName || definition.name.toLowerCase();
974
+ objects[manifestKey] = definition;
975
+ }
976
+ return {
977
+ version: "1.0.0",
978
+ timestamp: Date.now(),
979
+ packageName: options.packageName,
980
+ packageVersion: options.packageVersion,
981
+ objects,
982
+ moduleType: "smrt"
983
+ };
984
+ }
985
+ /**
986
+ * Convert a single resolved class definition to a `SmartObjectDefinition`.
987
+ *
988
+ * Handles:
989
+ * - Static property capture (`uiSlots`, `adminRoutes`) with child-wins
990
+ * semantics for overridden statics.
991
+ * - Field conversion (non-static public fields only) via {@link convertField}.
992
+ * - Method conversion (public instance/static methods) via {@link convertMethod}.
993
+ * - Collection name pluralisation.
994
+ * - Qualified name generation when `packageName` is supplied.
995
+ *
996
+ * @param classDef - A fully-resolved class definition.
997
+ * @param options.packageName - Package name used to build the qualified class
998
+ * name (`@pkg:ClassName`).
999
+ * @param options.packageVersion - Package version (informational, stored in
1000
+ * the definition).
1001
+ * @returns A `SmartObjectDefinition` ready to be stored in a manifest.
1002
+ *
1003
+ * @see {@link toManifest} for the bulk conversion entry point.
1004
+ */
1005
+ toSmartObjectDefinition(classDef, options = {}) {
1006
+ let staticProperties;
1007
+ const knownStaticProps = [
1008
+ "uiSlots",
1009
+ "adminRoutes",
1010
+ "signalSubscriptions"
1011
+ ];
1012
+ const ownStaticNames = /* @__PURE__ */ new Set();
1013
+ for (const field of classDef.fields) if (field.isStatic && knownStaticProps.includes(field.name) && field.initializer) try {
1014
+ const parsed = parseLiteralInitializer(field.initializer);
1015
+ if (parsed) {
1016
+ if (!staticProperties) staticProperties = {};
1017
+ staticProperties[field.name] = parsed;
1018
+ ownStaticNames.add(field.name);
1019
+ }
1020
+ } catch {}
1021
+ for (const field of classDef.allFields) if (field.isStatic && knownStaticProps.includes(field.name) && field.initializer) {
1022
+ if (ownStaticNames.has(field.name)) continue;
1023
+ try {
1024
+ const parsed = parseLiteralInitializer(field.initializer);
1025
+ if (parsed) {
1026
+ if (!staticProperties) staticProperties = {};
1027
+ staticProperties[field.name] = parsed;
1028
+ }
1029
+ } catch {}
1030
+ }
1031
+ const fields = {};
1032
+ for (const field of classDef.allFields) {
1033
+ if (field.isStatic) continue;
1034
+ const converted = this.convertField(field);
1035
+ if (converted) fields[field.name] = converted;
1036
+ }
1037
+ const methods = {};
1038
+ for (const method of classDef.methods) {
1039
+ const converted = this.convertMethod(method);
1040
+ if (converted) methods[method.name] = converted;
1041
+ }
1042
+ const collection = this.pluralize(classDef.className);
1043
+ const packageName = options.packageName || classDef.packageName;
1044
+ const qualifiedName = packageName ? createQualifiedName(packageName, classDef.className) : void 0;
1045
+ return {
1046
+ name: classDef.className.toLowerCase(),
1047
+ className: classDef.className,
1048
+ qualifiedName,
1049
+ collection,
1050
+ filePath: classDef.filePath,
1051
+ packageName: packageName || void 0,
1052
+ fields,
1053
+ methods,
1054
+ decoratorConfig: classDef.decoratorConfig || {},
1055
+ extends: classDef.extendsClause || void 0,
1056
+ extendsTypeArg: classDef.extendsTypeArg || void 0,
1057
+ exportName: classDef.className,
1058
+ collectionExportName: `${classDef.className}Collection`,
1059
+ staticProperties
1060
+ };
1061
+ }
1062
+ /**
1063
+ * Framework internal fields that should NOT be included in manifests
1064
+ * These are SmrtObject internals used by the framework, not user-defined fields
1065
+ */
1066
+ static FRAMEWORK_INTERNAL_FIELDS = /* @__PURE__ */ new Set([
1067
+ "_tableName",
1068
+ "options",
1069
+ "_loadedRelationships",
1070
+ "_db",
1071
+ "_ai",
1072
+ "_fs",
1073
+ "_isInitialized",
1074
+ "_errors",
1075
+ "_warnings"
1076
+ ]);
1077
+ /**
1078
+ * Convert a single raw field definition to a manifest `FieldDefinition`.
1079
+ *
1080
+ * Returns `null` for fields that should be omitted from the manifest:
1081
+ * - `private` or `protected` fields.
1082
+ * - Framework-internal fields (`_tableName`, `_db`, `_ai`, etc.).
1083
+ *
1084
+ * Delegates type inference to {@link inferFieldType} and applies additional
1085
+ * post-processing:
1086
+ * - Marks fields with `Function` type annotation as `transient`.
1087
+ * - Marks fields with `@field({ transient: true })` decorator as `transient`.
1088
+ * - Populates `_meta.underlyingType` for STI `Meta<T>` fields.
1089
+ *
1090
+ * @param field - Raw field definition from a scanned class.
1091
+ * @returns A `FieldDefinition` for the manifest, or `null` if the field
1092
+ * should be excluded.
1093
+ *
1094
+ * @see {@link inferFieldType} for the type inference logic.
1095
+ */
1096
+ convertField(field) {
1097
+ if (field.accessibility !== "public") return null;
1098
+ if (ManifestAdapter.FRAMEWORK_INTERNAL_FIELDS.has(field.name)) return null;
1099
+ const isFunctionType = field.typeAnnotation === "Function";
1100
+ const fieldDecoratorOptions = this.extractFieldDecoratorOptions(field);
1101
+ const inference = this.inferFieldType(field);
1102
+ const definition = {
1103
+ type: inference.type,
1104
+ required: inference.required
1105
+ };
1106
+ if (inference.related) definition.related = inference.related;
1107
+ if (inference.defaultValue !== void 0) definition.default = inference.defaultValue;
1108
+ if (fieldDecoratorOptions.type) definition.type = fieldDecoratorOptions.type;
1109
+ if (fieldDecoratorOptions.nullable === true) definition.required = false;
1110
+ else if (fieldDecoratorOptions.required !== void 0) definition.required = fieldDecoratorOptions.required;
1111
+ if (fieldDecoratorOptions.default !== void 0) definition.default = fieldDecoratorOptions.default;
1112
+ if (fieldDecoratorOptions.related !== void 0) definition.related = fieldDecoratorOptions.related;
1113
+ if (fieldDecoratorOptions.description !== void 0) definition.description = fieldDecoratorOptions.description;
1114
+ if (fieldDecoratorOptions.min !== void 0) definition.min = fieldDecoratorOptions.min;
1115
+ if (fieldDecoratorOptions.max !== void 0) definition.max = fieldDecoratorOptions.max;
1116
+ if (fieldDecoratorOptions.minLength !== void 0) definition.minLength = fieldDecoratorOptions.minLength;
1117
+ if (fieldDecoratorOptions.maxLength !== void 0) definition.maxLength = fieldDecoratorOptions.maxLength;
1118
+ if (Object.keys(fieldDecoratorOptions).length > 0) {
1119
+ definition._meta = {
1120
+ ...definition._meta,
1121
+ ...fieldDecoratorOptions
1122
+ };
1123
+ if (definition._meta?.type) delete definition._meta.type;
1124
+ if (definition.related !== void 0 && definition._meta?.related) delete definition._meta.related;
1125
+ }
1126
+ if (inference._meta && Object.keys(inference._meta).length > 0) definition._meta = {
1127
+ ...definition._meta,
1128
+ ...inference._meta
1129
+ };
1130
+ if (inference.underlyingType) definition._meta = {
1131
+ ...definition._meta,
1132
+ underlyingType: inference.underlyingType
1133
+ };
1134
+ if (isFunctionType) definition.transient = true;
1135
+ if (fieldDecoratorOptions.transient === true) definition.transient = true;
1136
+ if (fieldDecoratorOptions.sensitive === true) definition.sensitive = true;
1137
+ if (fieldDecoratorOptions.readonly === true) definition.readonly = true;
1138
+ return definition;
1139
+ }
1140
+ /**
1141
+ * Infer the SMRT field type and required flag from a raw field definition.
1142
+ *
1143
+ * Inference is attempted in the following priority order:
1144
+ * 1. **Field helper call in initializer** — currently always returns `null`
1145
+ * (field helpers removed); reserved for future use.
1146
+ * 2. **Decorator** — `@foreignKey`, `@oneToMany`, `@manyToMany`, `@field({ type })`.
1147
+ * 3. **Type annotation** — `string` → `text`, `number` with `0` vs `0.0`
1148
+ * heuristic → `integer` / `decimal`, `boolean`, `Date` → `datetime`,
1149
+ * arrays → `json`, `Record<>` / `object` → `json`, union types with
1150
+ * `null`, inline string/number literal unions, `Meta<T>` wrapper,
1151
+ * and type alias resolution (up to depth 5).
1152
+ * 4. **Numeric literal without annotation** — `version = 1` → `integer`.
1153
+ * 5. **Boolean literal without annotation** — `isRead = false` → `boolean`.
1154
+ * 6. **Default** — falls back to `text`.
1155
+ *
1156
+ * @param field - The raw field definition to analyse.
1157
+ * @returns A {@link FieldTypeInference} describing the inferred type,
1158
+ * required flag, default value, related class name (for relationships),
1159
+ * and the inference source for debugging.
1160
+ *
1161
+ * @see {@link FieldTypeInference} for the result shape.
1162
+ * @see {@link InferredFieldType} for valid type values.
1163
+ */
1164
+ inferFieldType(field) {
1165
+ if (field.initializer) {
1166
+ const helperResult = this.inferFromHelper(field.initializer);
1167
+ if (helperResult) return helperResult;
1168
+ }
1169
+ for (const decorator of field.decorators) {
1170
+ const decoratorResult = this.inferFromDecorator(decorator, field);
1171
+ if (decoratorResult) return decoratorResult;
1172
+ }
1173
+ if (field.typeAnnotation) return this.inferFromAnnotation(field);
1174
+ if (field.numericValue !== null) return {
1175
+ type: field.hasDecimalPoint ? "decimal" : "integer",
1176
+ required: !field.optional,
1177
+ defaultValue: field.numericValue,
1178
+ source: "heuristic"
1179
+ };
1180
+ if (field.initializer === "true" || field.initializer === "false") return {
1181
+ type: "boolean",
1182
+ required: !field.optional,
1183
+ defaultValue: field.initializer === "true",
1184
+ source: "heuristic"
1185
+ };
1186
+ const hasDefaultValue = field.initializer !== null;
1187
+ return {
1188
+ type: "text",
1189
+ required: !field.optional && !hasDefaultValue,
1190
+ source: "default"
1191
+ };
1192
+ }
1193
+ /**
1194
+ * Infer type from field helper call (removed)
1195
+ *
1196
+ * Field helpers have been removed in favor of decorators and TypeScript types:
1197
+ * - Use TypeScript types: name: string = '', price: number = 0.0
1198
+ * - Use @field() decorator for constraints: @field({ required: true })
1199
+ * - Use @foreignKey(), @oneToMany(), @manyToMany() decorators for relationships
1200
+ */
1201
+ inferFromHelper(_initializer) {
1202
+ return null;
1203
+ }
1204
+ /**
1205
+ * Infer type from field decorator
1206
+ */
1207
+ inferFromDecorator(decorator, field) {
1208
+ if (decorator.name === "field" && decorator.arguments.length > 0) {
1209
+ const fieldOptions = this.parseFieldDecoratorOptions(decorator.arguments[0]);
1210
+ const type = this.normalizeFieldType(fieldOptions?.type);
1211
+ if (type) {
1212
+ const hasDefaultValue = field.initializer !== null || fieldOptions?.default !== void 0;
1213
+ let required = !field.optional && !hasDefaultValue;
1214
+ if (fieldOptions?.nullable === true) required = false;
1215
+ else if (fieldOptions?.required !== void 0) required = fieldOptions.required;
1216
+ return {
1217
+ type,
1218
+ required,
1219
+ defaultValue: fieldOptions?.default,
1220
+ related: typeof fieldOptions?.related === "string" ? fieldOptions.related : void 0,
1221
+ source: "decorator"
1222
+ };
1223
+ }
1224
+ }
1225
+ if (decorator.name === "meta") {
1226
+ const parsedOptions = this.parseFieldDecoratorOptions(decorator.arguments[0]);
1227
+ const hasDefaultValue = field.initializer !== null;
1228
+ const meta = {};
1229
+ if (parsedOptions?.indexed !== void 0) meta.indexed = parsedOptions.indexed;
1230
+ if (parsedOptions?.nullable !== void 0) meta.nullable = parsedOptions.nullable;
1231
+ return {
1232
+ type: "meta",
1233
+ required: parsedOptions?.required !== void 0 ? Boolean(parsedOptions.required) : !field.optional && !hasDefaultValue,
1234
+ defaultValue: parsedOptions?.default !== void 0 ? parsedOptions.default : void 0,
1235
+ ...Object.keys(meta).length > 0 ? { _meta: meta } : {},
1236
+ source: "decorator"
1237
+ };
1238
+ }
1239
+ if (decorator.name === "foreignKey") {
1240
+ const relatedClass = stripQuotes(decorator.arguments[0]?.trim());
1241
+ const parsedOptions = this.parseFieldDecoratorOptions(decorator.arguments[1]);
1242
+ const meta = {};
1243
+ const META_KEYS = [
1244
+ "required",
1245
+ "nullable",
1246
+ "unique",
1247
+ "description",
1248
+ "default"
1249
+ ];
1250
+ if (parsedOptions) {
1251
+ for (const key of META_KEYS) if (parsedOptions[key] !== void 0) meta[key] = parsedOptions[key];
1252
+ }
1253
+ const hasDefaultValue = field.initializer !== null;
1254
+ return {
1255
+ type: "foreignKey",
1256
+ related: relatedClass || void 0,
1257
+ required: parsedOptions?.required !== void 0 ? Boolean(parsedOptions.required) : !field.optional && !hasDefaultValue,
1258
+ defaultValue: parsedOptions?.default !== void 0 ? parsedOptions.default : void 0,
1259
+ ...Object.keys(meta).length > 0 ? { _meta: meta } : {},
1260
+ source: "decorator"
1261
+ };
1262
+ }
1263
+ if (decorator.name === "tenantId") {
1264
+ const parsedOptions = this.parseFieldDecoratorOptions(decorator.arguments[0]);
1265
+ const nullable = parsedOptions?.nullable === true;
1266
+ const required = parsedOptions?.required !== void 0 ? Boolean(parsedOptions.required) : !nullable;
1267
+ return {
1268
+ type: "text",
1269
+ required,
1270
+ _meta: {
1271
+ sqlType: "UUID",
1272
+ ...parsedOptions ?? {},
1273
+ __tenancy: {
1274
+ isTenantIdField: true,
1275
+ autoFilter: parsedOptions?.autoFilter ?? true,
1276
+ required,
1277
+ autoPopulate: parsedOptions?.autoPopulate ?? true,
1278
+ nullable
1279
+ }
1280
+ },
1281
+ source: "decorator"
1282
+ };
1283
+ }
1284
+ if (decorator.name === "crossPackageRef") {
1285
+ const qualifiedName = stripQuotes(decorator.arguments[0]?.trim());
1286
+ const hasDefaultValue = field.initializer !== null;
1287
+ const parsedOptions = this.parseFieldDecoratorOptions(decorator.arguments[1]);
1288
+ const meta = {};
1289
+ const META_KEYS = [
1290
+ "validate",
1291
+ "nullable",
1292
+ "unique",
1293
+ "description",
1294
+ "default",
1295
+ "indexed",
1296
+ "idType"
1297
+ ];
1298
+ if (parsedOptions) {
1299
+ for (const key of META_KEYS) if (parsedOptions[key] !== void 0) meta[key] = parsedOptions[key];
1300
+ }
1301
+ return {
1302
+ type: "crossPackageRef",
1303
+ related: qualifiedName || void 0,
1304
+ required: parsedOptions?.required !== void 0 ? Boolean(parsedOptions.required) : !field.optional && !hasDefaultValue,
1305
+ defaultValue: parsedOptions?.default !== void 0 ? parsedOptions.default : void 0,
1306
+ ...Object.keys(meta).length > 0 ? { _meta: meta } : {},
1307
+ source: "decorator"
1308
+ };
1309
+ }
1310
+ if (decorator.name === "oneToMany") {
1311
+ const relatedClass = stripQuotes(decorator.arguments[0]?.trim());
1312
+ const parsedOptions = this.parseFieldDecoratorOptions(decorator.arguments[1]);
1313
+ const meta = {};
1314
+ if (parsedOptions?.foreignKey !== void 0) meta.foreignKey = parsedOptions.foreignKey;
1315
+ return {
1316
+ type: "oneToMany",
1317
+ related: relatedClass || void 0,
1318
+ required: false,
1319
+ ...Object.keys(meta).length > 0 ? { _meta: meta } : {},
1320
+ source: "decorator"
1321
+ };
1322
+ }
1323
+ if (decorator.name === "manyToMany") {
1324
+ const relatedClass = stripQuotes(decorator.arguments[0]?.trim());
1325
+ const parsedOptions = this.parseFieldDecoratorOptions(decorator.arguments[1]);
1326
+ const meta = {};
1327
+ if (parsedOptions?.through !== void 0) meta.through = parsedOptions.through;
1328
+ if (parsedOptions?.sourceKey !== void 0) meta.sourceKey = parsedOptions.sourceKey;
1329
+ if (parsedOptions?.targetKey !== void 0) meta.targetKey = parsedOptions.targetKey;
1330
+ return {
1331
+ type: "manyToMany",
1332
+ related: relatedClass || void 0,
1333
+ required: false,
1334
+ ...Object.keys(meta).length > 0 ? { _meta: meta } : {},
1335
+ source: "decorator"
1336
+ };
1337
+ }
1338
+ return null;
1339
+ }
1340
+ extractFieldDecoratorOptions(field) {
1341
+ const options = {};
1342
+ for (const decorator of field.decorators) {
1343
+ const reportMetadata = this.parseReportFieldDecorator(decorator, field);
1344
+ if (reportMetadata) {
1345
+ options.__report = reportMetadata;
1346
+ continue;
1347
+ }
1348
+ if (decorator.name !== "field") continue;
1349
+ const parsed = this.parseFieldDecoratorOptions(decorator.arguments[0]);
1350
+ if (parsed) Object.assign(options, parsed);
1351
+ }
1352
+ return options;
1353
+ }
1354
+ parseReportFieldDecorator(decorator, field) {
1355
+ if (decorator.name === "groupBy") return {
1356
+ kind: "group",
1357
+ sourceColumn: stripQuotes(decorator.arguments[0]?.trim()) ?? field.name
1358
+ };
1359
+ if ((/* @__PURE__ */ new Set([
1360
+ "minute",
1361
+ "hour",
1362
+ "day",
1363
+ "week",
1364
+ "month",
1365
+ "quarter",
1366
+ "year"
1367
+ ])).has(decorator.name)) {
1368
+ const sourceColumn = stripQuotes(decorator.arguments[0]?.trim());
1369
+ if (!sourceColumn) return null;
1370
+ return {
1371
+ kind: "bucket",
1372
+ unit: decorator.name,
1373
+ sourceColumn
1374
+ };
1375
+ }
1376
+ if (decorator.name === "aggregate") {
1377
+ const parsed = this.parseFieldDecoratorOptions(decorator.arguments[0]);
1378
+ if (!parsed?.fn || typeof parsed.fn !== "string") return null;
1379
+ return {
1380
+ kind: "aggregate",
1381
+ fn: parsed.fn,
1382
+ ...typeof parsed.column === "string" ? { column: parsed.column } : {},
1383
+ ...typeof parsed.distinct === "boolean" ? { distinct: parsed.distinct } : {}
1384
+ };
1385
+ }
1386
+ if ((/* @__PURE__ */ new Set([
1387
+ "sum",
1388
+ "avg",
1389
+ "min",
1390
+ "max"
1391
+ ])).has(decorator.name)) {
1392
+ const column = stripQuotes(decorator.arguments[0]?.trim());
1393
+ const parsedOptions = this.parseFieldDecoratorOptions(decorator.arguments[1]);
1394
+ return {
1395
+ kind: "aggregate",
1396
+ fn: decorator.name,
1397
+ ...column ? { column } : {},
1398
+ ...typeof parsedOptions?.distinct === "boolean" ? { distinct: parsedOptions.distinct } : {}
1399
+ };
1400
+ }
1401
+ if (decorator.name === "count") {
1402
+ const firstArg = decorator.arguments[0]?.trim();
1403
+ const firstOptions = this.parseFieldDecoratorOptions(firstArg);
1404
+ const secondOptions = this.parseFieldDecoratorOptions(decorator.arguments[1]);
1405
+ const column = firstOptions ? void 0 : stripQuotes(firstArg);
1406
+ const options = firstOptions ?? secondOptions;
1407
+ return {
1408
+ kind: "aggregate",
1409
+ fn: "count",
1410
+ ...column ? { column } : {},
1411
+ ...typeof options?.distinct === "boolean" ? { distinct: options.distinct } : {}
1412
+ };
1413
+ }
1414
+ return null;
1415
+ }
1416
+ parseFieldDecoratorOptions(rawArgument) {
1417
+ if (!rawArgument) return null;
1418
+ const parsed = parseLiteralInitializer(rawArgument);
1419
+ if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") return null;
1420
+ return parsed;
1421
+ }
1422
+ normalizeFieldType(value) {
1423
+ switch (value) {
1424
+ case "text":
1425
+ case "decimal":
1426
+ case "boolean":
1427
+ case "integer":
1428
+ case "datetime":
1429
+ case "json":
1430
+ case "foreignKey":
1431
+ case "crossPackageRef":
1432
+ case "oneToMany":
1433
+ case "manyToMany":
1434
+ case "meta": return value;
1435
+ default: return;
1436
+ }
1437
+ }
1438
+ /**
1439
+ * Infer type from TypeScript type annotation
1440
+ */
1441
+ inferFromAnnotation(field) {
1442
+ const type = field.typeAnnotation;
1443
+ const hasDefaultValue = field.initializer !== null;
1444
+ const isRequired = !field.optional && !hasDefaultValue;
1445
+ if (type?.startsWith("Meta<") && type.endsWith(">")) {
1446
+ const innerType = type.slice(5, -1);
1447
+ const underlyingInference = this.inferFromAnnotation({
1448
+ ...field,
1449
+ typeAnnotation: innerType
1450
+ });
1451
+ return {
1452
+ type: "meta",
1453
+ required: isRequired,
1454
+ defaultValue: underlyingInference.defaultValue,
1455
+ source: "annotation",
1456
+ underlyingType: underlyingInference.type
1457
+ };
1458
+ }
1459
+ if (type === "string") return {
1460
+ type: "text",
1461
+ required: isRequired,
1462
+ defaultValue: this.parseDefaultValue(field.initializer, "string"),
1463
+ source: "annotation"
1464
+ };
1465
+ if (type === "number") return {
1466
+ type: field.hasDecimalPoint ? "decimal" : "integer",
1467
+ required: isRequired,
1468
+ defaultValue: field.numericValue ?? void 0,
1469
+ source: "heuristic"
1470
+ };
1471
+ if (type === "boolean") return {
1472
+ type: "boolean",
1473
+ required: isRequired,
1474
+ defaultValue: this.parseDefaultValue(field.initializer, "boolean"),
1475
+ source: "annotation"
1476
+ };
1477
+ if (type === "Date") return {
1478
+ type: "datetime",
1479
+ required: isRequired,
1480
+ source: "annotation"
1481
+ };
1482
+ if (type?.endsWith("[]")) return {
1483
+ type: "json",
1484
+ required: isRequired,
1485
+ defaultValue: [],
1486
+ source: "annotation"
1487
+ };
1488
+ if (type?.startsWith("Record<") || type === "object") return {
1489
+ type: "json",
1490
+ required: isRequired,
1491
+ defaultValue: {},
1492
+ source: "annotation"
1493
+ };
1494
+ if (type?.includes(" | null") || type?.includes("null | ") || type?.includes(" | undefined") || type?.includes("undefined | ")) {
1495
+ const baseType = type.replace(/\s*\|\s*null/g, "").replace(/\s*\|\s*undefined/g, "").replace(/\bnull\s*\|\s*/g, "").replace(/\bundefined\s*\|\s*/g, "").trim();
1496
+ return this.inferFromAnnotation({
1497
+ ...field,
1498
+ typeAnnotation: baseType,
1499
+ optional: true
1500
+ });
1501
+ }
1502
+ if (type && /^'[^']*'(\s*\|\s*'[^']*')+$/.test(type)) return {
1503
+ type: "text",
1504
+ required: isRequired,
1505
+ defaultValue: this.parseDefaultValue(field.initializer, "string"),
1506
+ source: "annotation"
1507
+ };
1508
+ if (type && /^-?\d+(\s*\|\s*-?\d+)+$/.test(type)) return {
1509
+ type: "integer",
1510
+ required: isRequired,
1511
+ defaultValue: field.numericValue ?? void 0,
1512
+ source: "annotation"
1513
+ };
1514
+ if (type && !type.includes(" ") && !type.includes("<") && this.typeAliases[type] && (this._aliasDepth ?? 0) < 5) {
1515
+ const resolved = this.typeAliases[type];
1516
+ this._aliasDepth = (this._aliasDepth ?? 0) + 1;
1517
+ try {
1518
+ return this.inferFromAnnotation({
1519
+ ...field,
1520
+ typeAnnotation: resolved
1521
+ });
1522
+ } finally {
1523
+ this._aliasDepth = (this._aliasDepth ?? 0) - 1;
1524
+ }
1525
+ }
1526
+ if (field.initializer?.match(/^(['"]).*\1$/)) return {
1527
+ type: "text",
1528
+ required: isRequired,
1529
+ defaultValue: this.parseDefaultValue(field.initializer, "string"),
1530
+ source: "heuristic"
1531
+ };
1532
+ return {
1533
+ type: "json",
1534
+ required: isRequired,
1535
+ source: "default"
1536
+ };
1537
+ }
1538
+ /**
1539
+ * Parse default value from initializer string
1540
+ */
1541
+ parseDefaultValue(initializer, expectedType) {
1542
+ if (!initializer) return void 0;
1543
+ switch (expectedType) {
1544
+ case "string": {
1545
+ const stringMatch = initializer.match(/^(['"`])(.*)\1$/s);
1546
+ if (stringMatch) return stringMatch[2];
1547
+ break;
1548
+ }
1549
+ case "boolean":
1550
+ if (initializer === "true") return true;
1551
+ if (initializer === "false") return false;
1552
+ break;
1553
+ case "number": {
1554
+ const num = parseFloat(initializer);
1555
+ if (!Number.isNaN(num)) return num;
1556
+ break;
1557
+ }
1558
+ }
1559
+ }
1560
+ /**
1561
+ * Convert a raw method definition to a manifest `MethodDefinition`.
1562
+ *
1563
+ * Returns `null` for `private` or `protected` methods, which are excluded
1564
+ * from the manifest. Parameters are mapped to the manifest parameter shape
1565
+ * and default values are parsed via `parseDefaultValue`.
1566
+ *
1567
+ * @param method - Raw method definition from a scanned class.
1568
+ * @returns A manifest-compatible `MethodDefinition`, or `null` if the method
1569
+ * should be excluded.
1570
+ */
1571
+ convertMethod(method) {
1572
+ if (method.accessibility !== "public") return null;
1573
+ return {
1574
+ name: method.name,
1575
+ async: method.async,
1576
+ parameters: method.parameters.map((p) => ({
1577
+ name: p.name,
1578
+ type: p.type || "any",
1579
+ optional: p.optional,
1580
+ default: p.defaultValue ? this.parseDefaultValue(p.defaultValue, "string") : void 0
1581
+ })),
1582
+ returnType: method.returnType || "any",
1583
+ description: method.description || void 0,
1584
+ isStatic: method.isStatic,
1585
+ isPublic: true
1586
+ };
1587
+ }
1588
+ /**
1589
+ * Simple pluralization for collection names.
1590
+ *
1591
+ * This produces the manifest's `collection` label only; the authoritative DDL
1592
+ * table name is derived independently by core (`classnameToTablename` →
1593
+ * the `pluralize` library), so this needs to stay self-consistent rather than
1594
+ * cover every irregular plural. Note the `y → ies` rule fires only after a
1595
+ * consonant, so vowel+y words pluralise correctly (`Day` → `days`, not
1596
+ * `daies`).
1597
+ */
1598
+ pluralize(name) {
1599
+ const lower = name.toLowerCase();
1600
+ if (/[^aeiou]y$/.test(lower)) return `${lower.slice(0, -1)}ies`;
1601
+ if (lower.endsWith("s") || lower.endsWith("x") || lower.endsWith("z")) return `${lower}es`;
1602
+ if (lower.endsWith("ch") || lower.endsWith("sh")) return `${lower}es`;
1603
+ return `${lower}s`;
1604
+ }
1605
+ };
1606
+ //#endregion
1607
+ //#region src/scanner.ts
1608
+ var DEFAULT_INCLUDE = ["**/*.ts", "**/*.tsx"];
1609
+ var DEFAULT_EXCLUDE = [
1610
+ "**/node_modules/**",
1611
+ "**/dist/**",
1612
+ "**/build/**",
1613
+ "**/*.d.ts",
1614
+ "**/*.test.ts",
1615
+ "**/*.spec.ts",
1616
+ "**/__tests__/**"
1617
+ ];
1618
+ var OxcScanner = class {
1619
+ options;
1620
+ resolver;
1621
+ scanResults = null;
1622
+ /**
1623
+ * Create a new `OxcScanner` with the given options.
1624
+ *
1625
+ * All options are optional. By default the scanner targets every `.ts` and
1626
+ * `.tsx` file under `process.cwd()`, excluding `node_modules`, `dist`,
1627
+ * `build`, declaration files, and test files.
1628
+ *
1629
+ * @param options - Scanner configuration. See {@link OxcScannerOptions}.
1630
+ */
1631
+ constructor(options = {}) {
1632
+ this.options = {
1633
+ include: options.include || DEFAULT_INCLUDE,
1634
+ exclude: options.exclude || DEFAULT_EXCLUDE,
1635
+ cwd: options.cwd || process.cwd(),
1636
+ tsconfig: options.tsconfig || "",
1637
+ followImports: options.followImports ?? false,
1638
+ baseClasses: options.baseClasses || [],
1639
+ includePrivateMethods: options.includePrivateMethods ?? false,
1640
+ includeStaticMethods: options.includeStaticMethods ?? true,
1641
+ externalManifests: options.externalManifests || /* @__PURE__ */ new Map()
1642
+ };
1643
+ this.resolver = new InheritanceResolver({
1644
+ baseClasses: this.options.baseClasses,
1645
+ externalManifests: this.options.externalManifests
1646
+ });
1647
+ }
1648
+ /**
1649
+ * Phase 1 — Discover and parse TypeScript files using OXC.
1650
+ *
1651
+ * Uses `fast-glob` to enumerate matching files and then parses them in
1652
+ * parallel with OXC (Rust). The raw class definitions are registered with
1653
+ * the internal {@link InheritanceResolver} for use in the subsequent
1654
+ * {@link resolve} call.
1655
+ *
1656
+ * @returns A {@link ScanResults} object containing all classes found, any
1657
+ * parse errors, accumulated type aliases, SMRT import metadata, and
1658
+ * aggregate timing information.
1659
+ *
1660
+ * @example
1661
+ * ```typescript
1662
+ * const scanner = new OxcScanner({ cwd: '/project' });
1663
+ * const results = await scanner.scan();
1664
+ * console.log(`Parsed ${results.fileCount} files in ${results.totalParseTimeMs.toFixed(1)}ms`);
1665
+ * ```
1666
+ */
1667
+ async scan() {
1668
+ const startTime = performance.now();
1669
+ const files = await this.discoverFiles();
1670
+ const fileResults = await Promise.all(files.map((filePath) => this.parseFileWithTiming(filePath)));
1671
+ const results = {
1672
+ files: fileResults,
1673
+ classes: [],
1674
+ errors: [],
1675
+ totalParseTimeMs: performance.now() - startTime,
1676
+ fileCount: files.length,
1677
+ typeAliases: {}
1678
+ };
1679
+ for (const file of fileResults) {
1680
+ for (const classDef of file.classes) results.classes.push(classDef);
1681
+ for (const error of file.errors) results.errors.push(error);
1682
+ Object.assign(results.typeAliases, file.typeAliases);
1683
+ }
1684
+ this.resolver.addClasses(results.classes);
1685
+ this.scanResults = results;
1686
+ return results;
1687
+ }
1688
+ /**
1689
+ * Phase 2 — Resolve inheritance chains for all scanned classes.
1690
+ *
1691
+ * Must be called after {@link scan}. Walks each class's extends chain,
1692
+ * detects STI hierarchies, merges ancestor fields for STI subclasses, and
1693
+ * marks framework base classes.
1694
+ *
1695
+ * @returns An array of {@link ResolvedClassDefinition} objects — one for
1696
+ * every class that either carries `@smrt()` or extends a framework base
1697
+ * class (`SmrtObject`, `SmrtClass`, `SmrtCollection`).
1698
+ *
1699
+ * @throws {Error} If called before {@link scan}.
1700
+ *
1701
+ * @see {@link scanAndResolve} to run both phases in one call.
1702
+ */
1703
+ resolve() {
1704
+ if (!this.scanResults) throw new Error("Must call scan() before resolve()");
1705
+ return this.resolver.resolveAll();
1706
+ }
1707
+ /**
1708
+ * Run both scan phases in a single call.
1709
+ *
1710
+ * Equivalent to calling `await scanner.scan()` followed by
1711
+ * `scanner.resolve()`. This is the most common entry point for callers
1712
+ * that want the fully-resolved manifest-ready data in one step.
1713
+ *
1714
+ * @returns An object with:
1715
+ * - `results` — raw {@link ScanResults} from Phase 1.
1716
+ * - `resolved` — array of {@link ResolvedClassDefinition} from Phase 2.
1717
+ *
1718
+ * @example
1719
+ * ```typescript
1720
+ * const scanner = new OxcScanner({ cwd: '/project/src' });
1721
+ * const { results, resolved } = await scanner.scanAndResolve();
1722
+ * // resolved is ready to pass to ManifestAdapter.toManifest()
1723
+ * ```
1724
+ *
1725
+ * @see {@link ManifestAdapter} to convert `resolved` into a manifest JSON.
1726
+ */
1727
+ async scanAndResolve() {
1728
+ return {
1729
+ results: await this.scan(),
1730
+ resolved: this.resolve()
1731
+ };
1732
+ }
1733
+ /**
1734
+ * Register an external package manifest for cross-package base class resolution.
1735
+ *
1736
+ * When a project class extends a class defined in an installed SMRT package,
1737
+ * the resolver needs access to that package's class definitions to walk the
1738
+ * full inheritance chain. Call this method with each external package's
1739
+ * {@link ExternalManifest} before calling {@link scan} or {@link resolve}.
1740
+ *
1741
+ * @param manifest - The external manifest to register, including `packageName`,
1742
+ * `packageVersion`, and a `classes` map keyed by class name.
1743
+ *
1744
+ * @see {@link ExternalManifest}
1745
+ */
1746
+ addExternalManifest(manifest) {
1747
+ this.resolver.addExternalManifest(manifest);
1748
+ }
1749
+ /**
1750
+ * Scan all discovered files for @happyvertical/smrt-* imports.
1751
+ * Returns a map of package name → Set of imported class names.
1752
+ *
1753
+ * Used for tree-shaking: only external objects that are actually imported
1754
+ * in the project's source files will be included in the manifest.
1755
+ *
1756
+ * Must be called after scan() or as part of scanAndResolve().
1757
+ *
1758
+ * @example
1759
+ * ```typescript
1760
+ * const scanner = new OxcScanner({ cwd: process.cwd() });
1761
+ * await scanner.scan();
1762
+ * const imports = scanner.scanSmrtImports();
1763
+ * // Map { '@happyvertical/smrt-profiles' => Set { 'Person', 'Organization' } }
1764
+ * ```
1765
+ */
1766
+ scanSmrtImports() {
1767
+ if (!this.scanResults) throw new Error("Must call scan() before scanSmrtImports()");
1768
+ const merged = /* @__PURE__ */ new Map();
1769
+ for (const file of this.scanResults.files) if (file.smrtImports) for (const [pkg, classes] of file.smrtImports) {
1770
+ if (!merged.has(pkg)) merged.set(pkg, /* @__PURE__ */ new Set());
1771
+ const mergedSet = merged.get(pkg);
1772
+ for (const cls of classes) mergedSet.add(cls);
1773
+ }
1774
+ return merged;
1775
+ }
1776
+ /**
1777
+ * Return aggregate statistics about the last scan.
1778
+ *
1779
+ * Can be called after {@link scan} has completed. Returns counts useful for
1780
+ * diagnostics and the `--stats` CLI flag.
1781
+ *
1782
+ * @returns An object with:
1783
+ * - `totalClasses` — total class declarations seen (including non-SMRT).
1784
+ * - `smrtClasses` — classes with `@smrt()` decorator.
1785
+ * - `stiClasses` — SMRT classes participating in an STI hierarchy.
1786
+ * - `maxInheritanceDepth` — length of the deepest inheritance chain.
1787
+ * - `fileCount` — number of files scanned.
1788
+ * - `parseTimeMs` — total wall-clock parse time in milliseconds.
1789
+ */
1790
+ getStats() {
1791
+ return {
1792
+ ...this.resolver.getStats(),
1793
+ fileCount: this.scanResults?.fileCount || 0,
1794
+ parseTimeMs: this.scanResults?.totalParseTimeMs || 0
1795
+ };
1796
+ }
1797
+ /**
1798
+ * Discover files to scan using fast-glob
1799
+ */
1800
+ async discoverFiles() {
1801
+ const patterns = this.options.include;
1802
+ return await fg(patterns, {
1803
+ cwd: this.options.cwd,
1804
+ ignore: this.options.exclude,
1805
+ absolute: true,
1806
+ onlyFiles: true
1807
+ });
1808
+ }
1809
+ /**
1810
+ * Parse a single file with timing
1811
+ */
1812
+ async parseFileWithTiming(filePath) {
1813
+ return parseFile(filePath);
1814
+ }
1815
+ };
1816
+ //#endregion
1817
+ export { parseSource as a, parseFile as i, ManifestAdapter as n, InheritanceResolver as o, extractSmrtImports as r, OxcScanner as t };
1818
+
1819
+ //# sourceMappingURL=scanner-Caea1XyJ.js.map