pkg-scaffold 3.3.4 → 3.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ![Logo](./logo.png)
4
4
 
5
- > **The Ultimate Enterprise Codebase Janitor.** Faster than Knip with OXC integration, type-aware analysis, and self-healing capabilities. Fully standalone - solving what Knip cannot.
5
+ > **The Ultimate Enterprise Codebase Janitor.** Faster than Knip with OXC integration, type-aware analysis, and automated structural healing. Fully standalone - solving what Knip cannot.
6
6
 
7
7
  ![Version](https://img.shields.io/npm/v/pkg-scaffold) ![License](https://img.shields.io/badge/license-Apache--2.0-green.svg) ![Performance](https://img.shields.io/badge/performance-OXC--Inside-blueviolet.svg)
8
8
 
@@ -15,8 +15,7 @@
15
15
  * **šŸ’€ True Dead Code Detection:** Advanced graph-based reachability analysis to find truly dead files and unused exports, even deep within your codebase.
16
16
  * **šŸ”„ Circular Dependency Detection:** High-performance Tarjan-based algorithm to detect and report circular dependencies.
17
17
  * **šŸ›”ļø Supply Chain Guard:** Detects typosquatting and verifies integrity lockfile hashes.
18
- * **šŸ” Secret Detection:** Scans for hardcoded secrets (API keys, tokens) using AST and Regex.
19
- * **šŸ› ļø Self-Healing:** Not just reporting, but automatically fixing structural issues (removing dead files, pruning unused dependencies).
18
+ * **šŸ› ļø Automated Structural Healing:** Not just reporting, but automatically fixing structural issues (removing dead files, pruning unused dependencies) with git-based rollback protection.
20
19
  * **āš™ļø Flexible Configuration:** Supports `pkg-scaffold.json`, `pkg-scaffold.ts`, `scaffold.config.js`, and more.
21
20
 
22
21
  ## šŸ“¦ Installation
@@ -26,7 +25,7 @@ npm install -D pkg-scaffold
26
25
  # or
27
26
  pnpm add -D pkg-scaffold
28
27
  # or
29
- yarn add -D pkg-scaffold
28
+ pnpm add -D pkg-scaffold
30
29
  ```
31
30
 
32
31
  ## šŸš€ Usage
package/bin/cli.js CHANGED
@@ -28,7 +28,7 @@ async function bootstrap() {
28
28
  program
29
29
  .name('pkg-scaffold')
30
30
  .description(ansis.cyan('Enterprise-Grade AST Syntax Refactoring & Self-Healing Engine'))
31
- .version(packageJsonContent.version || '3.3.2');
31
+ .version(packageJsonContent.version || '3.3.5');
32
32
 
33
33
  program
34
34
  .option('-c, --cwd <path>', 'Specify the execution context root directory', process.cwd())
@@ -130,7 +130,7 @@ async function bootstrap() {
130
130
  }, timeoutMs);
131
131
  timeoutTimer.unref(); // Allow process to exit if work finishes
132
132
 
133
- console.log(ansis.bold.green(`\nšŸ“¦ pkg-scaffold v${packageJsonContent.version || '3.3.2'} Engine Activation`));
133
+ console.log(ansis.bold.green(`\nšŸ“¦ pkg-scaffold v${packageJsonContent.version || '3.3.5'} Engine Activation`));
134
134
  console.log(ansis.dim('------------------------------------------------------------'));
135
135
  console.log(`${ansis.bold('Target Workspace Root :')} ${ansis.blue(targetCwd)}`);
136
136
  console.log(`${ansis.bold('Refactoring Mode :')} ${options.fix ? ansis.yellow('Active Fixing & Self-Healing Enabled') : ansis.gray('Dry-Run Reporting Only')}`);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pkg-scaffold",
3
- "version": "3.3.4",
4
- "description": "The Ultimate Enterprise Codebase Janitor. Faster than Knip with OXC integration, type-aware analysis, and self-healing capabilities. Fully standalone - solving what Knip cannot.",
3
+ "version": "3.3.5",
4
+ "description": "The Ultimate Enterprise Codebase Janitor. Faster than Knip with OXC integration, type-aware analysis, and automated structural healing. Fully standalone - solving what Knip cannot.",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "bin": {
@@ -48,7 +48,8 @@
48
48
  "package.json",
49
49
  "packages",
50
50
  "scan",
51
- "self-healing",
51
+
52
+ "structural-healing",
52
53
  "setup",
53
54
  "type",
54
55
  "types",
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * ============================================================================
3
- * šŸ“¦ pkg-scaffold v3.3.0: Enterprise In-Memory Codebase State Manifest
3
+ * šŸ“¦ pkg-scaffold v3.3.5: Enterprise In-Memory Codebase State Manifest
4
4
  * ============================================================================
5
5
  * Implements a high-density, centralized graph database context for tracking
6
6
  * software engineering debt, dependencies, types, and vulnerabilities.
@@ -8,6 +8,7 @@
8
8
 
9
9
  import path from 'path';
10
10
  import fs from 'fs/promises';
11
+ import { DependencyProfiler } from './resolution/DependencyProfiler.js';
11
12
 
12
13
  /**
13
14
  * High-Fidelity Graph Element Node representing a single file asset boundary.
@@ -42,8 +43,7 @@ export class GraphNode {
42
43
  this.incomingEdges = new Set(); // Set of absolute filePaths depending on this component
43
44
  this.outgoingEdges = new Set(); // Set of absolute internal filePaths this component calls
44
45
 
45
- // Security & Compliance Anomaly Matrices
46
- this.securityThreats = [];
46
+ // Structural Syntax Boundaries
47
47
  this.calculatedDynamicImports = [];
48
48
  this.localSuppressedRules = new Set();
49
49
  this.externalPackageUsage = new Set(); // Tracked third-party package names
@@ -160,6 +160,9 @@ export class EngineContext {
160
160
  };
161
161
  this.usedExternalPackages = new Set(); // Global set of used npm packages
162
162
  this.manifestDependencies = new Map(); // Package.json path -> { dependencies, devDependencies, peerDependencies, optionalDependencies }
163
+
164
+ // DependencyProfiler instance for implicit invocation tracing
165
+ this._depProfiler = new DependencyProfiler(this);
163
166
  }
164
167
 
165
168
  /**
@@ -237,7 +240,7 @@ export class EngineContext {
237
240
  * Processes the entire active dependency map to compile structural issue indices.
238
241
  * Evaluates orphaned components, dead exports, and supply-chain threats.
239
242
  */
240
- generateSummaryReport() {
243
+ async generateSummaryReport() {
241
244
  this.metrics.endTime = Date.now();
242
245
  const durationSeconds = ((this.metrics.endTime - this.metrics.startTime) / 1000).toFixed(2);
243
246
 
@@ -254,7 +257,6 @@ export class EngineContext {
254
257
  structuralIssuesDetected: {
255
258
  deadFiles: [],
256
259
  deadExports: [],
257
- securityThreats: [],
258
260
  unusedDependencies: []
259
261
  },
260
262
  modificationsExecuted: {
@@ -266,6 +268,11 @@ export class EngineContext {
266
268
  for (const [filePath, node] of this.graph.entries()) {
267
269
  // Skip package control files from standard structural dead-code checks
268
270
  if (filePath.endsWith('package.json')) continue;
271
+
272
+ // Fix: Always track external package usage from all files, even if they are orphaned,
273
+ // to prevent false-positive unused dependency reports.
274
+ node.externalPackageUsage.forEach(pkg => this.usedExternalPackages.add(pkg));
275
+
269
276
  if (this.isPathIgnored(filePath)) continue;
270
277
 
271
278
  const relativePath = path.relative(this.cwd, filePath);
@@ -299,28 +306,34 @@ export class EngineContext {
299
306
  }
300
307
  }
301
308
 
302
- // Category C: Security Vulnerabilities
303
- if (node.securityThreats && node.securityThreats.length > 0) {
304
- node.securityThreats.forEach(threat => {
305
- summary.structuralIssuesDetected.securityThreats.push({
306
- file: relativePath,
307
- identifier: threat.variableKey,
308
- riskCode: threat.riskCode || 'HIGH_RISK_SECRET_LEAK',
309
- entropy: threat.entropyValue,
310
- line: threat.line || 1
311
- });
312
- });
313
- }
314
309
  }
315
310
 
316
311
  // Category D: Unused Dependencies Audit (Enhanced Classification)
317
312
  for (const [manifestPath, manifestData] of this.manifestDependencies.entries()) {
318
313
  const relativeManifest = path.relative(this.cwd, manifestPath);
319
-
314
+ const packageRoot = path.dirname(manifestPath);
315
+
316
+ // Collect packages that are implicitly used via scripts / config files
317
+ const implicitlyUsed = await this._depProfiler.traceImplicitInvocations(packageRoot);
318
+
319
+ // Resolve peer dependencies of all used packages so they are not flagged
320
+ const allUsedForPeerResolution = new Set([...this.usedExternalPackages, ...implicitlyUsed]);
321
+ const peerDepsOfUsed = await this._depProfiler.resolvePeerDependencies(allUsedForPeerResolution, packageRoot);
322
+
320
323
  const checkDeps = (deps, type) => {
321
324
  if (!deps) return;
322
325
  for (const dep of deps) {
323
- if (!this.usedExternalPackages.has(dep)) {
326
+ // Skip peer and optional dependencies – they are never "unused" in the
327
+ // traditional sense because they are not required to be imported directly.
328
+ if (this._depProfiler.shouldExcludeFromUnusedCheck(dep, type)) {
329
+ continue;
330
+ }
331
+
332
+ const isUsedInCode = this.usedExternalPackages.has(dep);
333
+ const isUsedImplicitly = implicitlyUsed.has(dep);
334
+ const isRequiredAsPeer = peerDepsOfUsed.has(dep);
335
+
336
+ if (!isUsedInCode && !isUsedImplicitly && !isRequiredAsPeer) {
324
337
  summary.structuralIssuesDetected.unusedDependencies.push({
325
338
  manifest: relativeManifest,
326
339
  package: dep,
@@ -333,6 +346,7 @@ export class EngineContext {
333
346
 
334
347
  checkDeps(manifestData.dependencies, 'dependency');
335
348
  checkDeps(manifestData.devDependencies, 'devDependency');
349
+ // peerDependencies and optionalDependencies are excluded via shouldExcludeFromUnusedCheck
336
350
  checkDeps(manifestData.peerDependencies, 'peerDependency');
337
351
  checkDeps(manifestData.optionalDependencies, 'optionalDependency');
338
352
  }
@@ -6,6 +6,16 @@ export class ASTAnalyzer {
6
6
  this.scopeStack = [];
7
7
  }
8
8
 
9
+ /**
10
+ * Returns the TypeScript ScriptKind for a given file path.
11
+ * Exposed as a public method so WorkerTaskRunner can call it directly.
12
+ */
13
+ getScriptKind(filePath) {
14
+ if (filePath.endsWith('.tsx') || filePath.endsWith('.jsx')) return ts.ScriptKind.TSX;
15
+ if (filePath.endsWith('.js') || filePath.endsWith('.mjs') || filePath.endsWith('.cjs')) return ts.ScriptKind.JS;
16
+ return ts.ScriptKind.TS;
17
+ }
18
+
9
19
  parseFile(filePath, content, fileNode) {
10
20
  if (this.context.verbose) {
11
21
  console.log(`[AST] Parsing: ${filePath}`);
@@ -16,7 +26,7 @@ export class ASTAnalyzer {
16
26
  content,
17
27
  ts.ScriptTarget.Latest,
18
28
  true,
19
- filePath.endsWith('.tsx') || filePath.endsWith('.jsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS
29
+ this.getScriptKind(filePath)
20
30
  );
21
31
 
22
32
  this.currentScope = { symbols: new Map(), parent: null };
@@ -29,6 +39,13 @@ export class ASTAnalyzer {
29
39
  this.currentScope = null;
30
40
  }
31
41
 
42
+ /**
43
+ * Alias for walkAST used by WorkerTaskRunner (legacy API compatibility).
44
+ */
45
+ walkNode(node, sourceFile, fileNode) {
46
+ return this.walkAST(node, fileNode, sourceFile);
47
+ }
48
+
32
49
  pushScope() {
33
50
  const newScope = { symbols: new Map(), parent: this.currentScope };
34
51
  this.scopeStack.push(newScope);
@@ -87,6 +104,16 @@ export class ASTAnalyzer {
87
104
  case ts.SyntaxKind.ModuleDeclaration:
88
105
  this.handleNamedDeclaration(node, fileNode, sourceFile);
89
106
  break;
107
+ case ts.SyntaxKind.Identifier:
108
+ fileNode.instantiatedIdentifiers.add(node.text);
109
+ break;
110
+ case ts.SyntaxKind.StringLiteral:
111
+ case ts.SyntaxKind.NoSubstitutionTemplateLiteral:
112
+ fileNode.rawStringReferences.add(node.text);
113
+ break;
114
+ case ts.SyntaxKind.PropertyAccessExpression:
115
+ fileNode.propertyAccessChains.add(node.getText(sourceFile));
116
+ break;
90
117
  case ts.SyntaxKind.CallExpression:
91
118
  this.handleCallExpression(node, fileNode, sourceFile);
92
119
  break;
@@ -97,13 +124,6 @@ export class ASTAnalyzer {
97
124
  case ts.SyntaxKind.Decorator:
98
125
  this.handleDecorator(node, fileNode, sourceFile);
99
126
  break;
100
- case ts.SyntaxKind.Identifier:
101
- // Track usage of identifiers
102
- const symbol = this.resolveSymbol(node.text);
103
- if (symbol) {
104
- // fileNode.usedSymbols.add(`${symbol.node.parent.kind}:${node.text}`); // More granular tracking needed
105
- }
106
- break;
107
127
  }
108
128
 
109
129
  ts.forEachChild(node, child => this.walkAST(child, fileNode, sourceFile));
@@ -117,88 +137,67 @@ export class ASTAnalyzer {
117
137
  if (node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
118
138
  const specifier = node.moduleSpecifier.text;
119
139
  fileNode.explicitImports.add(specifier);
140
+
141
+ // Track external package usage for dependency analysis
142
+ if (!specifier.startsWith('.') && !specifier.startsWith('/')) {
143
+ fileNode.externalPackageUsage.add(this._extractPackageName(specifier));
144
+ }
120
145
 
121
146
  if (node.importClause) {
147
+ if (node.importClause.name) {
148
+ fileNode.importedSymbols.add(`${specifier}:default`);
149
+ }
122
150
  if (node.importClause.namedBindings) {
123
- if (ts.isNamedImports(node.importClause.namedBindings)) {
151
+ if (ts.isNamespaceImport(node.importClause.namedBindings)) {
152
+ fileNode.importedSymbols.add(`${specifier}:*`);
153
+ } else if (ts.isNamedImports(node.importClause.namedBindings)) {
124
154
  node.importClause.namedBindings.elements.forEach(element => {
125
- const importedName = element.name.text;
126
- const propertyName = element.propertyName ? element.propertyName.text : importedName;
127
- fileNode.importedSymbols.add(`${specifier}:${propertyName}`);
128
- this.addDeclaredSymbol(element.name.text, element, sourceFile); // Add local import name to scope
155
+ const importedName = element.propertyName ? element.propertyName.text : element.name.text;
156
+ fileNode.importedSymbols.add(`${specifier}:${importedName}`);
129
157
  });
130
- } else if (ts.isNamespaceImport(node.importClause.namedBindings)) {
131
- fileNode.importedSymbols.add(`${specifier}:*`);
132
- this.addDeclaredSymbol(node.importClause.namedBindings.name.text, node.importClause.namedBindings, sourceFile); // Add namespace import to scope
133
158
  }
134
159
  }
135
- if (node.importClause.name) {
136
- fileNode.importedSymbols.add(`${specifier}:default`);
137
- this.addDeclaredSymbol(node.importClause.name.text, node.importClause.name, sourceFile); // Add default import to scope
138
- }
139
160
  }
140
161
  }
141
162
  }
142
163
 
143
164
  handleExportDeclaration(node, fileNode, sourceFile) {
144
- if (node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
145
- const specifier = node.moduleSpecifier.text;
146
- if (node.exportClause && ts.isNamedExports(node.exportClause)) {
147
- node.exportClause.elements.forEach(element => {
148
- const name = element.name.text;
149
- const propertyName = element.propertyName ? element.propertyName.text : name;
150
- fileNode.internalExports.set(name, {
151
- type: 're-export',
152
- source: specifier,
153
- originalName: propertyName,
154
- start: node.getStart(sourceFile),
155
- end: node.getEnd()
156
- });
157
- });
158
- } else if (node.exportClause && ts.isNamespaceExport(node.exportClause)) {
159
- // export * as name from 'module'
165
+ const specifier = node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier) ? node.moduleSpecifier.text : null;
166
+
167
+ if (specifier) {
168
+ // Re-export from source: export * from './module' or export { x } from './module'
169
+ fileNode.explicitImports.add(specifier);
170
+
171
+ // Track external package usage from re-exports
172
+ if (!specifier.startsWith('.') && !specifier.startsWith('/')) {
173
+ fileNode.externalPackageUsage.add(this._extractPackageName(specifier));
174
+ }
175
+
176
+ if (!node.exportClause) {
177
+ // export * from './module'
178
+ fileNode.internalExports.set('*', { type: 're-export-all', source: specifier });
179
+ fileNode.importedSymbols.add(`${specifier}:*`);
180
+ } else if (ts.isNamespaceExport(node.exportClause)) {
181
+ // export * as name from './module'
160
182
  const name = node.exportClause.name.text;
161
183
  fileNode.internalExports.set(name, { type: 're-export-namespace', source: specifier, originalName: '*', start: node.getStart(sourceFile), end: node.getEnd() });
162
- } else {
163
- // export * from 'module'
164
- fileNode.internalExports.set('*', { type: 're-export-all', source: specifier });
165
- }
166
- } else if (node.exportClause && ts.isNamedExports(node.exportClause)) {
184
+ fileNode.importedSymbols.add(`${specifier}:*`);
185
+ } else if (ts.isNamedExports(node.exportClause)) {
186
+ // export { x, y as z } from './module'
167
187
  node.exportClause.elements.forEach(element => {
168
- const name = element.name.text;
169
- const propertyName = element.propertyName ? element.propertyName.text : name;
170
- fileNode.internalExports.set(name, {
171
- type: 'export',
172
- originalName: propertyName,
173
- start: node.getStart(sourceFile),
174
- end: node.getEnd()
175
- });
188
+ const originalName = element.propertyName ? element.propertyName.text : element.name.text;
189
+ const exportedName = element.name.text;
190
+ fileNode.internalExports.set(exportedName, { type: 're-export', source: specifier, originalName, start: element.getStart(sourceFile), end: element.getEnd() });
191
+ fileNode.importedSymbols.add(`${specifier}:${originalName}`);
176
192
  });
177
- } else if (node.declaration) {
178
- // Direct export of a declaration (e.g., export const x = 1;)
179
- if (ts.isVariableStatement(node.declaration)) {
180
- node.declaration.declarationList.declarations.forEach(decl => {
181
- if (decl.name && ts.isIdentifier(decl.name)) {
182
- const name = decl.name.text;
183
- fileNode.internalExports.set(name, {
184
- type: 'variable',
185
- start: decl.getStart(sourceFile),
186
- end: decl.getEnd()
187
- });
188
- this.addDeclaredSymbol(name, decl, sourceFile);
189
- }
190
- });
191
- } else if (ts.isFunctionDeclaration(node.declaration) || ts.isClassDeclaration(node.declaration)) {
192
- const name = node.declaration.name?.text;
193
- if (name) {
194
- fileNode.internalExports.set(name, {
195
- type: ts.SyntaxKind[node.declaration.kind].toLowerCase().replace('declaration', ''),
196
- start: node.declaration.getStart(sourceFile),
197
- end: node.declaration.getEnd()
198
- });
199
- this.addDeclaredSymbol(name, node.declaration, sourceFile);
200
- }
201
193
  }
194
+ } else if (node.exportClause && ts.isNamedExports(node.exportClause)) {
195
+ // Local named exports: export { x, y as z }
196
+ node.exportClause.elements.forEach(element => {
197
+ const localName = element.propertyName ? element.propertyName.text : element.name.text;
198
+ const exportedName = element.name.text;
199
+ fileNode.internalExports.set(exportedName, { type: 'export', originalName: localName, start: element.getStart(sourceFile), end: element.getEnd() });
200
+ });
202
201
  }
203
202
  }
204
203
 
@@ -325,16 +324,32 @@ export class ASTAnalyzer {
325
324
  }
326
325
 
327
326
  handleCallExpression(node, fileNode, sourceFile) {
327
+ // Dynamic import(): import('./module').then(...)
328
328
  if (node.expression.kind === ts.SyntaxKind.ImportKeyword) {
329
329
  const arg = node.arguments[0];
330
- if (arg && ts.isStringLiteral(arg)) {
331
- fileNode.explicitImports.add(arg.text);
332
- fileNode.dynamicImports.add(arg.text);
330
+ if (arg) {
331
+ if (ts.isStringLiteral(arg)) {
332
+ fileNode.explicitImports.add(arg.text);
333
+ fileNode.dynamicImports.add(arg.text);
334
+ // Track external package usage from dynamic imports
335
+ if (!arg.text.startsWith('.') && !arg.text.startsWith('/')) {
336
+ fileNode.externalPackageUsage.add(this._extractPackageName(arg.text));
337
+ }
338
+ } else {
339
+ // Dynamic import with a non-literal expression (e.g., variable or template literal).
340
+ if (fileNode.calculatedDynamicImports) {
341
+ fileNode.calculatedDynamicImports.push({ kind: ts.SyntaxKind[arg.kind], start: arg.getStart(sourceFile) });
342
+ }
343
+ }
333
344
  }
334
345
  } else if (ts.isIdentifier(node.expression) && node.expression.text === 'require') {
335
346
  const arg = node.arguments[0];
336
347
  if (arg && ts.isStringLiteral(arg)) {
337
348
  fileNode.explicitImports.add(arg.text);
349
+ // Track external package usage from require() calls
350
+ if (!arg.text.startsWith('.') && !arg.text.startsWith('/')) {
351
+ fileNode.externalPackageUsage.add(this._extractPackageName(arg.text));
352
+ }
338
353
  }
339
354
  }
340
355
  }
@@ -371,7 +386,6 @@ export class ASTAnalyzer {
371
386
  if (ts.isCallExpression(node.expression)) {
372
387
  node.expression.arguments.forEach(arg => {
373
388
  // Further analysis of arguments can be done here if needed
374
- // e.g., if (ts.isStringLiteral(arg)) fileNode.decoratorArgs.add(`${decoratorName}:${arg.text}`);
375
389
  });
376
390
  }
377
391
  }
@@ -391,4 +405,18 @@ export class ASTAnalyzer {
391
405
  }
392
406
  }
393
407
  }
408
+
409
+ /**
410
+ * Extracts the root npm package name from an import specifier.
411
+ * Handles scoped packages (@scope/pkg) and subpath imports (pkg/utils, @scope/pkg/utils).
412
+ */
413
+ _extractPackageName(specifier) {
414
+ if (specifier.startsWith('@')) {
415
+ // Scoped package: @scope/name or @scope/name/subpath
416
+ const parts = specifier.split('/');
417
+ return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : specifier;
418
+ }
419
+ // Regular package: name or name/subpath
420
+ return specifier.split('/')[0];
421
+ }
394
422
  }
@@ -178,19 +178,39 @@ export class BarrelParser {
178
178
  }
179
179
 
180
180
  // Rule D: Sweep through anonymous star re-exports vectors (export * from 'module')
181
+ //
182
+ // Algorithm:
183
+ // 1. For each `export * from './child'`, recursively resolve the symbol in the child.
184
+ // 2. A non-barrel child immediately returns `{ originFile: childPath }` regardless of
185
+ // whether it actually declares the symbol. We therefore must verify that the
186
+ // returned origin file actually contains the symbol in its declaredLocalExports
187
+ // before accepting the result.
188
+ // 3. If the child is itself a barrel, the recursive call already performs the full
189
+ // chain walk, so we only need to verify the final origin.
181
190
  for (const relativePath of spec.wildcardExports) {
182
191
  const fullyResolvedPath = this.resolver.resolveModulePath(contextFilePath, relativePath);
183
192
 
184
193
  if (fullyResolvedPath) {
185
- // Look inside the graph metadata map configuration to verify child target availability
186
194
  const continuousResolutionTrace = await this.determineSymbolDeclarationOrigin(
187
195
  fullyResolvedPath,
188
196
  targetSymbolName,
189
197
  activeProjectGraph,
190
- protectionStack
198
+ new Set(protectionStack) // Use a copy so sibling branches don't block each other
191
199
  );
192
- // If a resolution was found in a child barrel, return it
193
- if (continuousResolutionTrace && continuousResolutionTrace.originFile !== fullyResolvedPath) {
200
+
201
+ if (!continuousResolutionTrace) continue;
202
+ if (continuousResolutionTrace.originFile === contextFilePath) continue;
203
+
204
+ // Verify that the resolved origin actually declares the symbol.
205
+ // This prevents a non-barrel sibling (e.g. constants.ts) from being
206
+ // incorrectly returned for a symbol it does not export (e.g. formatData).
207
+ const originSpec = await this.parseBarrelSpecification(continuousResolutionTrace.originFile);
208
+ if (originSpec.declaredLocalExports.has(continuousResolutionTrace.originSymbol)) {
209
+ return continuousResolutionTrace;
210
+ }
211
+ // The origin spec is itself a barrel (isBarrelInstance = true) and the
212
+ // recursive call already resolved through it – accept the result.
213
+ if (originSpec.isBarrelInstance) {
194
214
  return continuousResolutionTrace;
195
215
  }
196
216
  }
@@ -6,6 +6,15 @@ import { PluginRegistry } from '../plugins/PluginRegistry.js';
6
6
  * Ecosystem Entry Point Manifest & Dynamic Framework Router Heuristic Validator
7
7
  * Intercepts implicit conventions to handle cases where direct import statements are absent.
8
8
  * Now refactored to use a pluggable architecture.
9
+ *
10
+ * Improvements over v1:
11
+ * - Extended config-file detection list (Biome, Oxlint, tsup, unbuild, etc.)
12
+ * - Next.js App Router conventions (page.tsx, layout.tsx, loading.tsx, error.tsx, etc.)
13
+ * - Remix conventions (route files under app/routes/)
14
+ * - SvelteKit conventions (+page.svelte, +layout.svelte, etc.)
15
+ * - Astro page/layout conventions
16
+ * - Common entry-point patterns (bin/, cli.ts, server.ts, main.ts, app.ts)
17
+ * - Test file patterns extended to cover Vitest workspace files
9
18
  */
10
19
  export class MagicDetector {
11
20
  constructor(context) {
@@ -60,20 +69,109 @@ export class MagicDetector {
60
69
  }
61
70
 
62
71
  isCoreToolingSuiteElement(normalizedPath) {
63
- // Testing and execution matrices rules configuration keys
64
- if (/\.(test|spec|e2e|cy)\.(js|ts|tsx|jsx|stories\.tsx|stories\.ts)$/i.test(normalizedPath)) return true;
65
-
66
- // Testing tools and structural environment frameworks configuration keys
67
- const testEnvironments = [
68
- 'jest.config.', 'vitest.config.', 'playwright.config.', 'cypress.config.',
69
- 'webpack.config.', 'vite.config.', 'rollup.config.', 'tailwind.config.',
70
- '.eslintrc.', 'prettier.config.', '.postcssrc.', 'postcss.config.',
71
- 'bin/cli.js', 'index.js', 'WorkerTaskRunner.js',
72
- 'src/server.ts', 'src/main.ts', 'src/app.ts', 'src/index.tsx', 'src/index.ts',
73
- 'server.ts', 'main.ts', 'app.ts'
72
+ // ── Test / spec files ──────────────────────────────────────────────────────
73
+ if (/\.(test|spec|e2e|cy)\.(js|ts|tsx|jsx)$/i.test(normalizedPath)) return true;
74
+ if (/\.stories\.(js|ts|tsx|jsx)$/i.test(normalizedPath)) return true;
75
+
76
+ // ── Build / bundler config files ───────────────────────────────────────────
77
+ const configFragments = [
78
+ // Test runners
79
+ 'jest.config.', 'vitest.config.', 'vitest.workspace.',
80
+ 'playwright.config.', 'cypress.config.',
81
+ // Bundlers
82
+ 'webpack.config.', 'vite.config.', 'rollup.config.',
83
+ 'esbuild.config.', 'parcel.config.',
84
+ 'tsup.config.', 'unbuild.config.', 'pkgroll.config.',
85
+ // CSS / styling
86
+ 'tailwind.config.', 'postcss.config.', '.postcssrc.',
87
+ // Linters / formatters
88
+ '.eslintrc.', 'eslint.config.', 'prettier.config.', '.prettierrc.',
89
+ '.stylelintrc.', 'stylelint.config.',
90
+ 'biome.json', '.oxlintrc.',
91
+ // Babel / transpilation
92
+ '.babelrc.', 'babel.config.',
93
+ // Commit / git hooks
94
+ '.commitlintrc.', 'commitlint.config.',
95
+ '.lintstagedrc.', 'lint-staged.config.',
96
+ // Documentation
97
+ 'typedoc.config.', 'typedoc.json',
98
+ // Monorepo tools
99
+ 'turbo.json', 'nx.json', 'lerna.json',
100
+ // Misc tooling
101
+ 'knip.config.', 'knip.json',
102
+ 'syncpack.config.',
103
+ // Internal worker
104
+ 'WorkerTaskRunner.js'
74
105
  ];
75
-
76
- return testEnvironments.some(matchPattern => normalizedPath.includes(matchPattern));
106
+ if (configFragments.some(f => normalizedPath.includes(f))) return true;
107
+
108
+ // ── Common application entry points ───────────────────────────────────────
109
+ const entryPatterns = [
110
+ // CLI binaries
111
+ '/bin/cli.js', '/bin/cli.ts', '/bin/cli.mjs',
112
+ '/bin/index.js', '/bin/index.ts',
113
+ // Server / app entry points
114
+ '/src/server.ts', '/src/server.js',
115
+ '/src/main.ts', '/src/main.js',
116
+ '/src/app.ts', '/src/app.js',
117
+ '/src/index.ts', '/src/index.tsx',
118
+ '/src/index.js', '/src/index.jsx',
119
+ '/server.ts', '/server.js',
120
+ '/main.ts', '/main.js',
121
+ '/app.ts', '/app.js',
122
+ '/index.ts', '/index.tsx',
123
+ '/index.js', '/index.jsx',
124
+ ];
125
+ if (entryPatterns.some(p => normalizedPath.endsWith(p))) return true;
126
+
127
+ // ── Next.js App Router conventions ────────────────────────────────────────
128
+ // Files under app/ directory with Next.js special names
129
+ if (/\/app\/(page|layout|loading|error|not-found|template|default|route|middleware)\.(js|ts|tsx|jsx)$/.test(normalizedPath)) return true;
130
+ // Next.js Pages Router
131
+ if (/\/pages\/.*\.(js|ts|tsx|jsx)$/.test(normalizedPath)) return true;
132
+ // Next.js API routes
133
+ if (/\/pages\/api\/.*\.(js|ts)$/.test(normalizedPath)) return true;
134
+ // Next.js middleware
135
+ if (/\/middleware\.(js|ts)$/.test(normalizedPath)) return true;
136
+ // Next.js config
137
+ if (/\/next\.config\.(js|ts|mjs|cjs)$/.test(normalizedPath)) return true;
138
+
139
+ // ── Remix conventions ─────────────────────────────────────────────────────
140
+ if (/\/app\/routes\/.*\.(js|ts|tsx|jsx)$/.test(normalizedPath)) return true;
141
+ if (/\/app\/root\.(js|ts|tsx|jsx)$/.test(normalizedPath)) return true;
142
+ if (/\/app\/entry\.(client|server)\.(js|ts|tsx|jsx)$/.test(normalizedPath)) return true;
143
+
144
+ // ── SvelteKit conventions ─────────────────────────────────────────────────
145
+ if (/\/\+page(\.(server|client))?\.(svelte|js|ts)$/.test(normalizedPath)) return true;
146
+ if (/\/\+layout(\.(server|client))?\.(svelte|js|ts)$/.test(normalizedPath)) return true;
147
+ if (/\/\+error\.(svelte|js|ts)$/.test(normalizedPath)) return true;
148
+ if (/\/\+server\.(js|ts)$/.test(normalizedPath)) return true;
149
+ if (/\/svelte\.config\.(js|ts|mjs)$/.test(normalizedPath)) return true;
150
+
151
+ // ── Astro conventions ─────────────────────────────────────────────────────
152
+ if (/\/src\/pages\/.*\.astro$/.test(normalizedPath)) return true;
153
+ if (/\/src\/layouts\/.*\.astro$/.test(normalizedPath)) return true;
154
+ if (/\/astro\.config\.(mjs|js|ts)$/.test(normalizedPath)) return true;
155
+
156
+ // ── Nuxt conventions ──────────────────────────────────────────────────────
157
+ if (/\/pages\/.*\.vue$/.test(normalizedPath)) return true;
158
+ if (/\/layouts\/.*\.vue$/.test(normalizedPath)) return true;
159
+ if (/\/server\/api\/.*\.(js|ts)$/.test(normalizedPath)) return true;
160
+ if (/\/nuxt\.config\.(js|ts|mjs)$/.test(normalizedPath)) return true;
161
+
162
+ // ── React / Vite entry points ─────────────────────────────────────────────
163
+ if (/\/vite\.config\.(js|ts|mjs)$/.test(normalizedPath)) return true;
164
+
165
+ // ── Angular conventions ───────────────────────────────────────────────────
166
+ if (/\/main\.(ts|js)$/.test(normalizedPath)) return true;
167
+ if (/\/app\.module\.(ts|js)$/.test(normalizedPath)) return true;
168
+ if (/\/angular\.json$/.test(normalizedPath)) return true;
169
+
170
+ // ── Expo / React Native ───────────────────────────────────────────────────
171
+ if (/\/app\/_layout\.(js|ts|tsx|jsx)$/.test(normalizedPath)) return true;
172
+ if (/\/app\/index\.(js|ts|tsx|jsx)$/.test(normalizedPath)) return true;
173
+
174
+ return false;
77
175
  }
78
176
 
79
177
  /**