pkg-scaffold 2.2.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/package.json CHANGED
@@ -1,11 +1,16 @@
1
1
  {
2
2
  "name": "pkg-scaffold",
3
- "version": "2.2.0",
4
- "description": "Zero-config workspace initializer with advanced dependency intelligence: detects ghost dependencies (used but undeclared), orphaned packages (declared but unused), unused imports with file locations, deprecated packages, hardcoded secrets, and more.",
3
+ "version": "2.4.0",
4
+ "description": "An advanced, AST-driven dependency resolution, refactoring, and self-healing engine.",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "bin": {
8
- "pkg-scaffold": "./index.js"
8
+ "pkg-scaffold": "./bin/cli.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node bin/cli.js",
12
+ "test": "echo \"Error: no test specified\" && exit 0",
13
+ "test:stability": "npm run test"
9
14
  },
10
15
  "keywords": [
11
16
  "scaffold",
@@ -35,8 +40,15 @@
35
40
  },
36
41
  "homepage": "https://github.com/DreamLongYT/pkg-scaffold#readme",
37
42
  "dependencies": {
38
- "acorn": "^8.17.0",
39
- "acorn-walk": "^8.3.5",
40
- "npm-deprecated-check": "^1.4.0"
43
+ "ansis": "^3.0.0",
44
+ "commander": "^12.0.0",
45
+ "enhanced-resolve": "^5.16.0",
46
+ "execa": "^8.0.1",
47
+ "ramda": "^0.29.1",
48
+ "typescript": "^5.4.5",
49
+ "yocto-spinner": "^0.1.0"
50
+ },
51
+ "engines": {
52
+ "node": ">=18.0.0"
41
53
  }
42
- }
54
+ }
@@ -0,0 +1,282 @@
1
+ /**
2
+ * ============================================================================
3
+ * 📦 pkg-scaffold v3.0.0: Enterprise In-Memory Codebase State Manifest
4
+ * ============================================================================
5
+ * Implements a high-density, centralized graph database context for tracking
6
+ * software engineering debt, dependencies, types, and vulnerabilities.
7
+ */
8
+
9
+ import path from 'path';
10
+ import fs from 'fs/promises';
11
+
12
+ /**
13
+ * High-Fidelity Graph Element Node representing a single file asset boundary.
14
+ */
15
+ export class GraphNode {
16
+ constructor(filePath) {
17
+ this.filePath = path.normalize(filePath);
18
+ this.contentHash = '';
19
+ this.isLibraryEntry = false;
20
+ this.isFrameworkContract = false;
21
+ this.scriptKind = 0; // Ambient enum mapping
22
+
23
+ // Explicit and Computed Dynamic Syntax Boundaries
24
+ this.explicitImports = new Set();
25
+ this.dynamicImports = new Set();
26
+ this.importedSymbols = new Set(); // Format: 'specifier:symbol' or 'specifier:*'
27
+
28
+ // Internal API Exposed Interfaces (Symbol Name -> ExportMetadata)
29
+ this.internalExports = new Map();
30
+ this.typeOnlyExports = new Set();
31
+
32
+ // Semantic Reference Verification Registries
33
+ this.instantiatedIdentifiers = new Set();
34
+ this.rawStringReferences = new Set();
35
+ this.propertyAccessChains = new Set();
36
+
37
+ // Dependency Mesh Connection Maps
38
+ this.incomingEdges = new Set(); // Set of absolute filePaths depending on this component
39
+ this.outgoingEdges = new Set(); // Set of absolute internal filePaths this component calls
40
+
41
+ // Security & Compliance Anomaly Matrices
42
+ this.securityThreats = [];
43
+ this.calculatedDynamicImports = [];
44
+ this.localSuppressedRules = new Set();
45
+
46
+ // Detailed AST Location Diagnostics (Symbol -> Structural Location Mapping)
47
+ this.symbolSourceLocations = new Map(); // Symbol -> { line: number, column: number, length: number }
48
+ }
49
+
50
+ /**
51
+ * Evaluates if a specific exposed symbol token is utilized by any incoming edges.
52
+ * Leverages precise syntax identity collections.
53
+ */
54
+ isSymbolReferencedExternally(symbolName, projectGraph) {
55
+ if (this.isLibraryEntry) return true;
56
+
57
+ for (const parentPath of this.incomingEdges) {
58
+ const parentNode = projectGraph.get(parentPath);
59
+ if (!parentNode) continue;
60
+
61
+ // Direct identity reference check
62
+ if (parentNode.instantiatedIdentifiers.has(symbolName)) return true;
63
+
64
+ // Property lookup reference checks (e.g., config.databaseUrl)
65
+ for (const accessChain of parentNode.propertyAccessChains) {
66
+ if (accessChain.endsWith(`.${symbolName}`) || accessChain.includes(`.${symbolName}.`)) {
67
+ return true;
68
+ }
69
+ }
70
+
71
+ // Safe fallback lookup inside string reference caches (e.g., obj['databaseUrl'])
72
+ if (parentNode.rawStringReferences.has(symbolName)) return true;
73
+ }
74
+
75
+ return false;
76
+ }
77
+
78
+ /**
79
+ * Compiles complete localized diagnostic telemetry tracking metrics for this node instance.
80
+ */
81
+ compileNodeTelemetry() {
82
+ return {
83
+ path: this.filePath,
84
+ totalExplicitImportsCount: this.explicitImports.size,
85
+ totalExposedExportsCount: this.internalExports.size,
86
+ incomingDependenciesCount: this.incomingEdges.size,
87
+ outgoingDependenciesCount: this.outgoingEdges.size,
88
+ isDanglingOrphan: this.incomingEdges.size === 0 && !this.isLibraryEntry,
89
+ trackedThreatsCount: this.securityThreats.length
90
+ };
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Enterprise Engine Run State Registry & Suppression Context Matrix
96
+ */
97
+ export class EngineContext {
98
+ constructor(options = {}) {
99
+ this.cwd = path.normalize(options.cwd || process.cwd());
100
+ this.cacheDir = path.join(this.cwd, '.scaffold-cache');
101
+ this.ignoreFilePath = path.join(this.cwd, '.scaffold-ignore');
102
+ this.tsconfigFilename = options.tsconfig || 'tsconfig.json';
103
+ this.testCommand = options.testCommand || 'npm test';
104
+
105
+ this.allowAutoFix = options.autoFix ?? true;
106
+ this.isWorkspaceEnabled = options.workspace ?? false;
107
+ this.verbose = options.verbose ?? false;
108
+
109
+ // Core Memory Repositories
110
+ this.graph = new Map(); // Absolute File Path -> GraphNode
111
+ this.registryHashes = new Map(); // Package Name -> Secure Lockfile Signature String
112
+ this.globallyIgnoredSymbols = new Set();
113
+ this.globallyIgnoredPaths = [];
114
+ this.monorepoPackageRoots = new Set();
115
+
116
+ // Structural Heuristic Verification Metrics Tracker
117
+ this.metrics = {
118
+ startTime: 0,
119
+ endTime: 0,
120
+ totalFilesScanned: 0,
121
+ cacheHits: 0,
122
+ cacheMisses: 0,
123
+ prunedFilesCount: 0,
124
+ prunedExportsCount: 0,
125
+ totalSymbolsAnalyzed: 0,
126
+ securityVulnerabilitiesMitigated: 0
127
+ };
128
+ }
129
+
130
+ /**
131
+ * Initializes baseline context options, directory footprints, and suppression maps.
132
+ */
133
+ async initialize() {
134
+ this.metrics.startTime = Date.now();
135
+ await fs.mkdir(this.cacheDir, { recursive: true });
136
+ await this.compileIgnoreConfigurations();
137
+ }
138
+
139
+ /**
140
+ * Parses .scaffold-ignore layers using precise token segment matching
141
+ * instead of high-risk loose regex blocks.
142
+ */
143
+ async compileIgnoreConfigurations() {
144
+ try {
145
+ const content = await fs.readFile(this.ignoreFilePath, 'utf8');
146
+ const lines = content.split('\n');
147
+
148
+ for (let line of lines) {
149
+ line = line.trim();
150
+ if (!line || line.startsWith('#')) continue;
151
+
152
+ if (line.startsWith('export:')) {
153
+ const symbolToken = line.replace('export:', '').trim();
154
+ this.globallyIgnoredSymbols.add(symbolToken);
155
+ } else if (line.startsWith('path:')) {
156
+ const pathToken = line.replace('path:', '').trim();
157
+ this.globallyIgnoredPaths.push(path.normalize(pathToken));
158
+ } else {
159
+ // Standard structural path rule fallback
160
+ this.globallyIgnoredPaths.push(path.normalize(line));
161
+ }
162
+ }
163
+ } catch {
164
+ // Configuration optionally omitted; proceed with default execution flags
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Allocates or resolves a unified GraphNode reference inside our memory map index.
170
+ */
171
+ createNode(absoluteFilePath) {
172
+ const normalizedPath = path.normalize(absoluteFilePath);
173
+ if (this.graph.has(normalizedPath)) {
174
+ return this.graph.get(normalizedPath);
175
+ }
176
+ const node = new GraphNode(normalizedPath);
177
+ this.graph.set(normalizedPath, node);
178
+ return node;
179
+ }
180
+
181
+ /**
182
+ * Checks if an absolute file token path matches configuration ignore directives.
183
+ * Evaluates sub-path sequences exactly to prevent regular expression parsing drops.
184
+ */
185
+ isPathIgnored(absoluteFilePath) {
186
+ const relativeText = path.relative(this.cwd, absoluteFilePath);
187
+
188
+ for (const ignoredTarget of this.globallyIgnoredPaths) {
189
+ if (relativeText === ignoredTarget || relativeText.startsWith(path.join(ignoredTarget, path.sep))) {
190
+ return true;
191
+ }
192
+ // Handle explicit wildcard terminal indicators
193
+ if (ignoredTarget.endsWith('*')) {
194
+ const baseSegment = ignoredTarget.slice(0, -1);
195
+ if (relativeText.startsWith(baseSegment)) return true;
196
+ }
197
+ }
198
+ return false;
199
+ }
200
+
201
+ /**
202
+ * Processes the entire active dependency map to compile structural issue indices.
203
+ * Evaluates orphaned components, dead exports, and supply-chain threats.
204
+ */
205
+ generateSummaryReport() {
206
+ this.metrics.endTime = Date.now();
207
+ const durationSeconds = ((this.metrics.endTime - this.metrics.startTime) / 1000).toFixed(2);
208
+
209
+ const summary = {
210
+ executionDuration: `${durationSeconds}s`,
211
+ totalFilesProcessed: this.metrics.totalFilesScanned,
212
+ graphCacheOptimization: {
213
+ hits: this.metrics.cacheHits,
214
+ misses: this.metrics.cacheMisses,
215
+ ratio: this.metrics.totalFilesScanned > 0
216
+ ? `${((this.metrics.cacheHits / this.metrics.totalFilesScanned) * 100).toFixed(1)}%`
217
+ : '0%'
218
+ },
219
+ structuralIssuesDetected: {
220
+ deadFiles: [],
221
+ deadExports: [],
222
+ securityThreats: []
223
+ },
224
+ modificationsExecuted: {
225
+ filesUnlinked: this.metrics.prunedFilesCount,
226
+ exportsStripped: this.metrics.prunedExportsCount
227
+ }
228
+ };
229
+
230
+ for (const [filePath, node] of this.graph.entries()) {
231
+ // Skip package control files from standard structural dead-code checks
232
+ if (filePath.endsWith('package.json')) continue;
233
+ if (this.isPathIgnored(filePath)) continue;
234
+
235
+ const relativePath = path.relative(this.cwd, filePath);
236
+
237
+ // Category A: Completely orphaned components (no references, not library/framework entries)
238
+ if (node.incomingEdges.size === 0 && !node.isLibraryEntry && !node.isFrameworkContract) {
239
+ summary.structuralIssuesDetected.deadFiles.push(relativePath);
240
+ continue; // An orphaned file implies all internal sub-exports are dead; skip sub-checks
241
+ }
242
+
243
+ // Category B: Dead Named Exports inside active files
244
+ for (const [exportName, meta] of node.internalExports.entries()) {
245
+ this.metrics.totalSymbolsAnalyzed++;
246
+
247
+ // Skip entry configurations, global suppresses, and type-suppressed symbols
248
+ if (exportName === 'default' ||
249
+ this.globallyIgnoredSymbols.has(exportName) ||
250
+ node.localSuppressedRules.has(exportName)) {
251
+ continue;
252
+ }
253
+
254
+ if (!node.isSymbolReferencedExternally(exportName, this.graph)) {
255
+ const diagnosticLocation = node.symbolSourceLocations.get(exportName) || { line: 1, column: 1 };
256
+ summary.structuralIssuesDetected.deadExports.push({
257
+ file: relativePath,
258
+ symbol: exportName,
259
+ type: meta.type || 'named',
260
+ line: diagnosticLocation.line,
261
+ column: diagnosticLocation.column
262
+ });
263
+ }
264
+ }
265
+
266
+ // Category C: High-Entropy Password / Key Hardcode Vulnerabilities
267
+ if (node.securityThreats && node.securityThreats.length > 0) {
268
+ node.securityThreats.forEach(threat => {
269
+ summary.structuralIssuesDetected.securityThreats.push({
270
+ file: relativePath,
271
+ identifier: threat.variableKey,
272
+ riskCode: threat.riskCode || 'HIGH_RISK_SECRET_LEAK',
273
+ entropy: threat.entropyValue,
274
+ line: threat.line || 1
275
+ });
276
+ });
277
+ }
278
+ }
279
+
280
+ return summary;
281
+ }
282
+ }
@@ -0,0 +1,313 @@
1
+ import ts from 'typescript';
2
+ import fs from 'fs/promises';
3
+ import crypto from 'crypto';
4
+
5
+ /**
6
+ * Enterprise AST Syntax Walker & Feature Extractor
7
+ * Utilizes the official TypeScript Compiler infrastructure to execute deeply nested
8
+ * node classification without falling back to high-risk regular expression approximations.
9
+ */
10
+ export class ASTAnalyzer {
11
+ constructor(context) {
12
+ this.context = context;
13
+ // Standard high-entropy baseline selectors for AST variable tracking
14
+ this.entropyThreshold = 4.3;
15
+ }
16
+
17
+ /**
18
+ * Parses target file data into an isolated AST representation and populates metadata structures.
19
+ * @param {string} filePath - Absolute path to on-disk component
20
+ * @param {Object} fileNode - In-memory structural graph reference node
21
+ */
22
+ async processFile(filePath, fileNode) {
23
+ try {
24
+ const sourceText = await fs.readFile(filePath, 'utf8');
25
+
26
+ // Configure target extraction structures to parse TS, JSX, and modern TC39 specifications
27
+ const sourceFile = ts.createSourceFile(
28
+ filePath,
29
+ sourceText,
30
+ ts.ScriptTarget.Latest,
31
+ true, // Ensure parent pointers are bound to allow localized subtree walking
32
+ this.getScriptKind(filePath)
33
+ );
34
+
35
+ this.walkNode(sourceFile, sourceFile, fileNode);
36
+ this.extractTopLevelJSDocSuppreessions(sourceFile, fileNode);
37
+
38
+ return true;
39
+ } catch (parseError) {
40
+ if (this.context.verbose) {
41
+ console.error(`[AST Open Error] Failed compilation validation mapping on element: ${filePath}. Reason: ${parseError.message}`);
42
+ }
43
+ return false;
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Primary node walker loop executing atomic switch classifications.
49
+ * Challenge #7: Resolves conditional/destructured references to prevent cascading breakages.
50
+ */
51
+ walkNode(sourceFile, node, fileNode) {
52
+ if (!node) return;
53
+
54
+ switch (node.kind) {
55
+ // Handle Explicit Named or Absolute Star Namespace Imports
56
+ case ts.SyntaxKind.ImportDeclaration: {
57
+ if (node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
58
+ const specifier = node.moduleSpecifier.text;
59
+ fileNode.explicitImports.add(specifier);
60
+
61
+ if (node.importClause) {
62
+ // Trace named bounds: import { activeToken } from 'module';
63
+ if (node.importClause.namedBindings) {
64
+ if (ts.isNamedImports(node.importClause.namedBindings)) {
65
+ node.importClause.namedBindings.elements.forEach(element => {
66
+ const importedName = element.name.text;
67
+ const propertyName = element.propertyName ? element.propertyName.text : importedName;
68
+ fileNode.importedSymbols.add(`${specifier}:${propertyName}`);
69
+ });
70
+ } else if (ts.isNamespaceImport(node.importClause.namedBindings)) {
71
+ // Tracking total wildcard imports: import * as layout from 'module';
72
+ fileNode.importedSymbols.add(`${specifier}:*`);
73
+ }
74
+ }
75
+ // Trace default bounds: import React from 'react';
76
+ if (node.importClause.name) {
77
+ fileNode.importedSymbols.add(`${specifier}:default`);
78
+ }
79
+ }
80
+ }
81
+ break;
82
+ }
83
+
84
+ // Handle Explicit Namespace Requirements: import config = require('./config');
85
+ case ts.SyntaxKind.ImportEqualsDeclaration: {
86
+ if (node.moduleReference && ts.isExternalModuleReference(node.moduleReference)) {
87
+ if (node.moduleReference.expression && ts.isStringLiteral(node.moduleReference.expression)) {
88
+ fileNode.explicitImports.add(node.moduleReference.expression.text);
89
+ }
90
+ }
91
+ break;
92
+ }
93
+
94
+ // Challenge #1: Tracking dynamic expressions e.g., import('./chunks/' + variant)
95
+ case ts.SyntaxKind.CallExpression: {
96
+ if (node.expression && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
97
+ const firstArgument = node.arguments[0];
98
+ if (firstArgument) {
99
+ if (ts.isStringLiteral(firstArgument)) {
100
+ fileNode.explicitImports.add(firstArgument.text);
101
+ fileNode.dynamicImports.add(firstArgument.text);
102
+ } else {
103
+ // Deeply trace runtime calculated variables within the import parameters call
104
+ const stringPatterns = [];
105
+ this.traceStringExpressions(firstArgument, stringPatterns);
106
+ fileNode.calculatedDynamicImports.push({
107
+ rawText: firstArgument.getText(sourceFile),
108
+ heuristics: stringPatterns,
109
+ position: node.getStart(sourceFile)
110
+ });
111
+ }
112
+ }
113
+ }
114
+ break;
115
+ }
116
+
117
+ // Handle Variable Declaration Assignments & Challenge #11 (AST Secret Scanning)
118
+ case ts.SyntaxKind.VariableDeclaration: {
119
+ if (node.name && ts.isIdentifier(node.name)) {
120
+ this.auditAssignmentSafety(node.name.text, node.initializer, fileNode, sourceFile);
121
+ } else if (node.name && (ts.isObjectBindingPattern(node.name) || ts.isArrayBindingPattern(node.name))) {
122
+ // Flatten binding properties to map destructured usage accurately
123
+ node.name.elements.forEach(element => {
124
+ if (element.name && ts.isIdentifier(element.name)) {
125
+ this.auditAssignmentSafety(element.name.text, null, fileNode, sourceFile);
126
+ }
127
+ });
128
+ }
129
+ break;
130
+ }
131
+
132
+ // Handle Named Function Node Exports Matrix Configurations
133
+ case ts.SyntaxKind.FunctionDeclaration: {
134
+ if (node.name && ts.isIdentifier(node.name)) {
135
+ const name = node.name.text;
136
+ if (this.hasExportModifier(node)) {
137
+ fileNode.internalExports.set(name, { type: 'function', start: node.getStart(sourceFile), end: node.getEnd() });
138
+ }
139
+ }
140
+ break;
141
+ }
142
+
143
+ // Handle Structural Class Definitions and Class Export Signatures
144
+ case ts.SyntaxKind.ClassDeclaration: {
145
+ if (node.name && ts.isIdentifier(node.name)) {
146
+ const name = node.name.text;
147
+ if (this.hasExportModifier(node)) {
148
+ fileNode.internalExports.set(name, { type: 'class', start: node.getStart(sourceFile), end: node.getEnd() });
149
+ }
150
+ }
151
+ break;
152
+ }
153
+
154
+ // Handle Interface Definitions (Crucial for Challenge #10: Type Integrity Mapping)
155
+ case ts.SyntaxKind.InterfaceDeclaration: {
156
+ if (node.name && ts.isIdentifier(node.name)) {
157
+ const name = node.name.text;
158
+ if (this.hasExportModifier(node)) {
159
+ fileNode.internalExports.set(name, { type: 'interface', start: node.getStart(sourceFile), end: node.getEnd() });
160
+ }
161
+ }
162
+ break;
163
+ }
164
+
165
+ // Handle Type Invocations and Declarations Aliases
166
+ case ts.SyntaxKind.TypeAliasDeclaration: {
167
+ if (node.name && ts.isIdentifier(node.name)) {
168
+ const name = node.name.text;
169
+ if (this.hasExportModifier(node)) {
170
+ fileNode.internalExports.set(name, { type: 'type-alias', start: node.getStart(sourceFile), end: node.getEnd() });
171
+ }
172
+ }
173
+ break;
174
+ }
175
+
176
+ // Handle Explicit Export Assignments: export default baselineConfiguration;
177
+ case ts.SyntaxKind.ExportAssignment: {
178
+ const name = node.expression ? node.expression.getText(sourceFile) : 'default';
179
+ fileNode.internalExports.set('default', {
180
+ type: 'default-assignment',
181
+ referencedSymbol: name,
182
+ start: node.getStart(sourceFile),
183
+ end: node.getEnd()
184
+ });
185
+ break;
186
+ }
187
+
188
+ // Handle Arbitrary String References to catch deep framework routing or dynamic keys
189
+ case ts.SyntaxKind.StringLiteral: {
190
+ const text = node.text;
191
+ if (text.length > 2 && text.length < 120) {
192
+ fileNode.rawStringReferences.add(text);
193
+ }
194
+ break;
195
+ }
196
+
197
+ // Track general identifiers to register references to mapped import keys
198
+ case ts.SyntaxKind.Identifier: {
199
+ const idText = node.text;
200
+ // Avoid adding declarations to usage logs to keep verification accurate
201
+ if (!this.isNodeDeclarationName(node)) {
202
+ fileNode.instantiatedIdentifiers.add(idText);
203
+ }
204
+ break;
205
+ }
206
+ }
207
+
208
+ // Traverse recursively down the Node structural tree
209
+ ts.forEachChild(node, child => this.walkNode(sourceFile, child, fileNode));
210
+ }
211
+
212
+ /**
213
+ * Challenge #1: Evaluates math operations and template configurations inside dynamic imports.
214
+ */
215
+ traceStringExpressions(node, collector) {
216
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.PlusToken) {
217
+ this.traceStringExpressions(node.left, collector);
218
+ this.traceStringExpressions(node.right, collector);
219
+ } else if (ts.isStringLiteral(node)) {
220
+ collector.push({ type: 'literal', val: node.text });
221
+ } else if (ts.isTemplateExpression(node)) {
222
+ if (node.head) collector.push({ type: 'template-slice', val: node.head.text });
223
+ node.templateSpans.forEach(span => {
224
+ collector.push({ type: 'dynamic-var', val: span.expression.getText() });
225
+ if (span.literal) collector.push({ type: 'template-slice', val: span.literal.text });
226
+ });
227
+ } else {
228
+ collector.push({ type: 'computed-variable', val: node.getText() });
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Challenge #11: AST Secret Scanning. Evaluates entropy and patterns directly via assignments.
234
+ */
235
+ auditAssignmentSafety(variableName, initializer, fileNode, sourceFile) {
236
+ // Process variable mapping target indicators first
237
+ if (this.hasExportModifier(variableName?.parent)) {
238
+ fileNode.internalExports.set(variableName, { type: 'variable', start: variableName.parent.getStart(sourceFile), end: variableName.parent.getEnd() });
239
+ }
240
+
241
+ if (!initializer || !ts.isStringLiteral(initializer)) return;
242
+ const value = initializer.text;
243
+
244
+ // Challenge #11 Heuristic validation parameters matching variable patterns or contents values
245
+ const isSuspiciousKeyName = /api_?key|secret|token|password|auth_?token|private_?key/i.test(variableName);
246
+ const entropy = this.calculateShannonEntropy(value);
247
+
248
+ if ((isSuspiciousKeyName && value.length > 8) || (entropy > this.entropyThreshold && value.length > 16)) {
249
+ fileNode.securityThreats.push({
250
+ identifier: variableName,
251
+ entropy: parseFloat(entropy.toFixed(2)),
252
+ position: initializer.getStart(sourceFile),
253
+ riskCode: 'HIGH_RISK_SECRET_LEAK'
254
+ });
255
+ }
256
+ }
257
+
258
+ calculateShannonEntropy(str) {
259
+ const map = {};
260
+ for (let i = 0; i < str.length; i++) {
261
+ const char = str[i];
262
+ map[char] = (map[char] || 0) + 1;
263
+ }
264
+ let entropy = 0;
265
+ for (const char in map) {
266
+ const p = map[char] / str.length;
267
+ entropy -= p * Math.log2(p);
268
+ }
269
+ return entropy;
270
+ }
271
+
272
+ /**
273
+ * Challenge #18 & #8: Parse JSDoc suppression blocks right out of code statements.
274
+ */
275
+ extractTopLevelJSDocSuppreessions(sourceFile, fileNode) {
276
+ const fullText = sourceFile.text;
277
+ const commentRanges = ts.getLeadingCommentRanges(fullText, 0) || [];
278
+
279
+ for (const range of commentRanges) {
280
+ const comment = fullText.slice(range.pos, range.end);
281
+ const matches = comment.match(/@scaffold-suppress\s+([a-zA-Z0-9_\-*:]+)/g);
282
+ if (matches) {
283
+ matches.forEach(m => {
284
+ const directive = m.replace('@scaffold-suppress', '').trim();
285
+ fileNode.localSuppressedRules.add(directive);
286
+ });
287
+ }
288
+ }
289
+ }
290
+
291
+ hasExportModifier(node) {
292
+ if (!node || !node.modifiers) return false;
293
+ return node.modifiers.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword);
294
+ }
295
+
296
+ isNodeDeclarationName(node) {
297
+ const parent = node.parent;
298
+ if (!parent) return false;
299
+ if (ts.isVariableDeclaration(parent) && parent.name === node) return true;
300
+ if (ts.isFunctionDeclaration(parent) && parent.name === node) return true;
301
+ if (ts.isClassDeclaration(parent) && parent.name === node) return true;
302
+ if (ts.isInterfaceDeclaration(parent) && parent.name === node) return true;
303
+ if (ts.isImportSpecifier(parent) && parent.name === node) return true;
304
+ return false;
305
+ }
306
+
307
+ getScriptKind(filePath) {
308
+ if (filePath.endsWith('.ts')) return ts.ScriptKind.TS;
309
+ if (filePath.endsWith('.tsx')) return ts.ScriptKind.TSX;
310
+ if (filePath.endsWith('.jsx')) return ts.ScriptKind.JSX;
311
+ return ts.ScriptKind.JS;
312
+ }
313
+ }