pkg-scaffold 2.4.0 ā 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -107
- package/bin/cli.js +6 -3
- package/package.json +1 -1
- package/src/EngineContext.js +6 -1
- package/src/ast/ASTAnalyzer.js +30 -18
- package/src/ast/BarrelParser.js +12 -0
- package/src/ast/MagicDetector.js +2 -1
- package/src/healing/SelfHealer.js +1 -1
- package/src/index.js +86 -45
- package/src/performance/GraphCache.js +3 -1
- package/src/performance/WorkerPool.js +4 -1
- package/src/performance/WorkerTaskRunner.js +5 -3
- package/src/resolution/DepencyResolver.js +11 -0
- package/src/resolution/WorkSpaceGraph.js +1 -1
package/README.md
CHANGED
|
@@ -1,107 +1,94 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
**
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
[**
|
|
1
|
+
# š¦ pkg-scaffold v3.0.0
|
|
2
|
+
|
|
3
|
+
**Enterprise-Grade AST Syntax Refactoring & Self-Healing Engine**
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/pkg-scaffold)
|
|
6
|
+
[](https://www.npmjs.com/package/pkg-scaffold)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
[](https://github.com/DreamLongYT/pkg-scaffold)
|
|
9
|
+
|
|
10
|
+
`pkg-scaffold` is an advanced, AST-driven tool designed to prune dead code, orphaned files, and unused exports from your JavaScript and TypeScript codebases with unprecedented safety. Unlike traditional linters, `pkg-scaffold` doesn't just report issuesāit fixes them within a secure, transactional sandbox.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## š Key Features
|
|
15
|
+
|
|
16
|
+
- **AST-Based Deep Tracing**: Leverages the official TypeScript compiler to trace dependencies through complex barrel files, re-exports, and default export chains.
|
|
17
|
+
- **Automated Self-Healing**: Automatically runs your project's test suite after refactoring. If a regression is detected (non-zero exit code), it triggers an immediate rollback.
|
|
18
|
+
- **Transactional Safety**: All modifications occur in a Git-isolated sandbox with a backup journaling system. Your original state is always recoverable.
|
|
19
|
+
- **Interactive Optimization Plan**: Generates a detailed "dry-run" plan for user confirmation before any file is touched.
|
|
20
|
+
- **Suppression Engine**: Fine-grained control using `@scaffold-suppress` directives to protect specific files or exports from being pruned.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## āļø Competitive Analysis: Why pkg-scaffold?
|
|
25
|
+
|
|
26
|
+
While tools like [Knip](https://knip.dev/) or [Depcheck](https://github.com/depcheck/depcheck) are excellent for reporting, `pkg-scaffold` is built for **automated architectural cleanup**.
|
|
27
|
+
|
|
28
|
+
| Feature | **pkg-scaffold** | Knip.dev | Depcheck | ts-prune |
|
|
29
|
+
| :--- | :---: | :---: | :---: | :---: |
|
|
30
|
+
| **Dead Code Detection** | ā
AST-Deep | ā
Comprehensive | ā ļø Regex/Loose | ā
Basic |
|
|
31
|
+
| **Automated Pruning** | ā
Native | ā ļø Experimental | ā No | ā No |
|
|
32
|
+
| **Self-Healing (Auto-Rollback)** | ā
**Yes** | ā No | ā No | ā No |
|
|
33
|
+
| **Transaction Sandbox** | ā
**Git + Journal** | ā No | ā No | ā No |
|
|
34
|
+
| **Interactive Approval** | ā
**Yes** | ā No | ā No | ā No |
|
|
35
|
+
| **Barrel File Tracing** | ā
High-Fidelity | ā
Good | ā No | ā ļø Limited |
|
|
36
|
+
| **Status** | š Active | š Active | š ļø Maintenance | š ļø Maintenance |
|
|
37
|
+
|
|
38
|
+
> **The Verdict:** `pkg-scaffold` wins when you need a **safe, automated workflow** that guarantees your project stays functional after cleaning up technical debt.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## š ļø Installation & Usage
|
|
43
|
+
|
|
44
|
+
### Installation
|
|
45
|
+
```bash
|
|
46
|
+
npm install -g pkg-scaffold
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Basic Usage (Dry-Run)
|
|
50
|
+
Analyze your project without making any changes:
|
|
51
|
+
```bash
|
|
52
|
+
pkg-scaffold --cwd ./my-project --no-fix
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Active Refactoring (Self-Healing)
|
|
56
|
+
Analyze, prune, and validate with automatic rollback on test failure:
|
|
57
|
+
```bash
|
|
58
|
+
pkg-scaffold --cwd ./my-project --fix --test-command "npm test"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Options
|
|
62
|
+
| Flag | Description | Default |
|
|
63
|
+
| :--- | :--- | :--- |
|
|
64
|
+
| `--cwd <path>` | Execution context root directory | `process.cwd()` |
|
|
65
|
+
| `--fix` | Enable atomic code updates and pruning | `true` |
|
|
66
|
+
| `--test-command` | Script to run for self-healing validation | `npm test` |
|
|
67
|
+
| `--yes` | Skip confirmation prompts | `false` |
|
|
68
|
+
| `--verbose` | Toggle expanded debug telemetry | `false` |
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## š”ļø Suppression
|
|
73
|
+
|
|
74
|
+
Protect a file or a specific export from being removed:
|
|
75
|
+
|
|
76
|
+
```javascript
|
|
77
|
+
/**
|
|
78
|
+
* @scaffold-suppress legacyFunction
|
|
79
|
+
*/
|
|
80
|
+
export const legacyFunction = () => {
|
|
81
|
+
// This export is safe even if unused
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @scaffold-suppress
|
|
86
|
+
*/
|
|
87
|
+
// This entire file is protected from being flagged as orphaned
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## š License
|
|
93
|
+
|
|
94
|
+
MIT Ā© DreamLongYT
|
package/bin/cli.js
CHANGED
|
@@ -36,7 +36,8 @@ async function bootstrap() {
|
|
|
36
36
|
.option('--tsconfig <filename>', 'Specify path to custom layout configurations', 'tsconfig.json')
|
|
37
37
|
.option('--test-command <command>', 'Integrated continuous safety test validation script execution path', 'npm test')
|
|
38
38
|
.option('--workspace', 'Enable high-density workspace workspace/monorepo cluster mesh evaluation parsing', false)
|
|
39
|
-
.option('--verbose', 'Toggle expanded trace telemetry for debug operational diagnostics', false)
|
|
39
|
+
.option('--verbose', 'Toggle expanded trace telemetry for debug operational diagnostics', false)
|
|
40
|
+
.option('-y, --yes', 'Skip confirmation prompts and execute planned structural modifications automatically', false);
|
|
40
41
|
|
|
41
42
|
program.parse(process.argv);
|
|
42
43
|
const options = program.opts();
|
|
@@ -65,13 +66,15 @@ async function bootstrap() {
|
|
|
65
66
|
process.exit(1);
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
// Ensure cwd is always an absolute path regardless of how it was passed (e.g., '--cwd .')
|
|
68
70
|
const engine = new RefactoringEngine({
|
|
69
|
-
cwd: options.cwd,
|
|
71
|
+
cwd: path.resolve(options.cwd),
|
|
70
72
|
autoFix: options.fix,
|
|
71
73
|
tsconfig: options.tsconfig,
|
|
72
74
|
testCommand: options.testCommand,
|
|
73
75
|
workspace: options.workspace,
|
|
74
|
-
verbose: options.verbose
|
|
76
|
+
verbose: options.verbose,
|
|
77
|
+
skipConfirm: options.yes
|
|
75
78
|
});
|
|
76
79
|
|
|
77
80
|
await engine.run();
|
package/package.json
CHANGED
package/src/EngineContext.js
CHANGED
|
@@ -11,6 +11,7 @@ import fs from 'fs/promises';
|
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* High-Fidelity Graph Element Node representing a single file asset boundary.
|
|
14
|
+
* @scaffold-suppress GraphNode
|
|
14
15
|
*/
|
|
15
16
|
export class GraphNode {
|
|
16
17
|
constructor(filePath) {
|
|
@@ -105,6 +106,7 @@ export class EngineContext {
|
|
|
105
106
|
this.allowAutoFix = options.autoFix ?? true;
|
|
106
107
|
this.isWorkspaceEnabled = options.workspace ?? false;
|
|
107
108
|
this.verbose = options.verbose ?? false;
|
|
109
|
+
this.skipConfirm = options.skipConfirm ?? false;
|
|
108
110
|
|
|
109
111
|
// Core Memory Repositories
|
|
110
112
|
this.graph = new Map(); // Absolute File Path -> GraphNode
|
|
@@ -235,7 +237,10 @@ export class EngineContext {
|
|
|
235
237
|
const relativePath = path.relative(this.cwd, filePath);
|
|
236
238
|
|
|
237
239
|
// Category A: Completely orphaned components (no references, not library/framework entries)
|
|
238
|
-
|
|
240
|
+
// A file with ANY @scaffold-suppress directive is considered intentionally retained and must
|
|
241
|
+
// never be flagged as orphaned, even if no other file imports it directly.
|
|
242
|
+
const fileHasSuppressDirective = node.localSuppressedRules.size > 0;
|
|
243
|
+
if (node.incomingEdges.size === 0 && !node.isLibraryEntry && !node.isFrameworkContract && !fileHasSuppressDirective) {
|
|
239
244
|
summary.structuralIssuesDetected.deadFiles.push(relativePath);
|
|
240
245
|
continue; // An orphaned file implies all internal sub-exports are dead; skip sub-checks
|
|
241
246
|
}
|
package/src/ast/ASTAnalyzer.js
CHANGED
|
@@ -32,8 +32,8 @@ export class ASTAnalyzer {
|
|
|
32
32
|
this.getScriptKind(filePath)
|
|
33
33
|
);
|
|
34
34
|
|
|
35
|
-
this.walkNode(sourceFile, sourceFile, fileNode);
|
|
36
35
|
this.extractTopLevelJSDocSuppreessions(sourceFile, fileNode);
|
|
36
|
+
this.walkNode(sourceFile, sourceFile, fileNode);
|
|
37
37
|
|
|
38
38
|
return true;
|
|
39
39
|
} catch (parseError) {
|
|
@@ -130,11 +130,19 @@ export class ASTAnalyzer {
|
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
// Handle Named Function Node Exports Matrix Configurations
|
|
133
|
+
// If the function carries both ExportKeyword AND DefaultKeyword modifiers, it is an
|
|
134
|
+
// `export default function` declaration. In that case the canonical export name is
|
|
135
|
+
// 'default', not the function's identifier, so that barrel-file tracing (which looks
|
|
136
|
+
// up 'default' when following `export { default as X } from './y'`) resolves correctly.
|
|
133
137
|
case ts.SyntaxKind.FunctionDeclaration: {
|
|
134
|
-
if (
|
|
135
|
-
const
|
|
136
|
-
if (
|
|
137
|
-
|
|
138
|
+
if (this.hasExportModifier(node)) {
|
|
139
|
+
const isDefaultExport = node.modifiers && node.modifiers.some(m => m.kind === ts.SyntaxKind.DefaultKeyword);
|
|
140
|
+
if (isDefaultExport) {
|
|
141
|
+
// Register under 'default'; also store the real name as referencedSymbol for diagnostics
|
|
142
|
+
const realName = node.name && ts.isIdentifier(node.name) ? node.name.text : 'anonymous';
|
|
143
|
+
fileNode.internalExports.set('default', { type: 'default-function', referencedSymbol: realName, start: node.getStart(sourceFile), end: node.getEnd() });
|
|
144
|
+
} else if (node.name && ts.isIdentifier(node.name)) {
|
|
145
|
+
fileNode.internalExports.set(node.name.text, { type: 'function', start: node.getStart(sourceFile), end: node.getEnd() });
|
|
138
146
|
}
|
|
139
147
|
}
|
|
140
148
|
break;
|
|
@@ -142,10 +150,13 @@ export class ASTAnalyzer {
|
|
|
142
150
|
|
|
143
151
|
// Handle Structural Class Definitions and Class Export Signatures
|
|
144
152
|
case ts.SyntaxKind.ClassDeclaration: {
|
|
145
|
-
if (
|
|
146
|
-
const
|
|
147
|
-
if (
|
|
148
|
-
|
|
153
|
+
if (this.hasExportModifier(node)) {
|
|
154
|
+
const isDefaultExport = node.modifiers && node.modifiers.some(m => m.kind === ts.SyntaxKind.DefaultKeyword);
|
|
155
|
+
if (isDefaultExport) {
|
|
156
|
+
const realName = node.name && ts.isIdentifier(node.name) ? node.name.text : 'anonymous';
|
|
157
|
+
fileNode.internalExports.set('default', { type: 'default-class', referencedSymbol: realName, start: node.getStart(sourceFile), end: node.getEnd() });
|
|
158
|
+
} else if (node.name && ts.isIdentifier(node.name)) {
|
|
159
|
+
fileNode.internalExports.set(node.name.text, { type: 'class', start: node.getStart(sourceFile), end: node.getEnd() });
|
|
149
160
|
}
|
|
150
161
|
}
|
|
151
162
|
break;
|
|
@@ -234,8 +245,8 @@ export class ASTAnalyzer {
|
|
|
234
245
|
*/
|
|
235
246
|
auditAssignmentSafety(variableName, initializer, fileNode, sourceFile) {
|
|
236
247
|
// Process variable mapping target indicators first
|
|
237
|
-
if (this.hasExportModifier(variableName
|
|
238
|
-
fileNode.internalExports.set(variableName, { type: 'variable', start: variableName.parent.getStart(sourceFile), end: variableName.parent.getEnd() });
|
|
248
|
+
if (this.hasExportModifier(variableName.parent)) {
|
|
249
|
+
fileNode.internalExports.set(variableName.text, { type: 'variable', start: variableName.parent.getStart(sourceFile), end: variableName.parent.getEnd() });
|
|
239
250
|
}
|
|
240
251
|
|
|
241
252
|
if (!initializer || !ts.isStringLiteral(initializer)) return;
|
|
@@ -274,13 +285,14 @@ export class ASTAnalyzer {
|
|
|
274
285
|
*/
|
|
275
286
|
extractTopLevelJSDocSuppreessions(sourceFile, fileNode) {
|
|
276
287
|
const fullText = sourceFile.text;
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
288
|
+
// Scan all comments in the file for @scaffold-suppress
|
|
289
|
+
const commentRegex = /\/\*\*?[\s\S]*?\*\/|\/\/.*/g;
|
|
290
|
+
let match;
|
|
291
|
+
while ((match = commentRegex.exec(fullText)) !== null) {
|
|
292
|
+
const comment = match[0];
|
|
293
|
+
const suppressMatches = comment.match(/@scaffold-suppress\s+([a-zA-Z0-9_\-*:]+)/g);
|
|
294
|
+
if (suppressMatches) {
|
|
295
|
+
suppressMatches.forEach(m => {
|
|
284
296
|
const directive = m.replace('@scaffold-suppress', '').trim();
|
|
285
297
|
fileNode.localSuppressedRules.add(directive);
|
|
286
298
|
});
|
package/src/ast/BarrelParser.js
CHANGED
|
@@ -104,6 +104,18 @@ export class BarrelParser {
|
|
|
104
104
|
}
|
|
105
105
|
break;
|
|
106
106
|
}
|
|
107
|
+
|
|
108
|
+
// Track default exports declared directly within the file boundary:
|
|
109
|
+
// Handles both `export default function foo()` and `export default expression`.
|
|
110
|
+
// The canonical symbol name stored is 'default' so that forwardedNamedExports
|
|
111
|
+
// entries whose sourceSymbol is 'default' can resolve correctly via Rule A.
|
|
112
|
+
case ts.SyntaxKind.ExportAssignment: {
|
|
113
|
+
// ExportAssignment covers `export default <expr>` (isExportEquals === false)
|
|
114
|
+
// as well as `export = <expr>` (isExportEquals === true, CommonJS style).
|
|
115
|
+
// We register 'default' for both to ensure the barrel tracer can settle here.
|
|
116
|
+
spec.declaredLocalExports.add('default');
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
107
119
|
}
|
|
108
120
|
|
|
109
121
|
ts.forEachChild(node, child => this.harvestExportSignatures(child, spec));
|
package/src/ast/MagicDetector.js
CHANGED
|
@@ -117,7 +117,8 @@ export class MagicDetector {
|
|
|
117
117
|
const testEnvironments = [
|
|
118
118
|
'jest.config.', 'vitest.config.', 'playwright.config.', 'cypress.config.',
|
|
119
119
|
'webpack.config.', 'vite.config.', 'rollup.config.', 'tailwind.config.',
|
|
120
|
-
'.eslintrc.', 'prettier.config.', '.postcssrc.', 'postcss.config.'
|
|
120
|
+
'.eslintrc.', 'prettier.config.', '.postcssrc.', 'postcss.config.',
|
|
121
|
+
'bin/cli.js', 'index.js', 'WorkerTaskRunner.js'
|
|
121
122
|
];
|
|
122
123
|
|
|
123
124
|
return testEnvironments.some(matchPattern => normalizedPath.includes(matchPattern));
|
|
@@ -29,7 +29,7 @@ export class SelfHealer {
|
|
|
29
29
|
await refactorTask();
|
|
30
30
|
|
|
31
31
|
// Step 3: Stage changes inside our Git tracking context
|
|
32
|
-
await this.gitSandbox.
|
|
32
|
+
await this.gitSandbox.stageAndCheckpointChanges();
|
|
33
33
|
|
|
34
34
|
console.log('š§Ŗ Running testing suites to verify workspace integrity...');
|
|
35
35
|
const validationPassed = await this.verifyProjectHealthStatus();
|
package/src/index.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import fs from 'fs/promises';
|
|
10
10
|
import path from 'path';
|
|
11
11
|
import ansis from 'ansis';
|
|
12
|
+
import readline from 'readline/promises';
|
|
12
13
|
|
|
13
14
|
// Import local domain architecture sub-systems
|
|
14
15
|
import { EngineContext } from './EngineContext.js';
|
|
@@ -16,17 +17,17 @@ import { ASTAnalyzer } from './ast/ASTAnalyzer.js';
|
|
|
16
17
|
import { BarrelParser } from './ast/BarrelParser.js';
|
|
17
18
|
import { MagicDetector } from './ast/MagicDetector.js';
|
|
18
19
|
import { PathMapper } from './resolution/PathMapper.js';
|
|
19
|
-
import { WorkspaceGraph } from './resolution/
|
|
20
|
-
import { DependencyResolver } from './resolution/
|
|
21
|
-
import { TransactionManager } from './
|
|
22
|
-
import { ImpactAnalyzer } from './
|
|
23
|
-
import { SourceRewriter } from './
|
|
24
|
-
import { TypeIntegrity } from './
|
|
20
|
+
import { WorkspaceGraph } from './resolution/WorkSpaceGraph.js';
|
|
21
|
+
import { DependencyResolver } from './resolution/DepencyResolver.js';
|
|
22
|
+
import { TransactionManager } from './refractor/TransactionManager.js';
|
|
23
|
+
import { ImpactAnalyzer } from './refractor/ImpactAnalyzer.js';
|
|
24
|
+
import { SourceRewriter } from './refractor/SourceRewriter.js';
|
|
25
|
+
import { TypeIntegrity } from './refractor/TypeIntegrity.js';
|
|
25
26
|
import { GitSandbox } from './healing/GitSandbox.js';
|
|
26
27
|
import { SelfHealer } from './healing/SelfHealer.js';
|
|
27
28
|
import { IncrementalCacheManager } from './performance/GraphCache.js';
|
|
28
29
|
import { WorkerPool } from './performance/WorkerPool.js';
|
|
29
|
-
import { SupplyChainGuard } from './
|
|
30
|
+
import { SupplyChainGuard } from './performance/SupplyChainGuard.js';
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
33
|
* Primary Refactoring Engine Core Coordination Controller
|
|
@@ -67,6 +68,11 @@ export class RefactoringEngine {
|
|
|
67
68
|
try {
|
|
68
69
|
console.log(ansis.bold.green('šÆ Starting pkg-scaffold Operational Optimization Cycle...'));
|
|
69
70
|
|
|
71
|
+
const rl = readline.createInterface({
|
|
72
|
+
input: process.stdin,
|
|
73
|
+
output: process.stdout
|
|
74
|
+
});
|
|
75
|
+
|
|
70
76
|
// Pass 1: Boot environment contexts and load alias configuration maps
|
|
71
77
|
await this.context.initialize();
|
|
72
78
|
await this.pathMapper.loadMappings(this.context.tsconfigFilename);
|
|
@@ -103,19 +109,23 @@ export class RefactoringEngine {
|
|
|
103
109
|
parallelParseCompleted = await this.workerPool.parallelAnalyzeCodebase(sourceCodeFilesList, this);
|
|
104
110
|
}
|
|
105
111
|
|
|
106
|
-
// Synchronous fallback loop to parse remaining files or cache misses
|
|
112
|
+
// Synchronous fallback loop to parse remaining files or cache misses.
|
|
113
|
+
// IMPORTANT: The `parallelParseCompleted` flag only controls whether the *worker pool*
|
|
114
|
+
// already processed all files. It must NOT be set per-file inside this loop ā doing so
|
|
115
|
+
// would cause all files after the first cache-hit to be silently skipped.
|
|
107
116
|
for (const filePath of sourceCodeFilesList) {
|
|
108
117
|
const node = this.context.createNode(filePath);
|
|
109
118
|
const currentHash = await this.cacheManager.computeHash(filePath);
|
|
110
119
|
node.contentHash = currentHash;
|
|
111
120
|
|
|
112
|
-
if
|
|
121
|
+
// Check if this individual file is already up-to-date in the cache
|
|
122
|
+
const isFileCached = cacheManifest[filePath] && cacheManifest[filePath].hash === currentHash;
|
|
123
|
+
|
|
124
|
+
if (isFileCached) {
|
|
113
125
|
this.context.metrics.cacheHits++;
|
|
114
126
|
this.hydrateNodeFromCache(node, cacheManifest[filePath]);
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (!parallelParseCompleted) {
|
|
127
|
+
} else if (!parallelParseCompleted) {
|
|
128
|
+
// Only parse if the worker pool did not already handle this file
|
|
119
129
|
this.context.metrics.cacheMisses++;
|
|
120
130
|
await this.analyzer.processFile(filePath, node);
|
|
121
131
|
}
|
|
@@ -139,45 +149,68 @@ export class RefactoringEngine {
|
|
|
139
149
|
analysisSummary.structuralIssuesDetected.deadExports.length > 0;
|
|
140
150
|
|
|
141
151
|
if (structuralModificationsStaged) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
152
|
+
console.log(ansis.bold.yellow('\nš Proposed Optimization Plan:'));
|
|
153
|
+
console.log(ansis.dim('------------------------------------------------------------'));
|
|
154
|
+
|
|
155
|
+
if (analysisSummary.structuralIssuesDetected.deadFiles.length > 0) {
|
|
156
|
+
console.log(ansis.bold(` šļø Delete ${analysisSummary.structuralIssuesDetected.deadFiles.length} orphaned files:`));
|
|
157
|
+
analysisSummary.structuralIssuesDetected.deadFiles.forEach(f => console.log(ansis.dim(` ⢠${f}`)));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (analysisSummary.structuralIssuesDetected.deadExports.length > 0) {
|
|
161
|
+
console.log(ansis.bold(` āļø Prune ${analysisSummary.structuralIssuesDetected.deadExports.length} unused named exports:`));
|
|
162
|
+
analysisSummary.structuralIssuesDetected.deadExports.forEach(e => console.log(ansis.dim(` ⢠${e.symbol} in ${e.file}:${e.line}`)));
|
|
163
|
+
}
|
|
164
|
+
console.log(ansis.dim('------------------------------------------------------------'));
|
|
165
|
+
|
|
166
|
+
let proceed = this.context.skipConfirm;
|
|
167
|
+
if (!proceed) {
|
|
168
|
+
const answer = await rl.question(ansis.bold.cyan('\nā Apply these structural modifications? (y/N): '));
|
|
169
|
+
proceed = answer.toLowerCase() === 'y';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (proceed) {
|
|
173
|
+
await this.selfHealer.runSelfHealingLifecycle(async () => {
|
|
174
|
+
// Sub-Task A: Purge completely unreferenced dangling components
|
|
175
|
+
for (const relPath of analysisSummary.structuralIssuesDetected.deadFiles) {
|
|
176
|
+
const absPath = path.resolve(this.context.cwd, relPath);
|
|
177
|
+
console.log(ansis.red(`āļø Removing unreferenced file: ${relPath}`));
|
|
178
|
+
await this.txManager.stageDeletion(absPath);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Sub-Task B: Surgically remove unused named export blocks from active files
|
|
182
|
+
for (const unusedExport of analysisSummary.structuralIssuesDetected.deadExports) {
|
|
183
|
+
const absPath = path.resolve(this.context.cwd, unusedExport.file);
|
|
184
|
+
const node = this.context.graph.get(absPath);
|
|
168
185
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
186
|
+
if (!node) continue;
|
|
187
|
+
const meta = node.internalExports.get(unusedExport.symbol);
|
|
188
|
+
|
|
189
|
+
// Perform safety analysis to ensure the token isn't called via dynamic runtime methods
|
|
190
|
+
const safetyVerdict = await this.impactAnalyzer.verifyRefactorSafety(absPath, unusedExport.symbol, this.context.graph);
|
|
191
|
+
|
|
192
|
+
if (safetyVerdict.isSafeToPrune) {
|
|
193
|
+
console.log(ansis.yellow(`ā” Stripping unused export [${unusedExport.symbol}] from: ${unusedExport.file}:${unusedExport.line}`));
|
|
194
|
+
const nextText = await this.sourceRewriter.stripNamedExportSignature(absPath, unusedExport.symbol, meta);
|
|
195
|
+
|
|
196
|
+
await this.txManager.stageWrite(absPath, nextText);
|
|
197
|
+
|
|
198
|
+
// Align matching type declaration boundaries (.d.ts) to prevent compilation errors
|
|
199
|
+
await this.typeIntegrity.synchronizeDeclarationFile(absPath, unusedExport.symbol);
|
|
200
|
+
} else if (this.context.verbose) {
|
|
201
|
+
console.log(ansis.gray(`š”ļø Preserving symbol export [${unusedExport.symbol}] due to: ${safetyVerdict.blockReason}`));
|
|
202
|
+
}
|
|
173
203
|
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
204
|
+
});
|
|
205
|
+
} else {
|
|
206
|
+
console.log(ansis.bold.yellow('\nā ļø Optimization plan aborted by user. No changes applied.'));
|
|
207
|
+
}
|
|
176
208
|
}
|
|
177
209
|
}
|
|
178
210
|
|
|
179
211
|
// Pass 7: Save optimized graph footprints back to the cache directory
|
|
180
212
|
await this.cacheManager.saveCacheManifest(this.context.graph);
|
|
213
|
+
rl.close();
|
|
181
214
|
console.log(ansis.bold.green('\n⨠Core optimization cycle completed smoothly. Codebase workspace is healthy.'));
|
|
182
215
|
|
|
183
216
|
} catch (criticalFault) {
|
|
@@ -269,6 +302,14 @@ export class RefactoringEngine {
|
|
|
269
302
|
}
|
|
270
303
|
node.isLibraryEntry = cachedRecord.isLibraryEntry || false;
|
|
271
304
|
node.securityThreats = cachedRecord.securityThreats || [];
|
|
305
|
+
if (cachedRecord.localSuppressedRules) {
|
|
306
|
+
cachedRecord.localSuppressedRules.forEach(r => node.localSuppressedRules.add(r));
|
|
307
|
+
}
|
|
308
|
+
if (cachedRecord.symbolSourceLocations) {
|
|
309
|
+
Object.entries(cachedRecord.symbolSourceLocations).forEach(([k, v]) => {
|
|
310
|
+
node.symbolSourceLocations.set(k, v);
|
|
311
|
+
});
|
|
312
|
+
}
|
|
272
313
|
}
|
|
273
314
|
|
|
274
315
|
/**
|
|
@@ -63,7 +63,9 @@ export class IncrementalCacheManager {
|
|
|
63
63
|
instantiatedIdentifiers: Array.from(node.instantiatedIdentifiers),
|
|
64
64
|
propertyAccessChains: Array.from(node.propertyAccessChains),
|
|
65
65
|
internalExports: Object.fromEntries(node.internalExports),
|
|
66
|
-
securityThreats: node.securityThreats || []
|
|
66
|
+
securityThreats: node.securityThreats || [],
|
|
67
|
+
localSuppressedRules: Array.from(node.localSuppressedRules),
|
|
68
|
+
symbolSourceLocations: Object.fromEntries(node.symbolSourceLocations)
|
|
67
69
|
};
|
|
68
70
|
}
|
|
69
71
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Worker, isMainThread, parentPort, workerData } from 'worker_threads';
|
|
2
|
-
import os from '
|
|
2
|
+
import os from 'os';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -62,6 +62,9 @@ export class WorkerPool {
|
|
|
62
62
|
});
|
|
63
63
|
|
|
64
64
|
node.securityThreats = result.securityThreats || [];
|
|
65
|
+
if (result.localSuppressedRules) {
|
|
66
|
+
result.localSuppressedRules.forEach(r => node.localSuppressedRules.add(r));
|
|
67
|
+
}
|
|
65
68
|
});
|
|
66
69
|
|
|
67
70
|
return true;
|
|
@@ -36,10 +36,11 @@ async function processThreadChunks() {
|
|
|
36
36
|
text,
|
|
37
37
|
ts.ScriptTarget.Latest,
|
|
38
38
|
true,
|
|
39
|
-
|
|
39
|
+
standaloneAnalyzer.getScriptKind(file)
|
|
40
40
|
);
|
|
41
41
|
|
|
42
|
-
standaloneAnalyzer.
|
|
42
|
+
standaloneAnalyzer.extractTopLevelJSDocSuppreessions(sourceFile, mockNode);
|
|
43
|
+
standaloneAnalyzer.walkNode(sourceFile, sourceFile, mockNode);
|
|
43
44
|
|
|
44
45
|
partialGraphPayloadResults.push({
|
|
45
46
|
filePath: file,
|
|
@@ -50,7 +51,8 @@ async function processThreadChunks() {
|
|
|
50
51
|
instantiatedIdentifiers: Array.from(mockNode.instantiatedIdentifiers),
|
|
51
52
|
propertyAccessChains: Array.from(mockNode.propertyAccessChains),
|
|
52
53
|
internalExports: Object.fromEntries(mockNode.internalExports),
|
|
53
|
-
securityThreats: mockNode.securityThreats
|
|
54
|
+
securityThreats: mockNode.securityThreats,
|
|
55
|
+
localSuppressedRules: Array.from(mockNode.localSuppressedRules)
|
|
54
56
|
});
|
|
55
57
|
} catch {
|
|
56
58
|
// Ignore unparseable or locked syntax nodes in thread loops
|
|
@@ -28,6 +28,17 @@ export class DependencyResolver {
|
|
|
28
28
|
* @returns {string|null} Resolved absolute file path location on disk, or null if external/third-party node_module
|
|
29
29
|
*/
|
|
30
30
|
resolveModulePath(containingFile, importSpecifier) {
|
|
31
|
+
// Challenge #16: Ignore built-in Node.js modules (fs, path, etc.)
|
|
32
|
+
if (importSpecifier.startsWith('node:') || [
|
|
33
|
+
'assert', 'async_hooks', 'buffer', 'child_process', 'cluster', 'console', 'constants',
|
|
34
|
+
'crypto', 'dgram', 'dns', 'domain', 'events', 'fs', 'fs/promises', 'http', 'http2',
|
|
35
|
+
'https', 'inspector', 'module', 'net', 'os', 'path', 'perf_hooks', 'process',
|
|
36
|
+
'punycode', 'querystring', 'readline', 'repl', 'stream', 'string_decoder',
|
|
37
|
+
'timers', 'tls', 'trace_events', 'tty', 'url', 'util', 'v8', 'vm', 'worker_threads', 'zlib'
|
|
38
|
+
].includes(importSpecifier)) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
31
42
|
const containingDir = path.dirname(containingFile);
|
|
32
43
|
|
|
33
44
|
// Rule A: Intercept and resolve local monorepo workspace cross-links
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
|
|
3
|
+
// Native sub-directory crawling removed as it's not in fs/promises in older node, but we use readdir anyway.
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Monorepo Cross-Linking Topology Manager
|