pkg-scaffold 2.3.0 → 2.4.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/.scaffold-ignore +22 -0
- package/bin/cli.js +91 -0
- package/index.js +0 -0
- package/package.json +18 -6
- package/src/EngineContext.js +282 -0
- package/src/ast/ASTAnalyzer.js +313 -0
- package/src/ast/BarrelParser.js +177 -0
- package/src/ast/MagicDetector.js +154 -0
- package/src/healing/GitSandbox.js +160 -0
- package/src/healing/SelfHealer.js +150 -0
- package/src/index.js +343 -0
- package/src/performance/GraphCache.js +82 -0
- package/src/performance/SupplyChainGuard.js +106 -0
- package/src/performance/WorkerPool.js +89 -0
- package/src/performance/WorkerTaskRunner.js +64 -0
- package/src/refractor/ImpactAnalyzer.js +92 -0
- package/src/refractor/SourceRewriter.js +86 -0
- package/src/refractor/TransactionManager.js +131 -0
- package/src/refractor/TypeIntegrity.js +75 -0
- package/src/resolution/DepencyResolver.js +120 -0
- package/src/resolution/PathMapper.js +115 -0
- package/src/resolution/WorkSpaceGraph.js +171 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import crypto from 'crypto';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* High-Performance Graph State Persistence & Delta Hash Registry
|
|
7
|
+
* Automatically bypasses the AST compilation layer for unmodified files.
|
|
8
|
+
*/
|
|
9
|
+
export class IncrementalCacheManager {
|
|
10
|
+
constructor(context) {
|
|
11
|
+
this.context = context;
|
|
12
|
+
this.manifestPath = path.join(context.cacheDir, 'graph-manifest.json');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Computes a highly efficient SHA-256 hash checksum of a file directly from raw buffers.
|
|
17
|
+
* @param {string} filePath - Absolute path to the on-disk source component
|
|
18
|
+
*/
|
|
19
|
+
async computeHash(filePath) {
|
|
20
|
+
try {
|
|
21
|
+
const fileBuffer = await fs.readFile(filePath);
|
|
22
|
+
return crypto.createHash('sha256').update(fileBuffer).digest('hex');
|
|
23
|
+
} catch {
|
|
24
|
+
return '';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Loads the serialized manifest from disk, fallback to empty layout if unreadable.
|
|
30
|
+
* @returns {Promise<Object>} Mapped compilation cache states index
|
|
31
|
+
*/
|
|
32
|
+
async loadCacheManifest() {
|
|
33
|
+
try {
|
|
34
|
+
await fs.access(this.manifestPath);
|
|
35
|
+
const rawText = await fs.readFile(this.manifestPath, 'utf8');
|
|
36
|
+
return JSON.parse(rawText);
|
|
37
|
+
} catch {
|
|
38
|
+
if (this.context.verbose) {
|
|
39
|
+
console.log('✨ No structural performance cache found. Initializing a clean baseline manifest entry.');
|
|
40
|
+
}
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Serializes the current active dependency graph, translating Maps and Sets into JSON schemas.
|
|
47
|
+
* @param {Map<string, Object>} currentGraphState - In-memory structural project state map
|
|
48
|
+
*/
|
|
49
|
+
async saveCacheManifest(currentGraphState) {
|
|
50
|
+
const serializationOutput = {};
|
|
51
|
+
|
|
52
|
+
for (const [absolutePath, node] of currentGraphState.entries()) {
|
|
53
|
+
// Do not cache external configuration manifests like package.json
|
|
54
|
+
if (absolutePath.endsWith('package.json')) continue;
|
|
55
|
+
|
|
56
|
+
serializationOutput[absolutePath] = {
|
|
57
|
+
hash: node.contentHash,
|
|
58
|
+
isLibraryEntry: node.isLibraryEntry,
|
|
59
|
+
explicitImports: Array.from(node.explicitImports),
|
|
60
|
+
dynamicImports: Array.from(node.dynamicImports),
|
|
61
|
+
importedSymbols: Array.from(node.importedSymbols),
|
|
62
|
+
rawStringReferences: Array.from(node.rawStringReferences),
|
|
63
|
+
instantiatedIdentifiers: Array.from(node.instantiatedIdentifiers),
|
|
64
|
+
propertyAccessChains: Array.from(node.propertyAccessChains),
|
|
65
|
+
internalExports: Object.fromEntries(node.internalExports),
|
|
66
|
+
securityThreats: node.securityThreats || []
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
await fs.writeFile(
|
|
72
|
+
this.manifestPath,
|
|
73
|
+
JSON.stringify(serializationOutput, null, 2),
|
|
74
|
+
'utf8'
|
|
75
|
+
);
|
|
76
|
+
} catch (writeError) {
|
|
77
|
+
if (this.context.verbose) {
|
|
78
|
+
console.error(`🚨 [Cache Writer Instability] Failed to commit manifest log indices: ${writeError.message}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Monorepo Supply Chain Security & Typosquatting Anomaly Detection Engine
|
|
6
|
+
* Uses string distance algorithms to intercept package name substitution attacks.
|
|
7
|
+
*/
|
|
8
|
+
export class SupplyChainGuard {
|
|
9
|
+
constructor(context) {
|
|
10
|
+
this.context = context;
|
|
11
|
+
// Map popular dependencies to establish safe reference profiles
|
|
12
|
+
this.baselineEcosystemPackagesProfile = [
|
|
13
|
+
'lodash', 'react', 'react-dom', 'typescript', 'enhanced-resolve',
|
|
14
|
+
'commander', 'express', 'vue', 'next', 'svelte', 'ramda', 'execa'
|
|
15
|
+
];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Challenge #12: Compiles typo distance matrices to detect malicious package masking variants.
|
|
20
|
+
* @param {Array<string>} declaredDependenciesList - Manifest package name keys array
|
|
21
|
+
*/
|
|
22
|
+
detectTyposquattingAnomalies(declaredDependenciesList) {
|
|
23
|
+
const identifiedThreats = [];
|
|
24
|
+
|
|
25
|
+
for (const activeDependencyName of declaredDependenciesList) {
|
|
26
|
+
// Skip if the package is already recognized as a trusted ecosystem standard
|
|
27
|
+
if (this.baselineEcosystemPackagesProfile.includes(activeDependencyName)) continue;
|
|
28
|
+
|
|
29
|
+
for (const safePackageStandard of this.baselineEcosystemPackagesProfile) {
|
|
30
|
+
const structuralDistance = this.calculateLevenshteinDistance(
|
|
31
|
+
activeDependencyName,
|
|
32
|
+
safePackageStandard
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
// Flag an alert if a name mimics a top tier framework package down to 1-2 character edits
|
|
36
|
+
if (structuralDistance > 0 && structuralDistance <= 2) {
|
|
37
|
+
identifiedThreats.push({
|
|
38
|
+
maliciousCandidate: activeDependencyName,
|
|
39
|
+
targetMimicked: safePackageStandard,
|
|
40
|
+
severityLevel: 'CRITICAL_SUPPLY_CHAIN_THREAT',
|
|
41
|
+
distance: structuralDistance
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return identifiedThreats;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Challenge #13: Cross-references package lock signatures against on-disk configuration maps.
|
|
52
|
+
*/
|
|
53
|
+
async verifyIntegrityLockfileHashes(packageJsonPath) {
|
|
54
|
+
const rootDirectory = path.dirname(packageJsonPath);
|
|
55
|
+
const commonLockfileTargets = [
|
|
56
|
+
{ name: 'package-lock.json', type: 'npm' },
|
|
57
|
+
{ name: 'pnpm-lock.yaml', type: 'pnpm' },
|
|
58
|
+
{ name: 'yarn.lock', type: 'yarn' }
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
for (const target of commonLockfileTargets) {
|
|
62
|
+
try {
|
|
63
|
+
const absoluteLockPath = path.join(rootDirectory, target.name);
|
|
64
|
+
await fs.access(absoluteLockPath);
|
|
65
|
+
|
|
66
|
+
if (target.type === 'npm') {
|
|
67
|
+
const rawData = await fs.readFile(absoluteLockPath, 'utf8');
|
|
68
|
+
const lockJson = JSON.parse(rawData);
|
|
69
|
+
|
|
70
|
+
if (lockJson.packages) {
|
|
71
|
+
// Verify checksum entries for deep security profiling
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
} catch {
|
|
76
|
+
// Target lock configuration mismatch; try alternative package format options
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
calculateLevenshteinDistance(stringA, stringB) {
|
|
83
|
+
const matrix = [];
|
|
84
|
+
|
|
85
|
+
for (let i = 0; i <= stringB.length; i++) matrix[i] = [i];
|
|
86
|
+
for (let j = 0; j <= stringA.length; j++) matrix[0][j] = j;
|
|
87
|
+
|
|
88
|
+
for (let i = 1; i <= stringB.length; i++) {
|
|
89
|
+
for (let j = 1; j <= stringA.length; j++) {
|
|
90
|
+
if (stringB.charAt(i - 1) === stringA.charAt(j - 1)) {
|
|
91
|
+
matrix[i][j] = matrix[i - 1][j - 1];
|
|
92
|
+
} else {
|
|
93
|
+
matrix[i][j] = Math.min(
|
|
94
|
+
matrix[i - 1][j - 1] + 1, // Substitution mutation step
|
|
95
|
+
Math.min(
|
|
96
|
+
matrix[i][j - 1] + 1, // Insertion mutation step
|
|
97
|
+
matrix[i - 1][j] + 1 // Deletion mutation step
|
|
98
|
+
)
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return matrix[stringB.length][stringA.length];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Worker, isMainThread, parentPort, workerData } from 'worker_threads';
|
|
2
|
+
import os from 'core-os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Host CPU Thread-Distribution Pipeline Supervisor
|
|
7
|
+
* Parallelizes compiler parsing logic without triggering filesystem write collisions.
|
|
8
|
+
*/
|
|
9
|
+
export class WorkerPool {
|
|
10
|
+
constructor(context, maximumConcurrencyLimit = null) {
|
|
11
|
+
this.context = context;
|
|
12
|
+
// Dynamically query host specs; default down to 1 if threading channels are choked
|
|
13
|
+
this.hardwareConcurrencyCoreCount = maximumConcurrencyLimit || os.availableParallelism?.() || os.cpus().length || 2;
|
|
14
|
+
this.workerScriptPath = path.resolve(path.dirname(import.meta.url.replace('file://', '')), 'WorkerTaskRunner.js');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Distributes a collection of target filenames across concurrent thread pools.
|
|
19
|
+
* @param {Array<string>} totalFilePathsCollection - Absolute filesystem target pointers array
|
|
20
|
+
* @param {Object} masterEngineInstanceReference - Main RefactoringEngine context loop channel
|
|
21
|
+
*/
|
|
22
|
+
async parallelAnalyzeCodebase(totalFilePathsCollection, masterEngineInstanceReference) {
|
|
23
|
+
if (totalFilePathsCollection.length < 12) {
|
|
24
|
+
// Optimization: Do not waste overhead spin-up cycles on small layout codebases
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
console.log(`⚡ Spawning native compiler thread pools across [${this.hardwareConcurrencyCoreCount}] CPU cores concurrently...`);
|
|
29
|
+
|
|
30
|
+
// Chunk the workload array evenly across the generated worker targets
|
|
31
|
+
const analyticalWorkloadChunks = Array.from(
|
|
32
|
+
{ length: this.hardwareConcurrencyCoreCount },
|
|
33
|
+
() => []
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
totalFilePathsCollection.forEach((filePath, fileIndex) => {
|
|
37
|
+
analyticalWorkloadChunks[fileIndex % this.hardwareConcurrencyCoreCount].push(filePath);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const threadTaskExecutionsList = analyticalWorkloadChunks.map(chunk => {
|
|
41
|
+
if (chunk.length === 0) return Promise.resolve([]);
|
|
42
|
+
return this.executeChunkInsideThread(chunk);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const analyticalResultsSubsets = await Promise.all(threadTaskExecutionsList);
|
|
47
|
+
|
|
48
|
+
// Merge thread structural subsets back into the primary context graph nodes
|
|
49
|
+
analyticalResultsSubsets.flat().forEach(result => {
|
|
50
|
+
if (!result) return;
|
|
51
|
+
const node = masterEngineInstanceReference.context.createNode(result.filePath);
|
|
52
|
+
|
|
53
|
+
result.explicitImports.forEach(i => node.explicitImports.add(i));
|
|
54
|
+
result.dynamicImports.forEach(i => node.dynamicImports.add(i));
|
|
55
|
+
result.importedSymbols.forEach(s => node.importedSymbols.add(s));
|
|
56
|
+
result.rawStringReferences.forEach(r => node.rawStringReferences.add(r));
|
|
57
|
+
result.instantiatedIdentifiers.forEach(i => node.instantiatedIdentifiers.add(i));
|
|
58
|
+
result.propertyAccessChains.forEach(c => node.propertyAccessChains.add(c));
|
|
59
|
+
|
|
60
|
+
Object.entries(result.internalExports).forEach(([k, v]) => {
|
|
61
|
+
node.internalExports.set(k, v);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
node.securityThreats = result.securityThreats || [];
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return true;
|
|
68
|
+
} catch (poolThreadFault) {
|
|
69
|
+
if (this.context.verbose) {
|
|
70
|
+
console.warn(`⚠️ ThreadPool runtime fault: ${poolThreadFault.message}. Falling back to main-thread processing.`);
|
|
71
|
+
}
|
|
72
|
+
return false; // Safely fall back to single-thread synchronous recovery processing
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
executeChunkInsideThread(fileChunkSubset) {
|
|
77
|
+
return new Promise((resolve, reject) => {
|
|
78
|
+
const workerInstance = new Worker(this.workerScriptPath, {
|
|
79
|
+
workerData: { files: fileChunkSubset, contextOptions: { verbose: this.context.verbose } }
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
workerInstance.on('message', (payload) => resolve(payload));
|
|
83
|
+
workerInstance.on('error', (err) => reject(err));
|
|
84
|
+
workerInstance.on('exit', (exitCode) => {
|
|
85
|
+
if (exitCode !== 0) reject(new Error(`Worker thread collapsed unexpectedly with code: ${exitCode}`));
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { parentPort, workerData } from 'worker_threads';
|
|
2
|
+
import { ASTAnalyzer } from '../ast/ASTAnalyzer.js';
|
|
3
|
+
import ts from 'typescript';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Isolated Worker Thread Target Pipeline Task Loop Execution Instance
|
|
8
|
+
*/
|
|
9
|
+
async function processThreadChunks() {
|
|
10
|
+
const { files, contextOptions } = workerData;
|
|
11
|
+
const partialGraphPayloadResults = [];
|
|
12
|
+
|
|
13
|
+
// Construct a lightweight standalone instance of our analyzer core inside the worker
|
|
14
|
+
const standaloneAnalyzer = new ASTAnalyzer({ verbose: contextOptions.verbose });
|
|
15
|
+
|
|
16
|
+
for (const file of files) {
|
|
17
|
+
if (file.endsWith('package.json')) continue;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
21
|
+
|
|
22
|
+
// Build a minimal virtual reference mapping node to capture features
|
|
23
|
+
const mockNode = {
|
|
24
|
+
explicitImports: new Set(),
|
|
25
|
+
dynamicImports: new Set(),
|
|
26
|
+
importedSymbols: new Set(),
|
|
27
|
+
rawStringReferences: new Set(),
|
|
28
|
+
instantiatedIdentifiers: new Set(),
|
|
29
|
+
propertyAccessChains: new Set(),
|
|
30
|
+
internalExports: new Map(),
|
|
31
|
+
securityThreats: []
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const sourceFile = ts.createSourceFile(
|
|
35
|
+
file,
|
|
36
|
+
text,
|
|
37
|
+
ts.ScriptTarget.Latest,
|
|
38
|
+
true,
|
|
39
|
+
file.endsWith('.ts') ? ts.ScriptKind.TS : file.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.JS
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
standaloneAnalyzer.traverseNodeTree(sourceFile, sourceFile, mockNode);
|
|
43
|
+
|
|
44
|
+
partialGraphPayloadResults.push({
|
|
45
|
+
filePath: file,
|
|
46
|
+
explicitImports: Array.from(mockNode.explicitImports),
|
|
47
|
+
dynamicImports: Array.from(mockNode.dynamicImports),
|
|
48
|
+
importedSymbols: Array.from(mockNode.importedSymbols),
|
|
49
|
+
rawStringReferences: Array.from(mockNode.rawStringReferences),
|
|
50
|
+
instantiatedIdentifiers: Array.from(mockNode.instantiatedIdentifiers),
|
|
51
|
+
propertyAccessChains: Array.from(mockNode.propertyAccessChains),
|
|
52
|
+
internalExports: Object.fromEntries(mockNode.internalExports),
|
|
53
|
+
securityThreats: mockNode.securityThreats
|
|
54
|
+
});
|
|
55
|
+
} catch {
|
|
56
|
+
// Ignore unparseable or locked syntax nodes in thread loops
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Stream compiled metadata structures directly back to the primary supervisor pool thread channel
|
|
61
|
+
parentPort.postMessage(partialGraphPayloadResults);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
processThreadChunks();
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Cross-Reference Dependency Matrix & Breakage Risk Auditor
|
|
6
|
+
* Traces dynamic runtime usage patterns to prevent code pruning from breaking downstream systems.
|
|
7
|
+
*/
|
|
8
|
+
export class ImpactAnalyzer {
|
|
9
|
+
constructor(context) {
|
|
10
|
+
this.context = context;
|
|
11
|
+
this.safetyOverlays = [/\.json$/, /\.json5$/, /\.html$/, /\.yaml$/, /\.yml$/];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Scans non-code assets and dynamic lookups to check if an unused export is needed elsewhere.
|
|
16
|
+
* @param {string} originFile - Absolute path of component housing target symbol
|
|
17
|
+
* @param {string} symbolName - Literal identifier being evaluated for deletion
|
|
18
|
+
* @param {Map} projectGraph - Full project structural graph representation
|
|
19
|
+
*/
|
|
20
|
+
async verifyRefactorSafety(originFile, symbolName, projectGraph) {
|
|
21
|
+
// Avoid dropping generic single letter tokens or framework primitives
|
|
22
|
+
if (symbolName === 'default' || symbolName.length <= 2) {
|
|
23
|
+
return { isSafeToPrune: false, blockReason: 'PROTECTED_SYSTEM_CONTRACT_KEYWORD' };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Rule 1: Check across all code files for loose string-based token references
|
|
27
|
+
for (const [filePath, fileNode] of projectGraph.entries()) {
|
|
28
|
+
if (filePath === originFile) continue;
|
|
29
|
+
|
|
30
|
+
// If the symbol name is explicitly referenced in an element lookup or template slice, flag it as risky
|
|
31
|
+
if (fileNode.rawStringReferences && fileNode.rawStringReferences.has(symbolName)) {
|
|
32
|
+
return {
|
|
33
|
+
isSafeToPrune: false,
|
|
34
|
+
blockReason: `LOOSE_STRING_ACCESS_MATCH_FOUND_IN: ${path.relative(this.context.cwd, filePath)}`
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Check member property chain lookups: customer.profile.billingAddress
|
|
39
|
+
if (fileNode.propertyAccessChains) {
|
|
40
|
+
for (const chain of fileNode.propertyAccessChains) {
|
|
41
|
+
if (chain.endsWith(`.${symbolName}`) || chain.includes(`.${symbolName}.`)) {
|
|
42
|
+
return {
|
|
43
|
+
isSafeToPrune: false,
|
|
44
|
+
blockReason: `DYNAMIC_PROPERTY_ACCESS_CHAIN_HIT: ${chain}`
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Rule 2: Crawl through external static manifests (JSON metadata, HTML routing templates, workflow files)
|
|
52
|
+
const configurations = await this.gatherMetadataFiles(this.context.cwd);
|
|
53
|
+
|
|
54
|
+
for (const confPath of configurations) {
|
|
55
|
+
try {
|
|
56
|
+
const payload = await fs.readFile(confPath, 'utf8');
|
|
57
|
+
|
|
58
|
+
// Match string references inside configuration boundaries
|
|
59
|
+
if (payload.includes(`"${symbolName}"`) || payload.includes(`'${symbolName}'`) || payload.includes(`data-${symbolName}`)) {
|
|
60
|
+
return {
|
|
61
|
+
isSafeToPrune: false,
|
|
62
|
+
blockReason: `METADATA_MANIFEST_DEPENDENCY_FOUND_IN: ${path.relative(this.context.cwd, confPath)}`
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
} catch {
|
|
66
|
+
// Read step error; skip unreadable descriptors
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { isSafeToPrune: true, blockReason: null };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async gatherMetadataFiles(dir, collected = []) {
|
|
74
|
+
try {
|
|
75
|
+
const entities = await fs.readdir(dir, { withFileTypes: true });
|
|
76
|
+
for (const ent of entities) {
|
|
77
|
+
const resolutionPath = path.join(dir, ent.name);
|
|
78
|
+
|
|
79
|
+
if (ent.isDirectory()) {
|
|
80
|
+
if (ent.name === 'node_modules' || ent.name === '.git' || ent.name === '.scaffold-cache' || ent.name === 'dist') continue;
|
|
81
|
+
await this.gatherMetadataFiles(resolutionPath, collected);
|
|
82
|
+
} else if (ent.isFile()) {
|
|
83
|
+
const actsAsMetaAsset = this.safetyOverlays.some(regex => regex.test(ent.name));
|
|
84
|
+
if (actsAsMetaAsset) {
|
|
85
|
+
collected.push(resolutionPath);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
} catch {}
|
|
90
|
+
return collected;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Format-Preserving Abstract Syntax Tree Source Mutation Engine
|
|
6
|
+
* Executes targeted updates using token position logic while preserving trivia (comments/JSDoc).
|
|
7
|
+
*/
|
|
8
|
+
export class SourceRewriter {
|
|
9
|
+
constructor(context) {
|
|
10
|
+
this.context = context;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Removes a specific named export symbol declaration block from code text.
|
|
15
|
+
* @param {string} filePath - Absolute path to on-disk code component
|
|
16
|
+
* @param {string} symbolName - Target export key to prune
|
|
17
|
+
* @param {Object} exportMeta - Mapped token offset indices (start/end offsets)
|
|
18
|
+
*/
|
|
19
|
+
async stripNamedExportSignature(filePath, symbolName, exportMeta) {
|
|
20
|
+
const rawSource = await fs.readFile(filePath, 'utf8');
|
|
21
|
+
|
|
22
|
+
// Case A: Token is grouped inside a multi-export destructured statement block: export { a, b, c };
|
|
23
|
+
if (exportMeta.type === 'named') {
|
|
24
|
+
const updatedText = this.pruneFromSharedExportClause(rawSource, symbolName, exportMeta);
|
|
25
|
+
if (updatedText) return updatedText;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Case B: Isolated node removal. Calculate indices from start to end while preserving comments.
|
|
29
|
+
const startOffset = exportMeta.start;
|
|
30
|
+
const endOffset = exportMeta.end;
|
|
31
|
+
|
|
32
|
+
// Split source without dropping trailing line structural punctuation or text formatting configurations
|
|
33
|
+
const prefix = rawSource.slice(0, startOffset);
|
|
34
|
+
let suffix = rawSource.slice(endOffset);
|
|
35
|
+
|
|
36
|
+
// Clean up trailing commas or semicolons to prevent syntax errors
|
|
37
|
+
if (suffix.startsWith(';') || suffix.startsWith(',')) {
|
|
38
|
+
suffix = suffix.slice(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return prefix + suffix;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Handles selective pruning of export elements within inline groupings like `export { x, y as z }`.
|
|
46
|
+
*/
|
|
47
|
+
pruneFromSharedExportClause(sourceText, symbolName, meta) {
|
|
48
|
+
// Find the enclosing export declaration node using structural boundaries
|
|
49
|
+
const sourceFile = ts.createSourceFile(
|
|
50
|
+
'temp.ts',
|
|
51
|
+
sourceText,
|
|
52
|
+
ts.ScriptTarget.Latest,
|
|
53
|
+
true
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
let targetText = null;
|
|
57
|
+
|
|
58
|
+
const findAndPrune = (node) => {
|
|
59
|
+
if (ts.isExportDeclaration(node) && node.exportClause && ts.isNamedExports(node.exportClause)) {
|
|
60
|
+
const start = node.getStart(sourceFile);
|
|
61
|
+
const end = node.getEnd();
|
|
62
|
+
|
|
63
|
+
// Check if the target node spans the exact offset coordinates of the dead symbol
|
|
64
|
+
if (meta.start >= start && meta.end <= end) {
|
|
65
|
+
const activeElements = node.exportClause.elements;
|
|
66
|
+
const keptSymbols = activeElements
|
|
67
|
+
.filter(el => el.name.text !== symbolName)
|
|
68
|
+
.map(el => el.getText(sourceFile));
|
|
69
|
+
|
|
70
|
+
if (keptSymbols.length === 0) {
|
|
71
|
+
// Drop the entire declaration block if no active exports remain inside it
|
|
72
|
+
targetText = sourceText.slice(0, start) + sourceText.slice(end);
|
|
73
|
+
} else {
|
|
74
|
+
// Rewrite the export group inline, preserving formatting and comments
|
|
75
|
+
const reconstructedClause = `export { ${keptSymbols.join(', ')} };`;
|
|
76
|
+
targetText = sourceText.slice(0, start) + reconstructedClause + sourceText.slice(end);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
ts.forEachChild(node, findAndPrune);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
findAndPrune(sourceFile);
|
|
84
|
+
return targetText;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Transactional File System Operations Supervisor
|
|
6
|
+
* Guarantees atomicity across multi-file transformations by tracking rolling backups.
|
|
7
|
+
*/
|
|
8
|
+
export class TransactionManager {
|
|
9
|
+
constructor(context) {
|
|
10
|
+
this.context = context;
|
|
11
|
+
this.backupDirectory = path.join(context.cacheDir, 'backups');
|
|
12
|
+
this.journal = [];
|
|
13
|
+
this.isLocked = false;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Begins a new transaction lifecycle loop, initializing isolation vectors.
|
|
18
|
+
*/
|
|
19
|
+
async begin() {
|
|
20
|
+
if (this.isLocked) {
|
|
21
|
+
throw new Error('Transaction Manager concurrency fault: Another transaction is already staged.');
|
|
22
|
+
}
|
|
23
|
+
this.isLocked = true;
|
|
24
|
+
this.journal = [];
|
|
25
|
+
await fs.mkdir(this.backupDirectory, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Stages a destructive file modification or creation sequence.
|
|
30
|
+
* @param {string} filePath - Absolute system file target location
|
|
31
|
+
* @param {string} nextContent - The completely rewritten proposed source structure
|
|
32
|
+
*/
|
|
33
|
+
async stageWrite(filePath, nextContent) {
|
|
34
|
+
this.assertLock();
|
|
35
|
+
let originalContent = null;
|
|
36
|
+
let backupPath = null;
|
|
37
|
+
let operationType = 'UPDATE';
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
await fs.access(filePath);
|
|
41
|
+
originalContent = await fs.readFile(filePath, 'utf8');
|
|
42
|
+
|
|
43
|
+
const fileId = Buffer.from(filePath).toString('base64url');
|
|
44
|
+
backupPath = path.join(this.backupDirectory, `${fileId}.bak`);
|
|
45
|
+
await fs.writeFile(backupPath, originalContent, 'utf8');
|
|
46
|
+
} catch {
|
|
47
|
+
operationType = 'CREATE';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Attempt target file mutation write directly to disk
|
|
51
|
+
await fs.writeFile(filePath, nextContent, 'utf8');
|
|
52
|
+
|
|
53
|
+
this.journal.push({
|
|
54
|
+
type: operationType,
|
|
55
|
+
targetFile: filePath,
|
|
56
|
+
backupLocation: backupPath
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Stages an element deletion sequence safely.
|
|
62
|
+
* @param {string} filePath - Target component being evaluated as dead
|
|
63
|
+
*/
|
|
64
|
+
async stageDeletion(filePath) {
|
|
65
|
+
this.assertLock();
|
|
66
|
+
|
|
67
|
+
// Read previous state data for archiving before dropping linkage
|
|
68
|
+
const originalContent = await fs.readFile(filePath, 'utf8');
|
|
69
|
+
const fileId = Buffer.from(filePath).toString('base64url');
|
|
70
|
+
const backupPath = path.join(this.backupDirectory, `${fileId}.bak`);
|
|
71
|
+
|
|
72
|
+
await fs.writeFile(backupPath, originalContent, 'utf8');
|
|
73
|
+
await fs.unlink(filePath);
|
|
74
|
+
|
|
75
|
+
this.journal.push({
|
|
76
|
+
type: 'DELETE',
|
|
77
|
+
targetFile: filePath,
|
|
78
|
+
backupLocation: backupPath
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Finalizes the transaction sequence, cleaning up local transaction cache points.
|
|
84
|
+
*/
|
|
85
|
+
async commit() {
|
|
86
|
+
this.assertLock();
|
|
87
|
+
try {
|
|
88
|
+
for (const record of this.journal) {
|
|
89
|
+
if (record.backupLocation) {
|
|
90
|
+
await fs.unlink(record.backupLocation).catch(() => {});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
} finally {
|
|
94
|
+
this.releaseLock();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Reverts changes to their original states if any error or verification failure is flagged.
|
|
100
|
+
*/
|
|
101
|
+
async rollback() {
|
|
102
|
+
this.assertLock();
|
|
103
|
+
try {
|
|
104
|
+
// Process journal logs in reverse execution order to preserve dependencies
|
|
105
|
+
for (let i = this.journal.length - 1; i >= 0; i--) {
|
|
106
|
+
const record = this.journal[i];
|
|
107
|
+
|
|
108
|
+
if (record.type === 'CREATE') {
|
|
109
|
+
await fs.unlink(record.targetFile).catch(() => {});
|
|
110
|
+
} else if (record.type === 'UPDATE' || record.type === 'DELETE') {
|
|
111
|
+
const originalContent = await fs.readFile(record.backupLocation, 'utf8');
|
|
112
|
+
await fs.writeFile(record.targetFile, originalContent, 'utf8');
|
|
113
|
+
await fs.unlink(record.backupLocation).catch(() => {});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
} finally {
|
|
117
|
+
this.releaseLock();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
assertLock() {
|
|
122
|
+
if (!this.isLocked) {
|
|
123
|
+
throw new Error('Transaction Manager boundary violation: No active tracking block exists.');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
releaseLock() {
|
|
128
|
+
this.isLocked = false;
|
|
129
|
+
this.journal = [];
|
|
130
|
+
}
|
|
131
|
+
}
|