entkapp 4.1.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.
Files changed (46) hide show
  1. package/.scaffold-ignore +22 -0
  2. package/LICENSE +211 -0
  3. package/NOTICE +13 -0
  4. package/README.md +33 -0
  5. package/bin/cli.js +174 -0
  6. package/entkapp/config.json +42 -0
  7. package/entkapp/plugins/README.md +19 -0
  8. package/index.js +2254 -0
  9. package/logo.png +0 -0
  10. package/package.json +96 -0
  11. package/src/EngineContext.js +185 -0
  12. package/src/api/HeadlessAPI.js +376 -0
  13. package/src/api/PluginSDK.js +299 -0
  14. package/src/ast/ASTAnalyzer.js +492 -0
  15. package/src/ast/BarrelParser.js +221 -0
  16. package/src/ast/DeadCodeDetector.js +73 -0
  17. package/src/ast/MagicDetector.js +203 -0
  18. package/src/ast/OxcAnalyzer.js +337 -0
  19. package/src/ast/SecretScanner.js +304 -0
  20. package/src/healing/GitSandbox.js +82 -0
  21. package/src/healing/SelfHealer.js +52 -0
  22. package/src/index.js +723 -0
  23. package/src/performance/GraphCache.js +87 -0
  24. package/src/performance/SupplyChainGuard.js +92 -0
  25. package/src/performance/WorkerPool.js +109 -0
  26. package/src/plugins/BasePlugin.js +71 -0
  27. package/src/plugins/KnipAdapter.js +106 -0
  28. package/src/plugins/PluginRegistry.js +197 -0
  29. package/src/plugins/ecosystems/BackendServices.js +61 -0
  30. package/src/plugins/ecosystems/GenericPlugins.js +64 -0
  31. package/src/plugins/ecosystems/ModernFrameworks.js +159 -0
  32. package/src/plugins/ecosystems/MorePlugins.js +184 -0
  33. package/src/plugins/ecosystems/NextJsPlugin.js +33 -0
  34. package/src/plugins/ecosystems/PluginLoader.js +20 -0
  35. package/src/plugins/ecosystems/TypeScriptPlugin.js +56 -0
  36. package/src/refractor/ImpactAnalyzer.js +92 -0
  37. package/src/refractor/SourceRewriter.js +86 -0
  38. package/src/refractor/TransactionManager.js +138 -0
  39. package/src/refractor/TypeIntegrity.js +75 -0
  40. package/src/resolution/CircularDetector.js +91 -0
  41. package/src/resolution/ConfigLoader.js +87 -0
  42. package/src/resolution/DepencyResolver.js +133 -0
  43. package/src/resolution/DependencyProfiler.js +268 -0
  44. package/src/resolution/PathMapper.js +125 -0
  45. package/src/resolution/WorkSpaceGraph.js +474 -0
  46. package/tsconfig.json +26 -0
