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