@@ -0,0 +1,492 @@
1
+ import ts from 'typescript';
2
+ import path from 'path';
3
+ import fs from 'fs';
4
+
5
+ export class ASTAnalyzer {
6
+ constructor(context) {
7
+ this.context = context;
8
+ this.scopeStack = [];
9
+ }
10
+
11
+ /**
12
+ * Returns the TypeScript ScriptKind for a given file path.
13
+ * Exposed as a public method so WorkerTaskRunner can call it directly.
14
+ */
15
+ getScriptKind(filePath) {
16
+ if (filePath.endsWith('.tsx') || filePath.endsWith('.jsx')) return ts.ScriptKind.TSX;
17
+ if (filePath.endsWith('.js') || filePath.endsWith('.mjs') || filePath.endsWith('.cjs')) return ts.ScriptKind.JS;
18
+ return ts.ScriptKind.TS;
19
+ }
20
+
21
+ parseFile(filePath, content, fileNode) {
22
+ if (this.context.verbose) {
23
+ console.log(`[AST] Parsing: ${filePath}`);
24
+ }
25
+
26
+ const sourceFile = ts.createSourceFile(
27
+ filePath,
28
+ content,
29
+ ts.ScriptTarget.Latest,
30
+ true,
31
+ this.getScriptKind(filePath)
32
+ );
33
+
34
+ this.currentScope = { symbols: new Map(), parent: null };
35
+ this.scopeStack.push(this.currentScope);
36
+
37
+ this.extractTopLevelJSDocSuppreessions(sourceFile, fileNode);
38
+ this.walkAST(sourceFile, fileNode, sourceFile);
39
+
40
+ this.scopeStack.pop(); // Pop the global scope
41
+ this.currentScope = null;
42
+ }
43
+
44
+ /**
45
+ * Alias for walkAST used by WorkerTaskRunner (legacy API compatibility).
46
+ */
47
+ walkNode(node, sourceFile, fileNode) {
48
+ return this.walkAST(node, fileNode, sourceFile);
49
+ }
50
+
51
+ pushScope() {
52
+ const newScope = { symbols: new Map(), parent: this.currentScope };
53
+ this.scopeStack.push(newScope);
54
+ this.currentScope = newScope;
55
+ }
56
+
57
+ popScope() {
58
+ if (this.scopeStack.length > 1) {
59
+ this.scopeStack.pop();
60
+ this.currentScope = this.scopeStack[this.scopeStack.length - 1];
61
+ }
62
+ }
63
+
64
+ addDeclaredSymbol(name, node, sourceFile) {
65
+ if (this.currentScope) {
66
+ const loc = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
67
+ this.currentScope.symbols.set(name, { node, line: loc.line + 1, column: loc.character + 1 });
68
+ }
69
+ }
70
+
71
+ resolveSymbol(name) {
72
+ for (let i = this.scopeStack.length - 1; i >= 0; i--) {
73
+ const scope = this.scopeStack[i];
74
+ if (scope.symbols.has(name)) {
75
+ return scope.symbols.get(name);
76
+ }
77
+ }
78
+ return null;
79
+ }
80
+
81
+ walkAST(node, fileNode, sourceFile) {
82
+ // Handle scope entry for blocks, functions, classes, etc.
83
+ const isScopeNode = ts.isBlock(node) || ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node) || ts.isModuleDeclaration(node);
84
+ if (isScopeNode) {
85
+ this.pushScope();
86
+ }
87
+
88
+ switch (node.kind) {
89
+ case ts.SyntaxKind.ImportDeclaration:
90
+ this.handleImportDeclaration(node, fileNode, sourceFile);
91
+ break;
92
+ case ts.SyntaxKind.ExportDeclaration:
93
+ this.handleExportDeclaration(node, fileNode, sourceFile);
94
+ break;
95
+ case ts.SyntaxKind.ExportAssignment:
96
+ fileNode.internalExports.set('default', { type: 'default', start: node.getStart(sourceFile), end: node.getEnd() });
97
+ break;
98
+ case ts.SyntaxKind.VariableStatement:
99
+ this.handleVariableStatement(node, fileNode, sourceFile);
100
+ break;
101
+ case ts.SyntaxKind.FunctionDeclaration:
102
+ case ts.SyntaxKind.ClassDeclaration:
103
+ case ts.SyntaxKind.InterfaceDeclaration:
104
+ case ts.SyntaxKind.TypeAliasDeclaration:
105
+ case ts.SyntaxKind.EnumDeclaration:
106
+ case ts.SyntaxKind.ModuleDeclaration:
107
+ this.handleNamedDeclaration(node, fileNode, sourceFile);
108
+ break;
109
+ case ts.SyntaxKind.Identifier:
110
+ fileNode.instantiatedIdentifiers.add(node.text);
111
+ break;
112
+ case ts.SyntaxKind.StringLiteral:
113
+ case ts.SyntaxKind.NoSubstitutionTemplateLiteral:
114
+ fileNode.rawStringReferences.add(node.text);
115
+ break;
116
+ case ts.SyntaxKind.PropertyAccessExpression:
117
+ fileNode.propertyAccessChains.add(node.getText(sourceFile));
118
+ break;
119
+ case ts.SyntaxKind.CallExpression:
120
+ this.handleCallExpression(node, fileNode, sourceFile);
121
+ break;
122
+ case ts.SyntaxKind.JsxElement:
123
+ case ts.SyntaxKind.JsxSelfClosingElement:
124
+ this.handleJsxElement(node, fileNode, sourceFile);
125
+ break;
126
+ case ts.SyntaxKind.Decorator:
127
+ this.handleDecorator(node, fileNode, sourceFile);
128
+ break;
129
+ }
130
+
131
+ ts.forEachChild(node, child => this.walkAST(child, fileNode, sourceFile));
132
+
133
+ if (isScopeNode) {
134
+ this.popScope();
135
+ }
136
+ }
137
+
138
+ handleImportDeclaration(node, fileNode, sourceFile) {
139
+ if (node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
140
+ const specifier = node.moduleSpecifier.text;
141
+ fileNode.explicitImports.add(specifier);
142
+
143
+ // Track external package usage for dependency analysis
144
+ if (!specifier.startsWith('.') && !specifier.startsWith('/')) {
145
+ fileNode.externalPackageUsage.add(this._extractPackageName(specifier));
146
+ }
147
+
148
+ // 🚨 TARGET BUG 1 & 2: Audit package requirements for unlisted or root dependencies
149
+ if (this.context.workspaceGraph && typeof this.context.workspaceGraph.auditImportSpecifier === 'function') {
150
+ this.context.workspaceGraph.auditImportSpecifier(specifier, sourceFile.fileName);
151
+ }
152
+
153
+ if (node.importClause) {
154
+ // Initialize global tracking structures on the engine context if missing
155
+ if (!this.context.importUsageRegistry) this.context.importUsageRegistry = new Set();
156
+
157
+ // Cross-file import resolver function
158
+ const resolveAbsoluteTargetFile = (spec) => {
159
+ // If importing a local workspace monorepo module (e.g. '@monorepo/shared')
160
+ if (this.context.workspaceGraph && typeof this.context.workspaceGraph.isLocalWorkspaceSpecifier === 'function') {
161
+ if (this.context.workspaceGraph.isLocalWorkspaceSpecifier(spec)) {
162
+ const match = this.context.workspaceGraph.getWorkspacePackageMatch(spec);
163
+ // Grab the first entry point file calculated from its local tsconfig/package.json
164
+ if (match && match.entryPoints && match.entryPoints.length > 0) {
165
+ return match.entryPoints[0];
166
+ }
167
+ }
168
+ }
169
+ // Fallback to relative file layout calculations
170
+ if (spec.startsWith('.')) {
171
+ let resolved = path.resolve(path.dirname(sourceFile.fileName), spec);
172
+ // Append standard extensions if missing from import string
173
+ if (!path.extname(resolved)) {
174
+ if (fs.existsSync(resolved + '.ts')) resolved += '.ts';
175
+ else if (fs.existsSync(resolved + '.tsx')) resolved += '.tsx';
176
+ else if (fs.existsSync(resolved + '.js')) resolved += '.js';
177
+ }
178
+ return resolved;
179
+ }
180
+ return null;
181
+ };
182
+
183
+ if (node.importClause.name) {
184
+ fileNode.importedSymbols.add(`${specifier}:default`);
185
+
186
+ // Trace default consumption targets
187
+ const targetFile = resolveAbsoluteTargetFile(specifier);
188
+ if (targetFile) {
189
+ this.context.importUsageRegistry.add(`${targetFile}:default`);
190
+ }
191
+ }
192
+
193
+ if (node.importClause.namedBindings) {
194
+ if (ts.isNamespaceImport(node.importClause.namedBindings)) {
195
+ fileNode.importedSymbols.add(`${specifier}:*`);
196
+
197
+ const targetFile = resolveAbsoluteTargetFile(specifier);
198
+ if (targetFile) {
199
+ this.context.importUsageRegistry.add(`${targetFile}:*`);
200
+ }
201
+ } else if (ts.isNamedImports(node.importClause.namedBindings)) {
202
+ node.importClause.namedBindings.elements.forEach(element => {
203
+ const importedName = element.propertyName ? element.propertyName.text : element.name.text;
204
+ fileNode.importedSymbols.add(`${specifier}:${importedName}`);
205
+
206
+ // 🚨 TARGET BUG 3: Map the unique file:symbol consumption token
207
+ const targetFile = resolveAbsoluteTargetFile(specifier);
208
+ if (targetFile) {
209
+ this.context.importUsageRegistry.add(`${targetFile}:${importedName}`);
210
+ }
211
+ });
212
+ }
213
+ }
214
+ }
215
+ }
216
+ }
217
+
218
+ handleExportDeclaration(node, fileNode, sourceFile) {
219
+ const specifier = node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier) ? node.moduleSpecifier.text : null;
220
+
221
+ if (specifier) {
222
+ // Re-export from source: export * from './module' or export { x } from './module'
223
+ fileNode.explicitImports.add(specifier);
224
+
225
+ // Track external package usage from re-exports
226
+ if (!specifier.startsWith('.') && !specifier.startsWith('/')) {
227
+ fileNode.externalPackageUsage.add(this._extractPackageName(specifier));
228
+ }
229
+
230
+ if (!node.exportClause) {
231
+ // export * from './module'
232
+ fileNode.internalExports.set('*', { type: 're-export-all', source: specifier });
233
+ fileNode.importedSymbols.add(`${specifier}:*`);
234
+ } else if (ts.isNamespaceExport(node.exportClause)) {
235
+ // export * as name from './module'
236
+ const name = node.exportClause.name.text;
237
+ fileNode.internalExports.set(name, { type: 're-export-namespace', source: specifier, originalName: '*', start: node.getStart(sourceFile), end: node.getEnd() });
238
+ fileNode.importedSymbols.add(`${specifier}:*`);
239
+ } else if (ts.isNamedExports(node.exportClause)) {
240
+ // export { x, y as z } from './module'
241
+ node.exportClause.elements.forEach(element => {
242
+ const originalName = element.propertyName ? element.propertyName.text : element.name.text;
243
+ const exportedName = element.name.text;
244
+ fileNode.internalExports.set(exportedName, { type: 're-export', source: specifier, originalName, start: element.getStart(sourceFile), end: element.getEnd() });
245
+ fileNode.importedSymbols.add(`${specifier}:${originalName}`);
246
+ });
247
+ }
248
+ } else if (node.exportClause && ts.isNamedExports(node.exportClause)) {
249
+ // Local named exports: export { x, y as z }
250
+ node.exportClause.elements.forEach(element => {
251
+ const localName = element.propertyName ? element.propertyName.text : element.name.text;
252
+ const exportedName = element.name.text;
253
+ fileNode.internalExports.set(exportedName, { type: 'export', originalName: localName, start: element.getStart(sourceFile), end: element.getEnd() });
254
+ });
255
+ }
256
+ }
257
+
258
+ handleNamedDeclaration(node, fileNode, sourceFile) {
259
+ if (this.hasExportModifier(node)) {
260
+ const isDefault = node.modifiers?.some(m => m.kind === ts.SyntaxKind.DefaultKeyword);
261
+ const name = isDefault ? 'default' : (node.name?.text || 'anonymous');
262
+
263
+ // 🚨 TARGET BUG 3 (Part B): Extract exported symbol tokens into your global context map
264
+ if (!this.context.exportRegistry) this.context.exportRegistry = new Map();
265
+ const currentFile = sourceFile.fileName;
266
+ if (!this.context.exportRegistry.has(currentFile)) {
267
+ this.context.exportRegistry.set(currentFile, new Set());
268
+ }
269
+ if (name !== 'anonymous') {
270
+ this.context.exportRegistry.get(currentFile).add(name);
271
+ }
272
+
273
+ const exportInfo = {
274
+ type: ts.SyntaxKind[node.kind].toLowerCase().replace('declaration', ''),
275
+ start: node.getStart(sourceFile),
276
+ end: node.getEnd()
277
+ };
278
+
279
+ fileNode.internalExports.set(name, exportInfo);
280
+
281
+ if (ts.isEnumDeclaration(node)) {
282
+ exportInfo.members = node.members.map(m => ({
283
+ name: m.name.getText(sourceFile),
284
+ type: 'enumMember',
285
+ start: m.getStart(sourceFile),
286
+ end: m.getEnd()
287
+ }));
288
+ } else if (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node)) {
289
+ exportInfo.members = node.members
290
+ .filter(m => m.name)
291
+ .map(m => ({
292
+ name: m.name.getText(sourceFile),
293
+ type: ts.SyntaxKind[m.kind].toLowerCase(),
294
+ start: m.getStart(sourceFile),
295
+ end: m.getEnd()
296
+ }));
297
+ } else if (ts.isModuleDeclaration(node)) {
298
+ const members = [];
299
+ if (node.body && ts.isModuleBlock(node.body)) {
300
+ node.body.statements.forEach(stmt => {
301
+ if (this.hasExportModifier(stmt) && (ts.isVariableStatement(stmt) || ts.isFunctionDeclaration(stmt) || ts.isClassDeclaration(stmt))) {
302
+ if (ts.isVariableStatement(stmt)) {
303
+ stmt.declarationList.declarations.forEach(d => members.push({
304
+ name: d.name.getText(sourceFile),
305
+ type: 'variable',
306
+ start: d.getStart(sourceFile),
307
+ end: d.getEnd()
308
+ }));
309
+ } else if (stmt.name) {
310
+ members.push({
311
+ name: stmt.name.getText(sourceFile),
312
+ type: ts.SyntaxKind[stmt.kind].toLowerCase().replace('declaration', ''),
313
+ start: stmt.getStart(sourceFile),
314
+ end: stmt.getEnd()
315
+ });
316
+ }
317
+ }
318
+ });
319
+ }
320
+ exportInfo.members = members;
321
+ }
322
+
323
+ const loc = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
324
+ fileNode.symbolSourceLocations.set(name, { line: loc.line + 1, column: loc.character + 1 });
325
+ this.addDeclaredSymbol(name, node, sourceFile);
326
+ } else if (node.name && ts.isIdentifier(node.name)) {
327
+ this.addDeclaredSymbol(node.name.text, node, sourceFile);
328
+ }
329
+ }
330
+
331
+ handleVariableStatement(node, fileNode, sourceFile) {
332
+ if (this.hasExportModifier(node)) {
333
+ node.declarationList.declarations.forEach(decl => {
334
+ if (decl.name && ts.isIdentifier(decl.name)) {
335
+ const name = decl.name.text;
336
+ fileNode.internalExports.set(name, {
337
+ type: 'variable',
338
+ start: node.getStart(sourceFile),
339
+ end: node.getEnd()
340
+ });
341
+ const loc = sourceFile.getLineAndCharacterOfPosition(decl.getStart(sourceFile));
342
+ fileNode.symbolSourceLocations.set(name, { line: loc.line + 1, column: loc.character + 1 });
343
+ this.addDeclaredSymbol(name, decl, sourceFile);
344
+ } else if (decl.name && ts.isObjectBindingPattern(decl.name)) {
345
+ decl.name.elements.forEach(element => {
346
+ if(element.name && ts.isIdentifier(element.name)) {
347
+ const name = element.name.text;
348
+ fileNode.internalExports.set(name, {
349
+ type: 'variable',
350
+ start: node.getStart(sourceFile),
351
+ end: node.getEnd()
352
+ });
353
+ const loc = sourceFile.getLineAndCharacterOfPosition(element.getStart(sourceFile));
354
+ fileNode.symbolSourceLocations.set(name, { line: loc.line + 1, column: loc.character + 1 });
355
+ this.addDeclaredSymbol(name, element, sourceFile);
356
+ }
357
+ });
358
+ } else if (decl.name && ts.isArrayBindingPattern(decl.name)) {
359
+ decl.name.elements.forEach(element => {
360
+ if(ts.isBindingElement(element) && element.name && ts.isIdentifier(element.name)) {
361
+ const name = element.name.text;
362
+ fileNode.internalExports.set(name, {
363
+ type: 'variable',
364
+ start: node.getStart(sourceFile),
365
+ end: node.getEnd()
366
+ });
367
+ const loc = sourceFile.getLineAndCharacterOfPosition(element.getStart(sourceFile));
368
+ fileNode.symbolSourceLocations.set(name, { line: loc.line + 1, column: loc.character + 1 });
369
+ this.addDeclaredSymbol(name, element, sourceFile);
370
+ }
371
+ });
372
+ }
373
+ });
374
+ } else {
375
+ // Non-exported variable declarations also need to be added to scope
376
+ node.declarationList.declarations.forEach(decl => {
377
+ if (decl.name && ts.isIdentifier(decl.name)) {
378
+ this.addDeclaredSymbol(decl.name.text, decl, sourceFile);
379
+ } else if (decl.name && ts.isObjectBindingPattern(decl.name)) {
380
+ decl.name.elements.forEach(element => {
381
+ if (element.name && ts.isIdentifier(element.name)) {
382
+ this.addDeclaredSymbol(element.name.text, element, sourceFile);
383
+ }
384
+ });
385
+ } else if (decl.name && ts.isArrayBindingPattern(decl.name)) {
386
+ decl.name.elements.forEach(element => {
387
+ if (ts.isBindingElement(element) && element.name && ts.isIdentifier(element.name)) {
388
+ this.addDeclaredSymbol(element.name.text, element, sourceFile);
389
+ }
390
+ });
391
+ }
392
+ });
393
+ }
394
+ }
395
+
396
+ handleCallExpression(node, fileNode, sourceFile) {
397
+ // Dynamic import(): import('./module').then(...)
398
+ if (node.expression.kind === ts.SyntaxKind.ImportKeyword) {
399
+ const arg = node.arguments[0];
400
+ if (arg) {
401
+ if (ts.isStringLiteral(arg)) {
402
+ fileNode.explicitImports.add(arg.text);
403
+ fileNode.dynamicImports.add(arg.text);
404
+ // Track external package usage from dynamic imports
405
+ if (!arg.text.startsWith('.') && !arg.text.startsWith('/')) {
406
+ fileNode.externalPackageUsage.add(this._extractPackageName(arg.text));
407
+ }
408
+ } else {
409
+ // Dynamic import with a non-literal expression (e.g., variable or template literal).
410
+ if (fileNode.calculatedDynamicImports) {
411
+ fileNode.calculatedDynamicImports.push({ kind: ts.SyntaxKind[arg.kind], start: arg.getStart(sourceFile) });
412
+ }
413
+ }
414
+ }
415
+ } else if (ts.isIdentifier(node.expression) && node.expression.text === 'require') {
416
+ const arg = node.arguments[0];
417
+ if (arg && ts.isStringLiteral(arg)) {
418
+ fileNode.explicitImports.add(arg.text);
419
+ // Track external package usage from require() calls
420
+ if (!arg.text.startsWith('.') && !arg.text.startsWith('/')) {
421
+ fileNode.externalPackageUsage.add(this._extractPackageName(arg.text));
422
+ }
423
+ }
424
+ }
425
+ }
426
+
427
+ handleJsxElement(node, fileNode, sourceFile) {
428
+ const getElementName = (name) => {
429
+ if (ts.isIdentifier(name)) return name.text;
430
+ if (ts.isPropertyAccessExpression(name)) return name.name.text;
431
+ return 'unknown';
432
+ };
433
+
434
+ const tagName = getElementName(node.openingElement.tagName);
435
+ fileNode.jsxComponents.add(tagName);
436
+
437
+ node.openingElement.attributes.properties.forEach(attr => {
438
+ if (ts.isJsxAttribute(attr) && ts.isIdentifier(attr.name)) {
439
+ fileNode.jsxProps.add(`${tagName}:${attr.name.text}`);
440
+ }
441
+ });
442
+ }
443
+
444
+ handleDecorator(node, fileNode, sourceFile) {
445
+ const getDecoratorName = (expr) => {
446
+ if (ts.isIdentifier(expr)) return expr.text;
447
+ if (ts.isCallExpression(expr) && ts.isIdentifier(expr.expression)) return expr.expression.text;
448
+ if (ts.isCallExpression(expr) && ts.isPropertyAccessExpression(expr.expression)) return expr.expression.name.text;
449
+ return 'unknown';
450
+ };
451
+
452
+ const decoratorName = getDecoratorName(node.expression);
453
+ fileNode.decorators.add(decoratorName);
454
+
455
+ // Optionally, extract decorator arguments
456
+ if (ts.isCallExpression(node.expression)) {
457
+ node.expression.arguments.forEach(arg => {
458
+ // Further analysis of arguments can be done here if needed
459
+ });
460
+ }
461
+ }
462
+
463
+ hasExportModifier(node) {
464
+ return node.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
465
+ }
466
+
467
+ extractTopLevelJSDocSuppreessions(sourceFile, fileNode) {
468
+ const fullText = sourceFile.text;
469
+ const commentRegex = /\/\*\*?[\s\S]*?\*\/|\/\/.*/g;
470
+ let match;
471
+ while ((match = commentRegex.exec(fullText)) !== null) {
472
+ const suppressMatches = match[0].match(/@scaffold-suppress\s+([a-zA-Z0-9_\-*:]+)/g);
473
+ if (suppressMatches) {
474
+ suppressMatches.forEach(m => fileNode.localSuppressedRules.add(m.replace('@scaffold-suppress', '').trim()));
475
+ }
476
+ }
477
+ }
478
+
479
+ /**
480
+ * Extracts the root npm package name from an import specifier.
481
+ * Handles scoped packages (@scope/pkg) and subpath imports (pkg/utils, @scope/pkg/utils).
482
+ */
483
+ _extractPackageName(specifier) {
484
+ if (specifier.startsWith('@')) {
485
+ // Scoped package: @scope/name or @scope/name/subpath
486
+ const parts = specifier.split('/');
487
+ return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : specifier;
488
+ }
489
+ // Regular package: name or name/subpath
490
+ return specifier.split('/')[0];
491
+ }
492
+ }