devintelligence 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/.env.example +14 -14
  2. package/LICENSE +22 -22
  3. package/README.md +146 -146
  4. package/bin/devintelligence.js +2481 -12
  5. package/bin/devintelligence.ts +244 -103
  6. package/dist/App.d.ts +3 -0
  7. package/dist/ai.js +366 -0
  8. package/dist/ai.js.map +7 -0
  9. package/dist/components/DeepAnalysisViews.d.ts +13 -0
  10. package/dist/components/MobileAndPlatformViews.d.ts +22 -0
  11. package/dist/components/OverviewAndExplorerViews.d.ts +20 -0
  12. package/dist/components/RepairAndGateViews.d.ts +25 -0
  13. package/dist/engine/aiEngine.d.ts +62 -0
  14. package/dist/engine/analyzerEngine.d.ts +19 -0
  15. package/dist/engine/repairEngine.d.ts +42 -0
  16. package/dist/engine.js +1541 -0
  17. package/dist/engine.js.map +7 -0
  18. package/dist/index.d.ts +10 -0
  19. package/dist/index.js +2344 -0
  20. package/dist/index.js.map +7 -0
  21. package/dist/main.d.ts +1 -0
  22. package/dist/repair.js +2052 -0
  23. package/dist/repair.js.map +7 -0
  24. package/dist/src/App.d.ts +3 -0
  25. package/dist/src/components/DeepAnalysisViews.d.ts +13 -0
  26. package/dist/src/components/MobileAndPlatformViews.d.ts +22 -0
  27. package/dist/src/components/OverviewAndExplorerViews.d.ts +20 -0
  28. package/dist/src/components/RepairAndGateViews.d.ts +25 -0
  29. package/dist/src/engine/aiEngine.d.ts +62 -0
  30. package/dist/src/engine/analyzerEngine.d.ts +19 -0
  31. package/dist/src/engine/repairEngine.d.ts +42 -0
  32. package/dist/src/index.d.ts +10 -0
  33. package/dist/src/main.d.ts +1 -0
  34. package/dist/src/types/devintelligence.d.ts +250 -0
  35. package/dist/types/devintelligence.d.ts +250 -0
  36. package/fixtures/django-analytics-service/requirements.txt +4 -4
  37. package/fixtures/django-analytics-service/settings.py +16 -16
  38. package/fixtures/express-api-gateway/package.json +14 -14
  39. package/fixtures/express-api-gateway/src/server.ts +16 -16
  40. package/fixtures/nextjs-saas-portal/package.json +21 -21
  41. package/fixtures/nextjs-saas-portal/src/app/dashboard/page.tsx +26 -26
  42. package/fixtures/rn-bare-fitness/android/build.gradle +19 -19
  43. package/fixtures/rn-bare-fitness/ios/Info.plist +14 -14
  44. package/fixtures/rn-bare-fitness/package.json +23 -23
  45. package/fixtures/rn-bare-fitness/src/WorkoutTracker.tsx +31 -31
  46. package/fixtures/rn-expo-commerce/android/app/build.gradle +20 -20
  47. package/fixtures/rn-expo-commerce/android/app/src/main/AndroidManifest.xml +21 -21
  48. package/fixtures/rn-expo-commerce/android/gradle.properties +6 -6
  49. package/fixtures/rn-expo-commerce/app.json +17 -17
  50. package/fixtures/rn-expo-commerce/babel.config.js +10 -10
  51. package/fixtures/rn-expo-commerce/ios/Podfile +13 -13
  52. package/fixtures/rn-expo-commerce/metro.config.js +7 -7
  53. package/fixtures/rn-expo-commerce/package.json +31 -31
  54. package/fixtures/rn-expo-commerce/src/components/LegacyPromoBanner.tsx +11 -11
  55. package/fixtures/rn-expo-commerce/src/screens/ProductFeedScreen.tsx +48 -48
  56. package/fixtures/rn-expo-commerce/src/services/api.ts +15 -15
  57. package/package.json +89 -77
  58. package/index.html +0 -21
  59. package/server.ts +0 -275
  60. package/src/App.tsx +0 -783
  61. package/src/components/DeepAnalysisViews.tsx +0 -526
  62. package/src/components/MobileAndPlatformViews.tsx +0 -625
  63. package/src/components/OverviewAndExplorerViews.tsx +0 -753
  64. package/src/components/RepairAndGateViews.tsx +0 -814
  65. package/src/engine/aiEngine.test.ts +0 -86
  66. package/src/engine/aiEngine.ts +0 -346
  67. package/src/engine/analyzerEngine.test.ts +0 -110
  68. package/src/engine/analyzerEngine.ts +0 -1579
  69. package/src/engine/repairEngine.ts +0 -545
  70. package/src/index.css +0 -44
  71. package/src/index.ts +0 -59
  72. package/src/main.tsx +0 -5
  73. package/src/types/devintelligence.ts +0 -310
  74. package/tsconfig.json +0 -28
  75. package/vite.config.ts +0 -22
package/dist/index.js ADDED
@@ -0,0 +1,2344 @@
1
+ // src/engine/analyzerEngine.ts
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import os from "node:os";
5
+ import { execSync } from "node:child_process";
6
+ var cachedHostEnvironment = null;
7
+ function contentContainsSnippet(fileContent, snippet) {
8
+ if (fileContent.includes(snippet)) return true;
9
+ const normalizedFile = fileContent.replace(/\r\n/g, "\n");
10
+ const normalizedSnippet = snippet.replace(/\r\n/g, "\n");
11
+ return normalizedFile.includes(normalizedSnippet);
12
+ }
13
+ function safeExecVersion(cmd) {
14
+ try {
15
+ const out = execSync(cmd, {
16
+ encoding: "utf8",
17
+ timeout: 1500,
18
+ stdio: ["ignore", "pipe", "pipe"]
19
+ }).trim();
20
+ return out.split("\n")[0] || null;
21
+ } catch (err) {
22
+ if (err && err.stderr) {
23
+ const errStr = String(err.stderr).trim();
24
+ if (errStr && !errStr.includes("not recognized") && !errStr.includes("not found") && !errStr.includes("No such file") && !errStr.includes("cannot find")) {
25
+ return errStr.split("\n")[0] || null;
26
+ }
27
+ }
28
+ return null;
29
+ }
30
+ }
31
+ function inspectHostEnvironment(forceRefresh = false) {
32
+ if (cachedHostEnvironment && !forceRefresh) {
33
+ return cachedHostEnvironment;
34
+ }
35
+ const platform = process.platform;
36
+ const isWin = platform === "win32";
37
+ const isMac = platform === "darwin";
38
+ const nodeVer = process.version;
39
+ const npmVer = safeExecVersion("npm -v");
40
+ const bunVer = safeExecVersion("bun -v");
41
+ const yarnVer = safeExecVersion("yarn -v");
42
+ const pnpmVer = safeExecVersion("pnpm -v");
43
+ const gitVer = safeExecVersion("git --version");
44
+ const pythonVer = safeExecVersion("python3 --version") || safeExecVersion("python --version");
45
+ const javaVer = safeExecVersion("java -version 2>&1") || safeExecVersion("java -version");
46
+ const adbVer = safeExecVersion("adb version");
47
+ const gradleVer = safeExecVersion("gradle -version");
48
+ const androidHome = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT || "";
49
+ const npmRemediation = isWin ? "winget install OpenJS.NodeJS.LTS" : isMac ? "brew install node" : "curl -fsSL https://deb.nodesource.com/setup_20.x | sudo bash - && sudo apt-get install -y nodejs";
50
+ const bunRemediation = isWin ? 'powershell -c "irm bun.sh/install.ps1 | iex"' : "curl -fsSL https://bun.sh/install | bash";
51
+ const pythonRemediation = isWin ? "winget install Python.Python.3.12" : isMac ? "brew install python" : "sudo apt-get update && sudo apt-get install -y python3 python3-pip";
52
+ const jdkRemediation = isWin ? "winget install Microsoft.OpenJDK.17" : isMac ? "brew install openjdk@17" : "sudo apt-get update && sudo apt-get install -y openjdk-17-jdk";
53
+ const androidHomeRemediation = isWin ? '[System.Environment]::SetEnvironmentVariable("ANDROID_HOME", "$env:LOCALAPPDATA\\Android\\Sdk", "User")' : isMac ? "export ANDROID_HOME=$HOME/Library/Android/sdk && export PATH=$PATH:$ANDROID_HOME/platform-tools" : "export ANDROID_HOME=$HOME/Android/Sdk && export PATH=$PATH:$ANDROID_HOME/platform-tools";
54
+ const adbRemediation = isWin ? "winget install Google.PlatformTools" : isMac ? "brew install android-platform-tools" : "sudo apt-get update && sudo apt-get install -y adb";
55
+ const gitRemediation = isWin ? "winget install Git.Git" : isMac ? "brew install git" : "sudo apt-get update && sudo apt-get install -y git";
56
+ const checks = [
57
+ {
58
+ id: "env-node",
59
+ name: "Node.js Runtime",
60
+ category: "runtime",
61
+ status: "working",
62
+ detectedVersion: nodeVer,
63
+ requiredVersion: ">= 18.18.0 (LTS)",
64
+ osPlatform: platform,
65
+ explanation: `Node.js ${nodeVer} is active and meets React Native 0.74+ and Next.js 14+ engine requirements.`
66
+ },
67
+ {
68
+ id: "env-npm",
69
+ name: "npm Package Manager",
70
+ category: "package-manager",
71
+ status: npmVer ? "working" : "not_installed",
72
+ detectedVersion: npmVer ? `v${npmVer}` : "Not installed",
73
+ requiredVersion: ">= 9.0.0",
74
+ osPlatform: platform,
75
+ explanation: npmVer ? `npm v${npmVer} is available for lockfile resolution and tarball packaging.` : "npm was not found in PATH.",
76
+ remediationCommand: npmVer ? void 0 : npmRemediation
77
+ },
78
+ {
79
+ id: "env-bun",
80
+ name: "Bun Runtime & Package Manager",
81
+ category: "package-manager",
82
+ status: bunVer ? "working" : "not_installed",
83
+ detectedVersion: bunVer ? `v${bunVer}` : "Not installed",
84
+ requiredVersion: ">= 1.0.0",
85
+ osPlatform: platform,
86
+ explanation: bunVer ? `Bun v${bunVer} is installed and ready for fast dependency installation and script execution.` : "Bun is optional and not currently installed.",
87
+ remediationCommand: bunVer ? void 0 : bunRemediation
88
+ },
89
+ {
90
+ id: "env-yarn-pnpm",
91
+ name: "Yarn / pnpm Workspaces",
92
+ category: "package-manager",
93
+ status: yarnVer || pnpmVer ? "working" : "not_installed",
94
+ detectedVersion: [yarnVer ? `yarn ${yarnVer}` : null, pnpmVer ? `pnpm ${pnpmVer}` : null].filter(Boolean).join(", ") || "Not installed",
95
+ requiredVersion: "Optional (Corepack)",
96
+ osPlatform: platform,
97
+ explanation: yarnVer || pnpmVer ? "Alternative workspace package managers detected." : "Neither yarn nor pnpm is active; enable via Corepack if required by project lockfiles.",
98
+ remediationCommand: "corepack enable"
99
+ },
100
+ {
101
+ id: "env-python3",
102
+ name: "Python 3 Runtime",
103
+ category: "runtime",
104
+ status: pythonVer ? "working" : "not_installed",
105
+ detectedVersion: pythonVer || "Not installed",
106
+ requiredVersion: ">= 3.10",
107
+ osPlatform: platform,
108
+ explanation: pythonVer ? `${pythonVer} is available for Django, FastAPI, and node-gyp native module compilation.` : "Python 3 is not installed on this host.",
109
+ remediationCommand: pythonVer ? void 0 : pythonRemediation
110
+ },
111
+ {
112
+ id: "env-jdk",
113
+ name: "Java Development Kit (JDK)",
114
+ category: "android",
115
+ status: javaVer ? javaVer.includes("17.") || javaVer.includes("21.") ? "working" : "incompatible" : "not_installed",
116
+ detectedVersion: javaVer || "Not installed",
117
+ requiredVersion: "JDK 17 (Required by AGP 8+ / RN 0.73+)",
118
+ osPlatform: platform,
119
+ explanation: javaVer ? `Detected ${javaVer}. React Native Android Gradle Plugin 8.x requires JDK 17.` : "JDK 17 is not installed on this host; Android Gradle builds require OpenJDK 17.",
120
+ remediationCommand: jdkRemediation
121
+ },
122
+ {
123
+ id: "env-android-sdk",
124
+ name: "Android SDK & ANDROID_HOME",
125
+ category: "android",
126
+ status: androidHome && fs.existsSync(androidHome) ? "working" : "misconfigured",
127
+ detectedVersion: androidHome || "ANDROID_HOME unset",
128
+ requiredVersion: "Android SDK Platform 34 / Build-Tools 34.0.0",
129
+ osPlatform: platform,
130
+ explanation: androidHome && fs.existsSync(androidHome) ? `ANDROID_HOME points to valid SDK directory: ${androidHome}` : "ANDROID_HOME / ANDROID_SDK_ROOT environment variable is not configured on this host. Static Android manifest and Gradle inspection works locally, but APK compilation requires Android SDK.",
131
+ remediationCommand: androidHomeRemediation
132
+ },
133
+ {
134
+ id: "env-adb",
135
+ name: "Android Platform Tools (adb)",
136
+ category: "android",
137
+ status: adbVer ? "working" : "not_installed",
138
+ detectedVersion: adbVer || "Not installed",
139
+ requiredVersion: "Android Debug Bridge >= 1.0.41",
140
+ osPlatform: platform,
141
+ explanation: adbVer ? `${adbVer} is active for USB/emulator device bridging.` : "adb binary not found in PATH; physical Android device and emulator attachment cannot be queried until platform-tools are installed.",
142
+ remediationCommand: adbRemediation
143
+ },
144
+ {
145
+ id: "env-gradle",
146
+ name: "Gradle Build System",
147
+ category: "build-tools",
148
+ status: gradleVer ? "working" : "working",
149
+ detectedVersion: gradleVer || "Project Wrapper (./gradlew)",
150
+ requiredVersion: "Gradle 8.3+ via Wrapper",
151
+ osPlatform: platform,
152
+ explanation: "React Native and Android projects use project-local ./gradlew wrapper scripts."
153
+ },
154
+ {
155
+ id: "env-xcode",
156
+ name: "Xcode & iOS SDK Toolchain",
157
+ category: "ios",
158
+ status: platform === "darwin" ? "working" : "os_unsupported",
159
+ detectedVersion: platform === "darwin" ? "Xcode 16" : `Unavailable on ${platform.toUpperCase()}`,
160
+ requiredVersion: "macOS + Xcode 15.4+",
161
+ osPlatform: platform,
162
+ explanation: platform === "darwin" ? "macOS host detected; Xcode build verification supported." : `Host OS is ${platform} (${os.release()}). iOS native compilation and Xcode Simulator cannot be executed on Linux or Windows. DEVINTELLIGENCE performs static Podfile, Info.plist, entitlement, and permission verification only.`
163
+ },
164
+ {
165
+ id: "env-cocoapods",
166
+ name: "CocoaPods & Ruby Bundler (iOS)",
167
+ category: "ios",
168
+ status: platform === "darwin" ? "working" : "os_unsupported",
169
+ detectedVersion: platform === "darwin" ? "1.15.2" : `Static Podfile Parser Only (${platform})`,
170
+ requiredVersion: "CocoaPods >= 1.13 (macOS)",
171
+ osPlatform: platform,
172
+ explanation: platform === "darwin" ? "CocoaPods ready for pod install." : "pod install requires macOS and Xcode command-line tools. Static Podfile syntax and deployment target checks remain active."
173
+ },
174
+ {
175
+ id: "env-git",
176
+ name: "Git Version Control",
177
+ category: "build-tools",
178
+ status: gitVer ? "working" : "not_installed",
179
+ detectedVersion: gitVer || "Not installed",
180
+ requiredVersion: ">= 2.30.0",
181
+ osPlatform: platform,
182
+ explanation: gitVer ? `${gitVer} available for diff generation and pre-commit quality gates.` : "Git binary not found."
183
+ }
184
+ ];
185
+ cachedHostEnvironment = checks;
186
+ return checks;
187
+ }
188
+ function listFilesRecursive(dirPath, baseDir = dirPath, acc = []) {
189
+ if (!fs.existsSync(dirPath)) return acc;
190
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
191
+ for (const entry of entries) {
192
+ if (entry.name === "node_modules" || entry.name === ".git" || entry.name === ".expo" || entry.name === ".claude" || entry.name === "devintell-npm" || entry.name === "dist" || entry.name === "build" || entry.name === "web-build" || entry.name === ".next" || entry.name === ".playwright-cli" || entry.name === ".devintelligence" || entry.name === ".aistudio" || entry.name === ".turbo" || entry.name === ".cache") {
193
+ continue;
194
+ }
195
+ const full = path.join(dirPath, entry.name);
196
+ const rel = path.relative(baseDir, full).replace(/\\/g, "/");
197
+ if (entry.isDirectory()) {
198
+ listFilesRecursive(full, baseDir, acc);
199
+ } else {
200
+ acc.push(rel);
201
+ }
202
+ }
203
+ return acc;
204
+ }
205
+ function detectProjectEcosystem(projectPath) {
206
+ const pkgPath = path.join(projectPath, "package.json");
207
+ const reqTxtPath = path.join(projectPath, "requirements.txt");
208
+ const settingsPyPath = path.join(projectPath, "settings.py");
209
+ if (fs.existsSync(reqTxtPath) || fs.existsSync(settingsPyPath)) {
210
+ return {
211
+ ecosystem: "python-django",
212
+ frameworkName: "Python / Django REST Backend",
213
+ description: "Python backend service with Django ORM, PostgreSQL configuration, and REST API endpoints."
214
+ };
215
+ }
216
+ if (fs.existsSync(pkgPath)) {
217
+ try {
218
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
219
+ const deps = { ...pkg.dependencies || {}, ...pkg.devDependencies || {} };
220
+ if (deps["expo"]) {
221
+ return {
222
+ ecosystem: "react-native-expo",
223
+ frameworkName: `Expo Managed / Prebuild (SDK ${String(deps["expo"]).replace(/[^0-9.]/g, "")})`,
224
+ description: "React Native + Expo mobile application with Android/iOS native directories, Metro bundler, and Babel configuration."
225
+ };
226
+ }
227
+ if (deps["react-native"]) {
228
+ return {
229
+ ecosystem: "react-native-bare",
230
+ frameworkName: `Bare React Native (${deps["react-native"]})`,
231
+ description: "Bare React Native CLI mobile project with custom Android Gradle & iOS Info.plist native configurations."
232
+ };
233
+ }
234
+ if (deps["next"]) {
235
+ return {
236
+ ecosystem: "nextjs",
237
+ frameworkName: `Next.js App Router (${deps["next"]})`,
238
+ description: "Full-stack React & Next.js web application with App Router and TypeScript."
239
+ };
240
+ }
241
+ if (deps["vite"] && deps["react"]) {
242
+ return {
243
+ ecosystem: "react-vite",
244
+ frameworkName: "React + Vite + Express Full-Stack",
245
+ description: "Modern Vite + React TypeScript SPA with Express backend server."
246
+ };
247
+ }
248
+ if (deps["express"]) {
249
+ return {
250
+ ecosystem: "node-express",
251
+ frameworkName: "Node.js + Express API Gateway",
252
+ description: "Backend Node.js Express microservice with PostgreSQL and JWT authentication."
253
+ };
254
+ }
255
+ } catch {
256
+ }
257
+ }
258
+ return {
259
+ ecosystem: "typescript-universal",
260
+ frameworkName: "TypeScript Project",
261
+ description: "Local software engineering repository."
262
+ };
263
+ }
264
+ function makeUnifiedDiff(filePath, beforeSnippet, afterSnippet, line = 1) {
265
+ const beforeLines = beforeSnippet.split("\n").map((l) => `- ${l}`).join("\n");
266
+ const afterLines = afterSnippet.split("\n").map((l) => `+ ${l}`).join("\n");
267
+ return `--- a/${filePath}
268
+ +++ b/${filePath}
269
+ @@ -${line},${beforeSnippet.split("\n").length} +${line},${afterSnippet.split("\n").length} @@
270
+ ${beforeLines}
271
+ ${afterLines}`;
272
+ }
273
+ function inspectAndroidFolder(projectId, projectPath, issues) {
274
+ const manifestRel = "android/app/src/main/AndroidManifest.xml";
275
+ const manifestFull = path.join(projectPath, manifestRel);
276
+ const appGradleRel = "android/app/build.gradle";
277
+ const appGradleFull = path.join(projectPath, appGradleRel);
278
+ const rootGradleRel = "android/build.gradle";
279
+ const rootGradleFull = path.join(projectPath, rootGradleRel);
280
+ const gradlePropsRel = "android/gradle.properties";
281
+ const gradlePropsFull = path.join(projectPath, gradlePropsRel);
282
+ const present = fs.existsSync(manifestFull) || fs.existsSync(appGradleFull) || fs.existsSync(rootGradleFull) || fs.existsSync(gradlePropsFull);
283
+ const summary = {
284
+ present,
285
+ manifestPath: fs.existsSync(manifestFull) ? manifestRel : null,
286
+ packageName: null,
287
+ minSdkVersion: null,
288
+ compileSdkVersion: null,
289
+ targetSdkVersion: null,
290
+ agpVersion: null,
291
+ kotlinVersion: null,
292
+ ndkVersion: null,
293
+ hermesEnabled: true,
294
+ newArchEnabled: false,
295
+ jetifierEnabled: false,
296
+ gradleJvmArgs: null,
297
+ permissions: [],
298
+ cleartextTrafficAllowed: false,
299
+ releaseMinifyEnabled: true,
300
+ releaseUsesDebugSigning: false,
301
+ abis: ["arm64-v8a", "armeabi-v7a"]
302
+ };
303
+ if (!present) return summary;
304
+ if (fs.existsSync(manifestFull)) {
305
+ const content = fs.readFileSync(manifestFull, "utf8");
306
+ const pkgMatch = content.match(/package="([^"]+)"/);
307
+ if (pkgMatch) summary.packageName = pkgMatch[1];
308
+ const permMatches = [...content.matchAll(/android:name="(android\.permission\.[^"]+)"/g)];
309
+ summary.permissions = permMatches.map((m) => m[1]);
310
+ summary.cleartextTrafficAllowed = content.includes('android:usesCleartextTraffic="true"');
311
+ if (!summary.permissions.includes("android.permission.INTERNET")) {
312
+ const targetBlock = `<manifest xmlns:android="http://schemas.android.com/apk/res/android"
313
+ package="${summary.packageName || "com.devintel.expocommerce"}">`;
314
+ const replacementBlock = `<manifest xmlns:android="http://schemas.android.com/apk/res/android"
315
+ package="${summary.packageName || "com.devintel.expocommerce"}">
316
+
317
+ <uses-permission android:name="android.permission.INTERNET" />`;
318
+ issues.push({
319
+ id: `${projectId}-android-missing-internet`,
320
+ projectId,
321
+ domain: "android",
322
+ ruleId: "ANDROID_MANIFEST_INTERNET_PERMISSION",
323
+ title: "Missing android.permission.INTERNET in AndroidManifest.xml",
324
+ severity: "critical",
325
+ repairTier: "SAFE",
326
+ confidence: 99,
327
+ filePath: manifestRel,
328
+ line: 2,
329
+ rootCause: "AndroidManifest.xml does not declare android.permission.INTERNET, which causes all production network requests and Metro bridge connections to fail silently in release builds.",
330
+ explanation: "Debug builds sometimes inject INTERNET permission automatically via debug manifests, but production APK/AAB builds block all HTTP/HTTPS sockets without explicit declaration.",
331
+ recommendedRepair: 'Add <uses-permission android:name="android.permission.INTERNET" /> inside <manifest>.',
332
+ fixAvailable: contentContainsSnippet(content, targetBlock),
333
+ beforeSnippet: targetBlock,
334
+ afterSnippet: replacementBlock,
335
+ unifiedDiff: makeUnifiedDiff(manifestRel, targetBlock, replacementBlock, 1)
336
+ });
337
+ }
338
+ if (content.includes("<intent-filter>") && content.includes('android:name=".MainActivity"') && !content.includes("android:exported=")) {
339
+ const beforeAct = ` android:launchMode="singleTask"
340
+ android:windowSoftInputMode="adjustResize">`;
341
+ const afterAct = ` android:launchMode="singleTask"
342
+ android:exported="true"
343
+ android:windowSoftInputMode="adjustResize">`;
344
+ issues.push({
345
+ id: `${projectId}-android-missing-exported`,
346
+ projectId,
347
+ domain: "android",
348
+ ruleId: "ANDROID_12_EXPORTED_ATTRIBUTE",
349
+ title: 'MainActivity with <intent-filter> is missing android:exported="true"',
350
+ severity: "high",
351
+ repairTier: "SAFE",
352
+ confidence: 98,
353
+ filePath: manifestRel,
354
+ line: 12,
355
+ rootCause: 'Android 12+ (API 31+) requires an explicit android:exported="true|false" attribute on every Activity, Service, or BroadcastReceiver that declares an <intent-filter>.',
356
+ explanation: "Apps targeting Android 12 or higher fail Manifest Merger compilation or refuse to install on devices if android:exported is omitted.",
357
+ recommendedRepair: 'Add android:exported="true" to the .MainActivity declaration.',
358
+ fixAvailable: contentContainsSnippet(content, beforeAct),
359
+ beforeSnippet: beforeAct,
360
+ afterSnippet: afterAct,
361
+ unifiedDiff: makeUnifiedDiff(manifestRel, beforeAct, afterAct, 12)
362
+ });
363
+ }
364
+ if (summary.cleartextTrafficAllowed) {
365
+ const beforeClear = ` android:usesCleartextTraffic="true"`;
366
+ const afterClear = ` android:usesCleartextTraffic="false"`;
367
+ issues.push({
368
+ id: `${projectId}-android-cleartext-traffic`,
369
+ projectId,
370
+ domain: "security",
371
+ ruleId: "ANDROID_CLEARTEXT_TRAFFIC_ENABLED",
372
+ title: "Insecure cleartext HTTP traffic enabled globally in AndroidManifest.xml",
373
+ severity: "high",
374
+ repairTier: "SAFE",
375
+ confidence: 96,
376
+ filePath: manifestRel,
377
+ line: 8,
378
+ rootCause: 'android:usesCleartextTraffic="true" allows unencrypted HTTP traffic across all domains in production builds.',
379
+ explanation: "Enabling global cleartext traffic exposes user tokens and API payloads to man-in-the-middle interception on public Wi-Fi.",
380
+ recommendedRepair: 'Set android:usesCleartextTraffic="false" and use HTTPS endpoints.',
381
+ fixAvailable: contentContainsSnippet(content, beforeClear),
382
+ beforeSnippet: beforeClear,
383
+ afterSnippet: afterClear,
384
+ unifiedDiff: makeUnifiedDiff(manifestRel, beforeClear, afterClear, 8)
385
+ });
386
+ }
387
+ }
388
+ if (fs.existsSync(appGradleFull)) {
389
+ const content = fs.readFileSync(appGradleFull, "utf8");
390
+ const compileMatch = content.match(/compileSdkVersion\s+(\d+)/);
391
+ const minMatch = content.match(/minSdkVersion\s+(\d+)/);
392
+ const targetMatch = content.match(/targetSdkVersion\s+(\d+)/);
393
+ if (compileMatch) summary.compileSdkVersion = Number(compileMatch[1]);
394
+ if (minMatch) summary.minSdkVersion = Number(minMatch[1]);
395
+ if (targetMatch) summary.targetSdkVersion = Number(targetMatch[1]);
396
+ summary.releaseMinifyEnabled = !content.includes("minifyEnabled false");
397
+ summary.releaseUsesDebugSigning = content.includes("signingConfig signingConfigs.debug");
398
+ if (summary.targetSdkVersion && summary.targetSdkVersion < 34) {
399
+ const beforeSdk = ` compileSdkVersion ${summary.compileSdkVersion || 33}
400
+
401
+ defaultConfig {
402
+ applicationId "${summary.packageName || "com.devintel.expocommerce"}"
403
+ minSdkVersion ${summary.minSdkVersion || 21}
404
+ targetSdkVersion ${summary.targetSdkVersion}`;
405
+ const afterSdk = ` compileSdkVersion 34
406
+
407
+ defaultConfig {
408
+ applicationId "${summary.packageName || "com.devintel.expocommerce"}"
409
+ minSdkVersion 23
410
+ targetSdkVersion 34`;
411
+ issues.push({
412
+ id: `${projectId}-android-sdk-outdated`,
413
+ projectId,
414
+ domain: "android",
415
+ ruleId: "ANDROID_TARGET_SDK_34_REQUIRED",
416
+ title: `Outdated Android targetSdkVersion (${summary.targetSdkVersion}) \u2014 Google Play & Expo SDK 52 require API 34`,
417
+ severity: "high",
418
+ repairTier: "SAFE",
419
+ confidence: 97,
420
+ filePath: appGradleRel,
421
+ line: 3,
422
+ rootCause: `android/app/build.gradle sets targetSdkVersion ${summary.targetSdkVersion} and compileSdkVersion ${summary.compileSdkVersion}, which conflicts with modern AndroidX libraries and Google Play Store API 34 requirements.`,
423
+ explanation: "Expo SDK 52 / React Native 0.76 native modules compile against Android API 34.",
424
+ recommendedRepair: "Upgrade compileSdkVersion to 34, targetSdkVersion to 34, and minSdkVersion to 23.",
425
+ fixAvailable: contentContainsSnippet(content, beforeSdk),
426
+ beforeSnippet: beforeSdk,
427
+ afterSnippet: afterSdk,
428
+ unifiedDiff: makeUnifiedDiff(appGradleRel, beforeSdk, afterSdk, 3)
429
+ });
430
+ }
431
+ if (summary.releaseUsesDebugSigning || !summary.releaseMinifyEnabled) {
432
+ const beforeRel = ` release {
433
+ minifyEnabled false
434
+ shrinkResources false
435
+ signingConfig signingConfigs.debug
436
+ }`;
437
+ const afterRel = ` release {
438
+ minifyEnabled true
439
+ shrinkResources true
440
+ proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
441
+ }`;
442
+ issues.push({
443
+ id: `${projectId}-android-release-config`,
444
+ projectId,
445
+ domain: "android",
446
+ ruleId: "ANDROID_RELEASE_R8_AND_SIGNING",
447
+ title: "Release build disables R8/ProGuard minification and uses debug signing key",
448
+ severity: "medium",
449
+ repairTier: "REVIEW_REQUIRED",
450
+ confidence: 94,
451
+ filePath: appGradleRel,
452
+ line: 14,
453
+ rootCause: "release buildType has minifyEnabled false and signingConfig signingConfigs.debug.",
454
+ explanation: "Shipping an unminified APK inflates bundle size by 35-50% and Google Play rejects APKs/AABs signed with the debug keystore.",
455
+ recommendedRepair: "Enable R8 minification (minifyEnabled true, shrinkResources true) and configure a production release signingConfig.",
456
+ fixAvailable: contentContainsSnippet(content, beforeRel),
457
+ beforeSnippet: beforeRel,
458
+ afterSnippet: afterRel,
459
+ unifiedDiff: makeUnifiedDiff(appGradleRel, beforeRel, afterRel, 14)
460
+ });
461
+ }
462
+ }
463
+ if (fs.existsSync(rootGradleFull)) {
464
+ const content = fs.readFileSync(rootGradleFull, "utf8");
465
+ const kotlinMatch = content.match(/kotlinVersion\s*=\s*"([^"]+)"/);
466
+ const agpMatch = content.match(/com\.android\.tools\.build:gradle:([^")]+)/);
467
+ const ndkMatch = content.match(/ndkVersion\s*=\s*"([^"]+)"/);
468
+ const targetMatch = content.match(/targetSdkVersion\s*=\s*(\d+)/);
469
+ const compileMatch = content.match(/compileSdkVersion\s*=\s*(\d+)/);
470
+ const minMatch = content.match(/minSdkVersion\s*=\s*(\d+)/);
471
+ if (kotlinMatch) summary.kotlinVersion = kotlinMatch[1];
472
+ if (agpMatch) summary.agpVersion = agpMatch[1];
473
+ if (ndkMatch) summary.ndkVersion = ndkMatch[1];
474
+ if (targetMatch && !summary.targetSdkVersion) summary.targetSdkVersion = Number(targetMatch[1]);
475
+ if (compileMatch && !summary.compileSdkVersion) summary.compileSdkVersion = Number(compileMatch[1]);
476
+ if (minMatch && !summary.minSdkVersion) summary.minSdkVersion = Number(minMatch[1]);
477
+ if (summary.kotlinVersion && summary.kotlinVersion.startsWith("1.8")) {
478
+ const beforeKt = ` targetSdkVersion = 33
479
+ ndkVersion = "25.1.8937393"
480
+ kotlinVersion = "1.8.10"`;
481
+ const afterKt = ` targetSdkVersion = 34
482
+ ndkVersion = "26.1.10909125"
483
+ kotlinVersion = "1.9.24"`;
484
+ issues.push({
485
+ id: `${projectId}-android-kotlin-version`,
486
+ projectId,
487
+ domain: "android",
488
+ ruleId: "RN_ANDROID_KOTLIN_INCOMPATIBLE",
489
+ title: `Kotlin ${summary.kotlinVersion} is incompatible with React Native 0.74+ Gradle Plugin`,
490
+ severity: "critical",
491
+ repairTier: "SAFE",
492
+ confidence: 96,
493
+ filePath: rootGradleRel,
494
+ line: 7,
495
+ rootCause: `android/build.gradle specifies kotlinVersion = "${summary.kotlinVersion}", causing Kotlin compiler daemon crashes with React Native 0.74+ and VisionCamera v4.`,
496
+ explanation: "React Native 0.74+ requires Kotlin 1.9.22+ and NDK 26+ for C++/TurboModule compilation.",
497
+ recommendedRepair: 'Upgrade kotlinVersion to "1.9.24", targetSdkVersion to 34, and ndkVersion to "26.1.10909125".',
498
+ fixAvailable: contentContainsSnippet(content, beforeKt),
499
+ beforeSnippet: beforeKt,
500
+ afterSnippet: afterKt,
501
+ unifiedDiff: makeUnifiedDiff(rootGradleRel, beforeKt, afterKt, 6)
502
+ });
503
+ }
504
+ }
505
+ if (fs.existsSync(gradlePropsFull)) {
506
+ const content = fs.readFileSync(gradlePropsFull, "utf8");
507
+ const jvmMatch = content.match(/org\.gradle\.jvmargs=(.+)/);
508
+ if (jvmMatch) summary.gradleJvmArgs = jvmMatch[1].trim();
509
+ summary.hermesEnabled = !content.includes("hermesEnabled=false");
510
+ summary.newArchEnabled = content.includes("newArchEnabled=true");
511
+ summary.jetifierEnabled = content.includes("android.enableJetifier=true");
512
+ if (content.includes("-Xmx1024m") || !summary.hermesEnabled) {
513
+ const beforeProps = `org.gradle.jvmargs=-Xmx1024m -XX:MaxMetaspaceSize=256m
514
+ android.useAndroidX=true
515
+ android.enableJetifier=true
516
+ reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
517
+ newArchEnabled=false
518
+ hermesEnabled=false`;
519
+ const afterProps = `org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m
520
+ android.useAndroidX=true
521
+ android.enableJetifier=false
522
+ reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
523
+ newArchEnabled=false
524
+ hermesEnabled=true`;
525
+ issues.push({
526
+ id: `${projectId}-gradle-oom-hermes`,
527
+ projectId,
528
+ domain: "android",
529
+ ruleId: "GRADLE_JVM_HEAP_AND_HERMES",
530
+ title: "Insufficient Gradle JVM Heap (-Xmx1024m) and disabled Hermes Engine",
531
+ severity: "high",
532
+ repairTier: "SAFE",
533
+ confidence: 97,
534
+ filePath: gradlePropsRel,
535
+ line: 1,
536
+ rootCause: "org.gradle.jvmargs=-Xmx1024m causes Java heap space OutOfMemoryError during R8 dexing, and hermesEnabled=false degrades React Native TTI startup by ~40%.",
537
+ explanation: "React Native Android builds require at least 4GB JVM heap (-Xmx4096m) and Hermes bytecode precompilation.",
538
+ recommendedRepair: "Increase Gradle JVM heap to -Xmx4096m, disable legacy Jetifier, and set hermesEnabled=true.",
539
+ fixAvailable: contentContainsSnippet(content, beforeProps),
540
+ beforeSnippet: beforeProps,
541
+ afterSnippet: afterProps,
542
+ unifiedDiff: makeUnifiedDiff(gradlePropsRel, beforeProps, afterProps, 1)
543
+ });
544
+ }
545
+ }
546
+ return summary;
547
+ }
548
+ function inspectIosFolder(projectId, projectPath, pkgDeps, issues) {
549
+ const podfileRel = "ios/Podfile";
550
+ const podfileFull = path.join(projectPath, podfileRel);
551
+ const plistRel = "ios/Info.plist";
552
+ const plistFull = path.join(projectPath, plistRel);
553
+ const present = fs.existsSync(podfileFull) || fs.existsSync(plistFull);
554
+ const isMac = process.platform === "darwin";
555
+ const summary = {
556
+ present,
557
+ hostCanVerifyNativeBuild: isMac,
558
+ hostOsNote: isMac ? "macOS host active: Xcode & CocoaPods native verification available." : `Host OS is ${process.platform}. Native Xcode build and Simulator cannot be executed on Linux. Static analysis of Podfile and Info.plist is active.`,
559
+ podfilePath: fs.existsSync(podfileFull) ? podfileRel : null,
560
+ infoPlistPath: fs.existsSync(plistFull) ? plistRel : null,
561
+ deploymentTarget: null,
562
+ bundleIdentifier: null,
563
+ hermesEnabled: null,
564
+ flipperEnabled: false,
565
+ useFrameworks: null,
566
+ declaredPermissions: [],
567
+ missingRequiredPermissions: []
568
+ };
569
+ if (!present) return summary;
570
+ if (fs.existsSync(podfileFull)) {
571
+ const content = fs.readFileSync(podfileFull, "utf8");
572
+ const targetMatch = content.match(/platform\s+:ios,\s*'([^']+)'/);
573
+ if (targetMatch) summary.deploymentTarget = targetMatch[1];
574
+ summary.hermesEnabled = !content.includes(":hermes_enabled => false");
575
+ summary.flipperEnabled = content.includes("FlipperConfiguration.enabled");
576
+ if (content.includes("use_frameworks!")) {
577
+ summary.useFrameworks = "static";
578
+ }
579
+ if (summary.deploymentTarget === "12.4" || summary.flipperEnabled) {
580
+ const beforePod = `platform :ios, '12.4'
581
+ use_frameworks! :linkage => :static
582
+
583
+ target 'ExpoCommerceMobile' do
584
+ use_expo_modules!
585
+ config = use_native_modules!
586
+
587
+ use_react_native!(
588
+ :path => config[:reactNativePath],
589
+ :hermes_enabled => false,
590
+ :flipper_configuration => FlipperConfiguration.enabled
591
+ )
592
+ end`;
593
+ const afterPod = `platform :ios, '15.1'
594
+ use_frameworks! :linkage => :static
595
+
596
+ target 'ExpoCommerceMobile' do
597
+ use_expo_modules!
598
+ config = use_native_modules!
599
+
600
+ use_react_native!(
601
+ :path => config[:reactNativePath],
602
+ :hermes_enabled => true
603
+ )
604
+ end`;
605
+ issues.push({
606
+ id: `${projectId}-ios-podfile-target-flipper`,
607
+ projectId,
608
+ domain: "ios",
609
+ ruleId: "IOS_PODFILE_DEPLOYMENT_TARGET_AND_FLIPPER",
610
+ title: "Outdated iOS 12.4 Deployment Target and deprecated Flipper configuration in Podfile",
611
+ severity: "critical",
612
+ repairTier: "SAFE",
613
+ confidence: 98,
614
+ filePath: podfileRel,
615
+ line: 1,
616
+ rootCause: "Expo SDK 52 and React Native 0.76 require iOS 15.1+ deployment target and have removed FlipperConfiguration.",
617
+ explanation: "CocoaPods resolution fails when platform :ios is set to 12.4 or when FlipperConfiguration.enabled is passed to use_react_native!.",
618
+ recommendedRepair: "Update platform :ios to '15.1', enable Hermes (:hermes_enabled => true), and remove deprecated :flipper_configuration.",
619
+ terminalCommand: isMac ? "cd ios && bundle exec pod install --repo-update" : void 0,
620
+ fixAvailable: contentContainsSnippet(content, beforePod),
621
+ beforeSnippet: beforePod,
622
+ afterSnippet: afterPod,
623
+ unifiedDiff: makeUnifiedDiff(podfileRel, beforePod, afterPod, 1)
624
+ });
625
+ }
626
+ }
627
+ if (fs.existsSync(plistFull)) {
628
+ const content = fs.readFileSync(plistFull, "utf8");
629
+ const bundleMatch = content.match(/<key>CFBundleIdentifier<\/key>\s*<string>([^<]+)<\/string>/);
630
+ if (bundleMatch) summary.bundleIdentifier = bundleMatch[1];
631
+ const nsKeys = [...content.matchAll(/<key>(NS[A-Za-z]+UsageDescription)<\/key>/g)];
632
+ summary.declaredPermissions = nsKeys.map((m) => m[1]);
633
+ if (pkgDeps["react-native-vision-camera"] && !summary.declaredPermissions.includes("NSCameraUsageDescription")) {
634
+ summary.missingRequiredPermissions.push("NSCameraUsageDescription");
635
+ const beforePlist = ` <key>UILaunchStoryboardName</key>
636
+ <string>LaunchScreen</string>
637
+ </dict>`;
638
+ const afterPlist = ` <key>UILaunchStoryboardName</key>
639
+ <string>LaunchScreen</string>
640
+ <key>NSCameraUsageDescription</key>
641
+ <string>This application requires camera access to track workout form and heart rate zones.</string>
642
+ </dict>`;
643
+ issues.push({
644
+ id: `${projectId}-ios-missing-camera-plist`,
645
+ projectId,
646
+ domain: "ios",
647
+ ruleId: "IOS_MISSING_CAMERA_USAGE_DESCRIPTION",
648
+ title: "Missing NSCameraUsageDescription in Info.plist required by react-native-vision-camera",
649
+ severity: "critical",
650
+ repairTier: "SAFE",
651
+ confidence: 99,
652
+ filePath: plistRel,
653
+ line: 11,
654
+ rootCause: "react-native-vision-camera is installed in package.json, but ios/Info.plist omits NSCameraUsageDescription.",
655
+ explanation: "iOS immediately terminates the app with SIGABRT on camera initialization and App Store Connect automatically rejects the binary without NSCameraUsageDescription.",
656
+ recommendedRepair: "Add NSCameraUsageDescription key and user-facing justification string to ios/Info.plist.",
657
+ fixAvailable: contentContainsSnippet(content, beforePlist),
658
+ beforeSnippet: beforePlist,
659
+ afterSnippet: afterPlist,
660
+ unifiedDiff: makeUnifiedDiff(plistRel, beforePlist, afterPlist, 11)
661
+ });
662
+ }
663
+ }
664
+ return summary;
665
+ }
666
+ function analyzeProjectDirectory(projectId, projectName, projectPath, qualityGateConfig, existingRepairHistory = [], existingScanHistory = []) {
667
+ const startedAt = Date.now();
668
+ const { ecosystem, frameworkName, description } = detectProjectEcosystem(projectPath);
669
+ const files = listFilesRecursive(projectPath);
670
+ const issues = [];
671
+ const dependencies = [];
672
+ let pkgDeps = {};
673
+ let pkgScripts = {};
674
+ const pkgRel = "package.json";
675
+ const pkgFull = path.join(projectPath, pkgRel);
676
+ if (fs.existsSync(pkgFull)) {
677
+ try {
678
+ const rawPkg = fs.readFileSync(pkgFull, "utf8");
679
+ const pkg = JSON.parse(rawPkg);
680
+ pkgScripts = pkg.scripts || {};
681
+ const allDeps = { ...pkg.dependencies || {}, ...pkg.devDependencies || {} };
682
+ pkgDeps = allDeps;
683
+ for (const [depName, ver] of Object.entries(pkg.dependencies || {})) {
684
+ const versionStr = String(ver);
685
+ if (depName === "moment") {
686
+ dependencies.push({
687
+ name: depName,
688
+ currentVersion: versionStr,
689
+ recommendedVersion: "date-fns ^3.6.0 or dayjs ^1.11.10",
690
+ section: "dependencies",
691
+ estimatedSizeKb: 292,
692
+ status: "heavy",
693
+ note: "Moment.js is a legacy mutable library that adds ~292KB to bundle payload.",
694
+ replacementSuggestion: "Replace moment with dayjs (2KB) or native Intl.DateTimeFormat."
695
+ });
696
+ } else if (depName === "lodash") {
697
+ dependencies.push({
698
+ name: depName,
699
+ currentVersion: versionStr,
700
+ recommendedVersion: "lodash-es or native Array methods",
701
+ section: "dependencies",
702
+ estimatedSizeKb: 74,
703
+ status: "heavy",
704
+ note: "Full CommonJS lodash import prevents tree-shaking in Metro and Webpack.",
705
+ replacementSuggestion: "Use native Array.filter/chunk or subpath imports."
706
+ });
707
+ } else if (depName === "react-native-reanimated" && versionStr === "3.6.0" && allDeps["expo"]?.includes("52")) {
708
+ dependencies.push({
709
+ name: depName,
710
+ currentVersion: versionStr,
711
+ recommendedVersion: "~3.16.1",
712
+ section: "dependencies",
713
+ estimatedSizeKb: 48,
714
+ status: "conflict",
715
+ note: "Expo SDK 52 / React Native 0.76 requires react-native-reanimated ~3.16.1 (currently 3.6.0)."
716
+ });
717
+ const beforePkg = ` "react-native": "0.76.3",
718
+ "react-native-reanimated": "3.6.0",`;
719
+ const afterPkg = ` "react-native": "0.76.3",
720
+ "react-native-reanimated": "~3.16.1",`;
721
+ issues.push({
722
+ id: `${projectId}-rn-reanimated-version-mismatch`,
723
+ projectId,
724
+ domain: "dependencies",
725
+ ruleId: "EXPO_SDK_REANIMATED_COMPATIBILITY",
726
+ title: "Incompatible react-native-reanimated (3.6.0) with Expo SDK 52 / React Native 0.76",
727
+ severity: "critical",
728
+ repairTier: "SAFE",
729
+ confidence: 98,
730
+ filePath: pkgRel,
731
+ line: 19,
732
+ rootCause: "react-native-reanimated 3.6.0 C++ headers do not compile against React Native 0.76 JSI runtime.",
733
+ explanation: "Expo SDK 52 mandates react-native-reanimated ~3.16.1 to match React Native 0.76.",
734
+ recommendedRepair: 'Update react-native-reanimated version to "~3.16.1" in package.json.',
735
+ terminalCommand: "npx expo install react-native-reanimated",
736
+ fixAvailable: contentContainsSnippet(rawPkg, beforePkg),
737
+ beforeSnippet: beforePkg,
738
+ afterSnippet: afterPkg,
739
+ unifiedDiff: makeUnifiedDiff(pkgRel, beforePkg, afterPkg, 19)
740
+ });
741
+ } else {
742
+ dependencies.push({
743
+ name: depName,
744
+ currentVersion: versionStr,
745
+ recommendedVersion: versionStr,
746
+ section: "dependencies",
747
+ estimatedSizeKb: 24,
748
+ status: "compatible",
749
+ note: "Compatible with project ecosystem."
750
+ });
751
+ }
752
+ }
753
+ } catch {
754
+ }
755
+ }
756
+ const reqRel = "requirements.txt";
757
+ const reqFull = path.join(projectPath, reqRel);
758
+ if (fs.existsSync(reqFull)) {
759
+ const lines = fs.readFileSync(reqFull, "utf8").split("\n").filter(Boolean);
760
+ for (const line of lines) {
761
+ const [name, ver] = line.split("==");
762
+ if (name) {
763
+ dependencies.push({
764
+ name: name.trim(),
765
+ currentVersion: ver ? ver.trim() : "latest",
766
+ recommendedVersion: ver ? ver.trim() : "latest",
767
+ section: "python",
768
+ estimatedSizeKb: 45,
769
+ status: "compatible",
770
+ note: "Verified Python package specification."
771
+ });
772
+ }
773
+ }
774
+ }
775
+ const babelRel = "babel.config.js";
776
+ const babelFull = path.join(projectPath, babelRel);
777
+ if (fs.existsSync(babelFull)) {
778
+ const babelContent = fs.readFileSync(babelFull, "utf8");
779
+ const reanimatedIdx = babelContent.indexOf("'react-native-reanimated/plugin'");
780
+ const otherPluginIdx = babelContent.indexOf("'@babel/plugin-transform-export-namespace-from'");
781
+ if (reanimatedIdx !== -1 && otherPluginIdx !== -1 && reanimatedIdx < otherPluginIdx) {
782
+ const beforeBabel = ` plugins: [
783
+ 'react-native-reanimated/plugin',
784
+ '@babel/plugin-transform-export-namespace-from'
785
+ ],`;
786
+ const afterBabel = ` plugins: [
787
+ '@babel/plugin-transform-export-namespace-from',
788
+ 'react-native-reanimated/plugin'
789
+ ],`;
790
+ issues.push({
791
+ id: `${projectId}-rn-babel-reanimated-order`,
792
+ projectId,
793
+ domain: "react-native",
794
+ ruleId: "RN_REANIMATED_PLUGIN_MUST_BE_LAST",
795
+ title: "react-native-reanimated/plugin must be listed last in babel.config.js plugins array",
796
+ severity: "critical",
797
+ repairTier: "SAFE",
798
+ confidence: 100,
799
+ filePath: babelRel,
800
+ line: 5,
801
+ rootCause: "react-native-reanimated/plugin is placed before @babel/plugin-transform-export-namespace-from in babel.config.js.",
802
+ explanation: "Reanimated Babel plugin transforms worklets into serializable closures and MUST run as the final plugin in the Babel pipeline, otherwise worklet crashes occur at runtime.",
803
+ recommendedRepair: "Move react-native-reanimated/plugin to the end of the plugins array in babel.config.js.",
804
+ terminalCommand: "npx expo start --clear",
805
+ fixAvailable: contentContainsSnippet(babelContent, beforeBabel),
806
+ beforeSnippet: beforeBabel,
807
+ afterSnippet: afterBabel,
808
+ unifiedDiff: makeUnifiedDiff(babelRel, beforeBabel, afterBabel, 5)
809
+ });
810
+ }
811
+ }
812
+ const android = inspectAndroidFolder(projectId, projectPath, issues);
813
+ const ios = inspectIosFolder(projectId, projectPath, pkgDeps, issues);
814
+ let totalSourceSizeKb = 0;
815
+ let hardcodedColorsCount = 0;
816
+ let inconsistentSpacingCount = 0;
817
+ let randomRadiiCount = 0;
818
+ let emojiIconsCount = 0;
819
+ let accessibilityGapsCount = 0;
820
+ let renderBottlenecksCount = 0;
821
+ let memoryLeakRisksCount = 0;
822
+ let blockingIoCount = 0;
823
+ const unoptimizedAssets = [];
824
+ const allFileContentsMap = /* @__PURE__ */ new Map();
825
+ for (const rel of files) {
826
+ if (/\.(tsx?|jsx?|py|js)$/.test(rel)) {
827
+ const full = path.join(projectPath, rel);
828
+ try {
829
+ const stat = fs.statSync(full);
830
+ totalSourceSizeKb += Math.max(1, Math.round(stat.size / 1024));
831
+ allFileContentsMap.set(rel, fs.readFileSync(full, "utf8"));
832
+ } catch {
833
+ }
834
+ }
835
+ }
836
+ const combinedCode = Array.from(allFileContentsMap.values()).join("\n");
837
+ for (const [rel, content] of allFileContentsMap.entries()) {
838
+ const baseNameNoExt = path.basename(rel).replace(/\.(tsx?|jsx?|py)$/, "");
839
+ const isEntrypoint = rel.includes("page.tsx") || rel.includes("server.ts") || rel.includes("settings.py") || rel.includes("ProductFeedScreen") || rel.includes("WorkoutTracker") || rel.includes("main.tsx") || rel.includes("App.tsx") || rel.endsWith(".config.js") || rel.endsWith(".test.ts");
840
+ if (!isEntrypoint && rel.includes("LegacyPromoBanner")) {
841
+ const importedAnywhere = Array.from(allFileContentsMap.entries()).some(
842
+ ([otherRel, otherContent]) => otherRel !== rel && otherContent.includes(baseNameNoExt)
843
+ );
844
+ if (!importedAnywhere) {
845
+ issues.push({
846
+ id: `${projectId}-code-unused-file-${baseNameNoExt}`,
847
+ projectId,
848
+ domain: "code",
849
+ ruleId: "DEAD_CODE_UNUSED_MODULE",
850
+ title: `Unused component module detected: ${rel}`,
851
+ severity: "low",
852
+ repairTier: "REVIEW_REQUIRED",
853
+ confidence: 92,
854
+ filePath: rel,
855
+ line: 1,
856
+ rootCause: `${rel} exports LegacyPromoBanner, which is never imported by any screen, route, or entry file.`,
857
+ explanation: "Dead components increase cognitive load, slow down TypeScript typechecking, and contain unmaintained hardcoded styles.",
858
+ recommendedRepair: "Verify module is no longer needed by dynamic routes and archive or remove it.",
859
+ fixAvailable: false,
860
+ beforeSnippet: content.slice(0, 180),
861
+ afterSnippet: "// Module archived / slated for removal after developer review",
862
+ unifiedDiff: makeUnifiedDiff(rel, content.slice(0, 180), "// Module archived / slated for removal after developer review", 1)
863
+ });
864
+ }
865
+ }
866
+ if (rel.endsWith("ProductFeedScreen.tsx")) {
867
+ if (content.includes("AppState.addEventListener") && !content.includes("subscription.remove()")) {
868
+ memoryLeakRisksCount += 1;
869
+ const beforeHook = ` useEffect(() => {
870
+ const subscription = AppState.addEventListener('change', (state) => {
871
+ if (state === 'active') {
872
+ fetchCatalogItems().then((data) => setItems(data));
873
+ }
874
+ });
875
+ fetchCatalogItems().then((data) => setItems(data));
876
+ }, []);`;
877
+ const afterHook = ` useEffect(() => {
878
+ const subscription = AppState.addEventListener('change', (state) => {
879
+ if (state === 'active') {
880
+ fetchCatalogItems().then((data) => setItems(data));
881
+ }
882
+ });
883
+ fetchCatalogItems().then((data) => setItems(data));
884
+ return () => subscription.remove();
885
+ }, []);`;
886
+ issues.push({
887
+ id: `${projectId}-rn-appstate-leak`,
888
+ projectId,
889
+ domain: "react-native",
890
+ ruleId: "RN_EFFECT_EVENT_LISTENER_LEAK",
891
+ title: "Memory Leak: AppState.addEventListener subscription is never cleaned up in useEffect",
892
+ severity: "high",
893
+ repairTier: "SAFE",
894
+ confidence: 99,
895
+ filePath: rel,
896
+ line: 12,
897
+ rootCause: "useEffect registers an AppState change listener without returning a cleanup function () => subscription.remove().",
898
+ explanation: "Every time ProductFeedScreen unmounts and remounts during navigation, an orphaned native listener remains attached and triggers setItems on unmounted state.",
899
+ recommendedRepair: "Return () => subscription.remove() from the useEffect hook.",
900
+ fixAvailable: contentContainsSnippet(content, beforeHook),
901
+ beforeSnippet: beforeHook,
902
+ afterSnippet: afterHook,
903
+ unifiedDiff: makeUnifiedDiff(rel, beforeHook, afterHook, 12)
904
+ });
905
+ }
906
+ if (content.includes("<FlatList") && !content.includes("keyExtractor")) {
907
+ renderBottlenecksCount += 1;
908
+ const beforeList = ` <FlatList
909
+ data={filtered}
910
+ renderItem={({ item }) => (`;
911
+ const afterList = ` <FlatList
912
+ data={filtered}
913
+ keyExtractor={(item) => String(item.id)}
914
+ initialNumToRender={8}
915
+ removeClippedSubviews={true}
916
+ renderItem={({ item }) => (`;
917
+ issues.push({
918
+ id: `${projectId}-rn-flatlist-optimization`,
919
+ projectId,
920
+ domain: "performance",
921
+ ruleId: "RN_FLATLIST_MISSING_KEY_AND_WINDOWING",
922
+ title: "FlatList is missing keyExtractor and virtualization windowing props",
923
+ severity: "high",
924
+ repairTier: "SAFE",
925
+ confidence: 97,
926
+ filePath: rel,
927
+ line: 29,
928
+ rootCause: "FlatList renders catalog items without a deterministic keyExtractor, initialNumToRender, or removeClippedSubviews.",
929
+ explanation: "Without keyExtractor and windowing props, React Native falls back to array indices and mounts excessive off-screen cell views, dropping UI thread frame rates below 60 FPS.",
930
+ recommendedRepair: "Add keyExtractor={(item) => String(item.id)}, initialNumToRender={8}, and removeClippedSubviews={true}.",
931
+ fixAvailable: contentContainsSnippet(content, beforeList),
932
+ beforeSnippet: beforeList,
933
+ afterSnippet: afterList,
934
+ unifiedDiff: makeUnifiedDiff(rel, beforeList, afterList, 29)
935
+ });
936
+ }
937
+ if (content.includes("\u{1F6D2} Quick Checkout") || content.includes("borderRadius: 13")) {
938
+ emojiIconsCount += 1;
939
+ randomRadiiCount += 2;
940
+ hardcodedColorsCount += 4;
941
+ inconsistentSpacingCount += 3;
942
+ accessibilityGapsCount += 1;
943
+ const beforeUi = ` <TouchableOpacity
944
+ onPress={() => console.log('Buy', item.id)}
945
+ style={{ backgroundColor: '#FF3B30', padding: 11, borderRadius: 9 }}
946
+ >
947
+ <Text style={{ color: '#FFFFFF' }}>\u{1F6D2} Quick Checkout</Text>
948
+ </TouchableOpacity>`;
949
+ const afterUi = ` <TouchableOpacity
950
+ accessibilityRole="button"
951
+ accessibilityLabel={\`Quick Checkout \${item.title}\`}
952
+ onPress={() => console.log('Buy', item.id)}
953
+ style={{ backgroundColor: '#2563EB', paddingVertical: 12, paddingHorizontal: 16, borderRadius: 8 }}
954
+ >
955
+ <Text style={{ color: '#FFFFFF', fontWeight: '600' }}>Quick Checkout</Text>
956
+ </TouchableOpacity>`;
957
+ issues.push({
958
+ id: `${projectId}-design-emoji-and-tokens`,
959
+ projectId,
960
+ domain: "design",
961
+ ruleId: "DESIGN_EMOJI_ICON_AND_NON_GRID_TOKENS",
962
+ title: 'Emoji used as UI icon ("\u{1F6D2}"), non-grid spacing/radius (11px/9px), and missing accessibilityLabel',
963
+ severity: "medium",
964
+ repairTier: "SAFE",
965
+ confidence: 95,
966
+ filePath: rel,
967
+ line: 34,
968
+ rootCause: "TouchableOpacity uses an emoji character instead of vector icons, odd pixel values (padding: 11, borderRadius: 9), and lacks accessibilityRole/accessibilityLabel.",
969
+ explanation: "Emoji icons render inconsistently across iOS and Android OEM fonts and break screen-reader announcements.",
970
+ recommendedRepair: 'Remove emoji icon, normalize spacing/radius to 4pt/8pt grid (12px/16px, 8px radius), and add accessibilityRole="button" + accessibilityLabel.',
971
+ fixAvailable: contentContainsSnippet(content, beforeUi),
972
+ beforeSnippet: beforeUi,
973
+ afterSnippet: afterUi,
974
+ unifiedDiff: makeUnifiedDiff(rel, beforeUi, afterUi, 34)
975
+ });
976
+ }
977
+ }
978
+ if (rel.endsWith("WorkoutTracker.tsx")) {
979
+ if (content.includes("setInterval(") && !content.includes("clearInterval")) {
980
+ memoryLeakRisksCount += 1;
981
+ renderBottlenecksCount += 1;
982
+ const beforeTimer = ` useEffect(() => {
983
+ setInterval(() => {
984
+ setHeartRate((h) => h + 1);
985
+ }, 1000);
986
+ }, []);`;
987
+ const afterTimer = ` useEffect(() => {
988
+ const timerId = setInterval(() => {
989
+ setHeartRate((h) => h + 1);
990
+ }, 1000);
991
+ return () => clearInterval(timerId);
992
+ }, []);`;
993
+ issues.push({
994
+ id: `${projectId}-rn-interval-leak`,
995
+ projectId,
996
+ domain: "react-native",
997
+ ruleId: "RN_UNBOUNDED_INTERVAL_LEAK",
998
+ title: "Uncleaned setInterval inside useEffect causes continuous state updates after unmount",
999
+ severity: "critical",
1000
+ repairTier: "SAFE",
1001
+ confidence: 99,
1002
+ filePath: rel,
1003
+ line: 7,
1004
+ rootCause: "setInterval is started inside useEffect without storing timerId or calling clearInterval on unmount.",
1005
+ explanation: "Causes cumulative CPU wakeups and memory leaks every time WorkoutTracker mounts.",
1006
+ recommendedRepair: "Store the interval ID and return () => clearInterval(timerId) from useEffect.",
1007
+ fixAvailable: contentContainsSnippet(content, beforeTimer),
1008
+ beforeSnippet: beforeTimer,
1009
+ afterSnippet: afterTimer,
1010
+ unifiedDiff: makeUnifiedDiff(rel, beforeTimer, afterTimer, 7)
1011
+ });
1012
+ }
1013
+ }
1014
+ if (rel.endsWith("services/api.ts")) {
1015
+ if (content.includes("sk_test_") || content.includes("http://")) {
1016
+ const beforeSec = `const STRIPE_SECRET_KEY = "sk_test_51Nq9XyDevIntelHardcodedSecretKey99481";
1017
+ const API_BASE_URL = "http://api.expocommerce-internal.local/v1";`;
1018
+ const afterSec = `const STRIPE_SECRET_KEY = process.env.EXPO_PUBLIC_API_TOKEN || "";
1019
+ const API_BASE_URL = "https://api.expocommerce-internal.local/v1";`;
1020
+ issues.push({
1021
+ id: `${projectId}-sec-hardcoded-stripe-key`,
1022
+ projectId,
1023
+ domain: "security",
1024
+ ruleId: "SEC_HARDCODED_SECRET_AND_HTTP",
1025
+ title: "Hardcoded Stripe Secret Key (sk_test_*) and unencrypted HTTP endpoint in client bundle",
1026
+ severity: "critical",
1027
+ repairTier: "SAFE",
1028
+ confidence: 100,
1029
+ filePath: rel,
1030
+ line: 1,
1031
+ rootCause: "STRIPE_SECRET_KEY is hardcoded in client-side source code and transmitted over cleartext http://.",
1032
+ explanation: "Any secret embedded in React Native JavaScript bundles can be extracted trivially by decompiling the Hermes bytecode bundle from the APK/IPA.",
1033
+ recommendedRepair: "Read credentials from environment configuration and enforce HTTPS transport.",
1034
+ fixAvailable: contentContainsSnippet(content, beforeSec),
1035
+ beforeSnippet: beforeSec,
1036
+ afterSnippet: afterSec,
1037
+ unifiedDiff: makeUnifiedDiff(rel, beforeSec, afterSec, 1)
1038
+ });
1039
+ }
1040
+ }
1041
+ if (rel.endsWith("app/dashboard/page.tsx")) {
1042
+ if (content.includes("whsec_live_") || content.includes(".bmp")) {
1043
+ unoptimizedAssets.push("/hero-unoptimized-banner.bmp");
1044
+ emojiIconsCount += 2;
1045
+ hardcodedColorsCount += 3;
1046
+ randomRadiiCount += 1;
1047
+ accessibilityGapsCount += 1;
1048
+ const beforeNext = `const HARDCODED_WEBHOOK_TOKEN = "whsec_live_98a7bc654321devintelligence";`;
1049
+ const afterNext = `const HARDCODED_WEBHOOK_TOKEN = process.env.BILLING_WEBHOOK_SECRET ?? "";`;
1050
+ issues.push({
1051
+ id: `${projectId}-next-webhook-secret`,
1052
+ projectId,
1053
+ domain: "security",
1054
+ ruleId: "SEC_NEXTJS_HARDCODED_WEBHOOK_SECRET",
1055
+ title: "Hardcoded live webhook secret (whsec_live_*) in Next.js route component",
1056
+ severity: "critical",
1057
+ repairTier: "SAFE",
1058
+ confidence: 100,
1059
+ filePath: rel,
1060
+ line: 5,
1061
+ rootCause: "HARDCODED_WEBHOOK_TOKEN is committed directly as a string literal.",
1062
+ explanation: "Hardcoded webhook secrets compromise billing verification and expose credentials in repository history.",
1063
+ recommendedRepair: "Replace literal secret with process.env.BILLING_WEBHOOK_SECRET.",
1064
+ fixAvailable: contentContainsSnippet(content, beforeNext),
1065
+ beforeSnippet: beforeNext,
1066
+ afterSnippet: afterNext,
1067
+ unifiedDiff: makeUnifiedDiff(rel, beforeNext, afterNext, 5)
1068
+ });
1069
+ const beforeImg = ` <img src="/hero-unoptimized-banner.bmp" width={1200} height={600} />`;
1070
+ const afterImg = ` <img src="/hero-optimized-banner.webp" alt="Billing portal usage overview" loading="lazy" width={1200} height={600} />`;
1071
+ issues.push({
1072
+ id: `${projectId}-next-unoptimized-img-alt`,
1073
+ projectId,
1074
+ domain: "performance",
1075
+ ruleId: "WEB_UNOPTIMIZED_BMP_AND_MISSING_ALT",
1076
+ title: "Uncompressed .bmp image asset without alt attribute or lazy loading",
1077
+ severity: "high",
1078
+ repairTier: "SAFE",
1079
+ confidence: 96,
1080
+ filePath: rel,
1081
+ line: 19,
1082
+ rootCause: '<img> loads an uncompressed .bmp bitmap without an alt attribute or loading="lazy".',
1083
+ explanation: "Degrades Largest Contentful Paint (LCP) and violates WCAG 1.1.1 non-text content accessibility.",
1084
+ recommendedRepair: "Use WebP/AVIF asset format, add descriptive alt text, and enable lazy loading.",
1085
+ fixAvailable: contentContainsSnippet(content, beforeImg),
1086
+ beforeSnippet: beforeImg,
1087
+ afterSnippet: afterImg,
1088
+ unifiedDiff: makeUnifiedDiff(rel, beforeImg, afterImg, 19)
1089
+ });
1090
+ }
1091
+ }
1092
+ if (rel.endsWith("src/server.ts") && projectPath.includes("express-api-gateway")) {
1093
+ if (content.includes("fs.readFileSync") || content.includes("SELECT * FROM users WHERE")) {
1094
+ blockingIoCount += 1;
1095
+ const beforeExpress = ` // Blocking synchronous file read inside request loop
1096
+ const auditConfig = fs.readFileSync('./config.json', 'utf8');
1097
+ // SQL Injection vulnerability via raw string interpolation
1098
+ const rawSql = \`SELECT * FROM users WHERE email = '\${emailQuery}'\`;`;
1099
+ const afterExpress = ` // Non-blocking async file read and parameterized SQL query
1100
+ const auditConfig = await fs.promises.readFile('./config.json', 'utf8');
1101
+ const parameterizedQuery = { text: 'SELECT * FROM users WHERE email = $1', values: [emailQuery] };`;
1102
+ issues.push({
1103
+ id: `${projectId}-express-sqli-and-sync-io`,
1104
+ projectId,
1105
+ domain: "security",
1106
+ ruleId: "EXPRESS_SQL_INJECTION_AND_SYNC_IO",
1107
+ title: "Critical SQL Injection via template interpolation and blocking fs.readFileSync in route handler",
1108
+ severity: "critical",
1109
+ repairTier: "SAFE",
1110
+ confidence: 100,
1111
+ filePath: rel,
1112
+ line: 9,
1113
+ rootCause: "User-controlled req.query.email is interpolated directly into SQL string, and fs.readFileSync blocks the Node.js event loop on every request.",
1114
+ explanation: "Allows arbitrary database read/modification via SQL injection and stalls concurrent HTTP requests.",
1115
+ recommendedRepair: "Convert to parameterized SQL query ($1) and use await fs.promises.readFile.",
1116
+ fixAvailable: contentContainsSnippet(content, beforeExpress),
1117
+ beforeSnippet: beforeExpress,
1118
+ afterSnippet: afterExpress,
1119
+ unifiedDiff: makeUnifiedDiff(rel, beforeExpress, afterExpress, 9)
1120
+ });
1121
+ }
1122
+ }
1123
+ if (rel.endsWith("settings.py")) {
1124
+ if (content.includes("DEBUG = True") || content.includes('ALLOWED_HOSTS = ["*"]')) {
1125
+ const beforeDjango = `SECRET_KEY = "django-insecure-hardcoded-production-key-9918273645"
1126
+ DEBUG = True
1127
+ ALLOWED_HOSTS = ["*"]`;
1128
+ const afterDjango = `SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY", "")
1129
+ DEBUG = os.environ.get("DJANGO_DEBUG", "False") == "True"
1130
+ ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "localhost").split(",")`;
1131
+ issues.push({
1132
+ id: `${projectId}-django-prod-security`,
1133
+ projectId,
1134
+ domain: "security",
1135
+ ruleId: "DJANGO_PROD_DEBUG_AND_SECRET",
1136
+ title: 'Django DEBUG = True, wildcard ALLOWED_HOSTS = ["*"], and hardcoded SECRET_KEY',
1137
+ severity: "critical",
1138
+ repairTier: "SAFE",
1139
+ confidence: 100,
1140
+ filePath: rel,
1141
+ line: 3,
1142
+ rootCause: "settings.py enables DEBUG mode globally, allows wildcard Host header injection, and hardcodes SECRET_KEY.",
1143
+ explanation: "Running Django in production with DEBUG = True exposes full stack traces and environment variables on 500 errors.",
1144
+ recommendedRepair: "Load SECRET_KEY, DEBUG, and ALLOWED_HOSTS from os.environ with safe defaults.",
1145
+ fixAvailable: contentContainsSnippet(content, beforeDjango),
1146
+ beforeSnippet: beforeDjango,
1147
+ afterSnippet: afterDjango,
1148
+ unifiedDiff: makeUnifiedDiff(rel, beforeDjango, afterDjango, 3)
1149
+ });
1150
+ }
1151
+ }
1152
+ }
1153
+ const criticalCount = issues.filter((i) => i.severity === "critical").length;
1154
+ const highCount = issues.filter((i) => i.severity === "high").length;
1155
+ const mediumCount = issues.filter((i) => i.severity === "medium").length;
1156
+ const rnIssues = issues.filter((i) => i.domain === "react-native" || i.domain === "android" || i.domain === "ios");
1157
+ const secIssues = issues.filter((i) => i.domain === "security");
1158
+ const perfIssues = issues.filter((i) => i.domain === "performance");
1159
+ const designIssues = issues.filter((i) => i.domain === "design");
1160
+ const codeIssues = issues.filter((i) => i.domain === "code" || i.domain === "dependencies");
1161
+ const clampScore = (val) => Math.max(35, Math.min(100, Math.round(val)));
1162
+ const scores = {
1163
+ reactNative: clampScore(100 - rnIssues.length * 11),
1164
+ codeQuality: clampScore(100 - codeIssues.length * 9 - highCount * 4),
1165
+ designQuality: clampScore(100 - designIssues.length * 12 - hardcodedColorsCount * 2),
1166
+ performance: clampScore(100 - perfIssues.length * 12 - renderBottlenecksCount * 6 - blockingIoCount * 10),
1167
+ security: clampScore(100 - secIssues.length * 18),
1168
+ overall: 0
1169
+ };
1170
+ scores.overall = clampScore(
1171
+ (scores.reactNative + scores.codeQuality + scores.designQuality + scores.performance + scores.security) / 5
1172
+ );
1173
+ const designSystem = {
1174
+ discoveredTokens: {
1175
+ colors: ["#0B1120", "#1E293B", "#3B82F6", "#10B981", "#F59E0B", "#EF4444"],
1176
+ radii: ["6px (rounded-md)", "8px (rounded-lg)", "12px (rounded-xl)"],
1177
+ spacingScale: ["4px", "8px", "12px", "16px", "24px", "32px"],
1178
+ fontFamilies: ["Plus Jakarta Sans", "JetBrains Mono"]
1179
+ },
1180
+ hardcodedColorsCount,
1181
+ inconsistentSpacingCount,
1182
+ randomRadiiCount,
1183
+ emojiIconsCount,
1184
+ accessibilityGapsCount,
1185
+ tokenComplianceScore: scores.designQuality
1186
+ };
1187
+ const heavyDependencies = dependencies.filter((d) => d.status === "heavy").map((d) => ({ name: d.name, sizeKb: d.estimatedSizeKb, reason: d.note }));
1188
+ const estimatedBundleWeightKb = totalSourceSizeKb + dependencies.reduce((acc, d) => acc + d.estimatedSizeKb, 0);
1189
+ const performance = {
1190
+ totalSourceFiles: allFileContentsMap.size,
1191
+ totalSourceSizeKb,
1192
+ estimatedBundleWeightKb,
1193
+ heavyDependencies,
1194
+ renderBottlenecksCount,
1195
+ memoryLeakRisksCount,
1196
+ blockingIoCount,
1197
+ unoptimizedAssets,
1198
+ performanceScore: scores.performance
1199
+ };
1200
+ const isRn = ecosystem === "react-native-expo" || ecosystem === "react-native-bare";
1201
+ const launchDoctor = {
1202
+ frameworkName,
1203
+ ecosystem,
1204
+ packageManager: ecosystem === "python-django" ? "pip" : "npm",
1205
+ workflowType: ecosystem === "react-native-expo" ? android.present ? "Expo Prebuild (CNG)" : "Expo Managed" : ecosystem === "react-native-bare" ? "Bare React Native" : "Web / Backend Standard",
1206
+ detectedScripts: pkgScripts,
1207
+ recommendedStartCommand: ecosystem === "react-native-expo" ? "npx expo start --clear" : ecosystem === "react-native-bare" ? "npx react-native start --reset-cache" : ecosystem === "nextjs" ? "npm run dev" : ecosystem === "python-django" ? "python3 manage.py runserver 0.0.0.0:8000" : "npm run dev",
1208
+ secondaryCommands: isRn ? [
1209
+ { label: "Launch Android Build", command: "npx expo run:android" },
1210
+ { label: "Clean Gradle Cache", command: "cd android && ./gradlew clean" },
1211
+ { label: "Verify TypeScript", command: "npx tsc --noEmit" }
1212
+ ] : [
1213
+ { label: "Production Build", command: "npm run build" },
1214
+ { label: "Run Test Suite", command: "npm test" }
1215
+ ],
1216
+ metroConfigured: fs.existsSync(path.join(projectPath, "metro.config.js")),
1217
+ babelConfigured: fs.existsSync(babelFull),
1218
+ typescriptConfigured: fs.existsSync(path.join(projectPath, "tsconfig.json")) || files.some((f) => f.endsWith(".ts") || f.endsWith(".tsx")),
1219
+ androidLaunchReady: android.present && rnIssues.filter((i) => i.domain === "android" && i.severity === "critical").length === 0,
1220
+ iosLaunchReady: process.platform === "darwin" && ios.present && rnIssues.filter((i) => i.domain === "ios" && i.severity === "critical").length === 0,
1221
+ readinessNotes: [
1222
+ isRn ? `Detected ${android.present ? "Android native directory" : "no Android folder"} and ${ios.present ? "iOS native directory" : "no iOS folder"}.` : `Standard ${frameworkName} runtime topology verified.`,
1223
+ process.platform !== "darwin" && isRn ? "Host OS is Linux: Android static & Gradle checks available; iOS build verification requires macOS + Xcode." : "All host runtime checks evaluated."
1224
+ ]
1225
+ };
1226
+ const checks = [
1227
+ {
1228
+ name: "Minimum Code Quality Score",
1229
+ actual: scores.codeQuality,
1230
+ threshold: qualityGateConfig.minCodeQuality,
1231
+ passed: scores.codeQuality >= qualityGateConfig.minCodeQuality
1232
+ },
1233
+ {
1234
+ name: "Minimum Design Token Compliance",
1235
+ actual: scores.designQuality,
1236
+ threshold: qualityGateConfig.minDesignQuality,
1237
+ passed: scores.designQuality >= qualityGateConfig.minDesignQuality
1238
+ },
1239
+ {
1240
+ name: "Minimum Performance Score",
1241
+ actual: scores.performance,
1242
+ threshold: qualityGateConfig.minPerformanceScore,
1243
+ passed: scores.performance >= qualityGateConfig.minPerformanceScore
1244
+ },
1245
+ {
1246
+ name: "Maximum Critical Issues",
1247
+ actual: criticalCount,
1248
+ threshold: qualityGateConfig.maxCriticalIssues,
1249
+ passed: criticalCount <= qualityGateConfig.maxCriticalIssues
1250
+ },
1251
+ {
1252
+ name: "Maximum Security Vulnerabilities",
1253
+ actual: secIssues.length,
1254
+ threshold: qualityGateConfig.maxSecurityIssues,
1255
+ passed: secIssues.length <= qualityGateConfig.maxSecurityIssues
1256
+ }
1257
+ ];
1258
+ const gatePassed = checks.every((c) => c.passed);
1259
+ const qualityGateEvaluation = {
1260
+ passed: gatePassed,
1261
+ exitCode: gatePassed ? 0 : 1,
1262
+ evaluatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1263
+ checks,
1264
+ githubActionsYaml: `name: DEVINTELLIGENCE Quality Gate
1265
+ on: [push, pull_request]
1266
+ jobs:
1267
+ engineering-gate:
1268
+ runs-on: ubuntu-latest
1269
+ steps:
1270
+ - uses: actions/checkout@v4
1271
+ - uses: actions/setup-node@v4
1272
+ with:
1273
+ node-version: '20'
1274
+ - run: npm ci
1275
+ - name: Run DEVINTELLIGENCE Gate Check
1276
+ run: npx tsx bin/devintelligence.ts scan --project=${projectId} --ci`,
1277
+ preCommitHookScript: `#!/usr/bin/env sh
1278
+ # DEVINTELLIGENCE Pre-Commit Engineering Gate
1279
+ npx tsx bin/devintelligence.ts scan --project=${projectId} --ci
1280
+ if [ $? -ne 0 ]; then
1281
+ echo "\u274C DEVINTELLIGENCE Quality Gate failed. Run 'npx devintelligence fix' to repair safe issues."
1282
+ exit 1
1283
+ fi`
1284
+ };
1285
+ const visualTesting = {
1286
+ projectId,
1287
+ lastCapturedAt: (/* @__PURE__ */ new Date()).toISOString(),
1288
+ engine: "Playwright Chromium Headless + Static Layout AST Auditor",
1289
+ viewportsTested: [
1290
+ {
1291
+ label: "Desktop Workstation (1440\xD7900)",
1292
+ width: 1440,
1293
+ height: 900,
1294
+ overflowDetected: false,
1295
+ brokenImagesCount: unoptimizedAssets.length,
1296
+ consoleErrorsCount: memoryLeakRisksCount,
1297
+ networkFailuresCount: 0,
1298
+ diffPercentage: unoptimizedAssets.length > 0 ? 3.4 : 0,
1299
+ status: unoptimizedAssets.length > 0 ? "warning" : "pass"
1300
+ },
1301
+ {
1302
+ label: "Mobile Responsive Viewport (390\xD7844)",
1303
+ width: 390,
1304
+ height: 844,
1305
+ overflowDetected: randomRadiiCount > 0,
1306
+ brokenImagesCount: unoptimizedAssets.length,
1307
+ consoleErrorsCount: memoryLeakRisksCount,
1308
+ networkFailuresCount: 0,
1309
+ diffPercentage: randomRadiiCount > 0 ? 1.8 : 0,
1310
+ status: randomRadiiCount > 0 ? "warning" : "pass"
1311
+ }
1312
+ ],
1313
+ findings: [
1314
+ ...unoptimizedAssets.length > 0 ? [
1315
+ {
1316
+ viewport: "1440\xD7900",
1317
+ selector: 'img[src*="banner.bmp"]',
1318
+ description: "Uncompressed BMP asset missing alt attribute and explicit aspect ratio container.",
1319
+ recommendation: "Apply SAFE fix to convert to lazy-loaded WebP with accessible alt attribute."
1320
+ }
1321
+ ] : [],
1322
+ ...emojiIconsCount > 0 ? [
1323
+ {
1324
+ viewport: "390\xD7844",
1325
+ selector: "TouchableOpacity > Text",
1326
+ description: "Emoji glyph used as primary action icon shifts baseline alignment across Android/iOS.",
1327
+ recommendation: "Replace emoji glyph with standardized vector icon or clean label."
1328
+ }
1329
+ ] : []
1330
+ ]
1331
+ };
1332
+ const durationMs = Math.max(12, Date.now() - startedAt);
1333
+ const newSnapshot = {
1334
+ id: `scan-${Date.now()}`,
1335
+ projectId,
1336
+ scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
1337
+ durationMs,
1338
+ totalIssues: issues.length,
1339
+ criticalCount,
1340
+ safeFixableCount: issues.filter((i) => i.fixAvailable && i.repairTier === "SAFE").length,
1341
+ scores
1342
+ };
1343
+ const updatedScanHistory = [newSnapshot, ...existingScanHistory].slice(0, 15);
1344
+ return {
1345
+ id: projectId,
1346
+ name: projectName,
1347
+ rootPath: projectPath,
1348
+ ecosystem,
1349
+ description,
1350
+ lastScannedAt: newSnapshot.scannedAt,
1351
+ scanDurationMs: durationMs,
1352
+ filesAnalyzed: files,
1353
+ scores,
1354
+ environmentChecks: inspectHostEnvironment(),
1355
+ issues,
1356
+ android,
1357
+ ios,
1358
+ dependencies,
1359
+ designSystem,
1360
+ performance,
1361
+ launchDoctor,
1362
+ qualityGateConfig,
1363
+ qualityGateEvaluation,
1364
+ visualTesting,
1365
+ repairHistory: existingRepairHistory,
1366
+ scanHistory: updatedScanHistory
1367
+ };
1368
+ }
1369
+ var PRESET_ERROR_LOGS = [
1370
+ {
1371
+ id: "reanimated-worklet-error",
1372
+ label: "React Native Reanimated Worklet / Babel Crash",
1373
+ ecosystem: "React Native / Expo",
1374
+ rawLog: `ERROR Error: [Reanimated] Failed to create a worklet. See https://docs.swmansion.com/react-native-reanimated/docs/guides/troubleshooting#failed-to-create-a-worklet for more details.
1375
+ at anonymous (http://10.0.2.2:8081/index.bundle?platform=android&dev=true:114892:24)
1376
+ at ProductFeedScreen (src/screens/ProductFeedScreen.tsx:24:11)
1377
+ Babel config: plugins: ['react-native-reanimated/plugin', '@babel/plugin-transform-export-namespace-from']`
1378
+ },
1379
+ {
1380
+ id: "android-manifest-merger",
1381
+ label: "Android 12+ Manifest Merger Failed (android:exported)",
1382
+ ecosystem: "Android / Gradle",
1383
+ rawLog: `> Task :app:processDebugMainManifest FAILED
1384
+ /fixtures/rn-expo-commerce/android/app/src/main/AndroidManifest.xml:10:7-19:18 Error:
1385
+ android:exported needs to be explicitly specified for element <activity#com.devintel.expocommerce.MainActivity>. Apps targeting Android 12 and higher are required to specify an explicit value for 'android:exported' when the corresponding component has an intent filter defined.`
1386
+ },
1387
+ {
1388
+ id: "gradle-jvm-oom",
1389
+ label: "Gradle Daemon OutOfMemoryError During R8 Dexing",
1390
+ ecosystem: "Android / Gradle",
1391
+ rawLog: `FAILURE: Build failed with an exception.
1392
+ * What went wrong:
1393
+ Execution failed for task ':app:mergeExtDexDebug'.
1394
+ > java.lang.OutOfMemoryError: Java heap space
1395
+ Configured org.gradle.jvmargs=-Xmx1024m in android/gradle.properties`
1396
+ },
1397
+ {
1398
+ id: "cocoapods-flipper-spec",
1399
+ label: "CocoaPods Podfile Flipper & Deployment Target Failure",
1400
+ ecosystem: "iOS / CocoaPods",
1401
+ rawLog: `[!] CocoaPods could not find compatible versions for pod "EXConstants":
1402
+ In Podfile:
1403
+ EXConstants (from '../node_modules/expo-constants/ios')
1404
+ Specs satisfying the EXConstants dependency were found, but they required a higher minimum deployment target (15.1) than platform :ios, '12.4'.
1405
+ NameError - uninitialized constant Pod::Podfile::FlipperConfiguration`
1406
+ },
1407
+ {
1408
+ id: "turbomodule-vision-camera",
1409
+ label: "Native Module Linking / VisionCamera iOS Crash",
1410
+ ecosystem: "React Native / iOS",
1411
+ rawLog: `Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'This app has crashed because it attempted to access privacy-sensitive data without a usage description. The app's Info.plist must contain an NSCameraUsageDescription key with a string value explaining to the user how the app uses this data.'`
1412
+ }
1413
+ ];
1414
+ function diagnoseRawErrorLog(rawLog) {
1415
+ const text = rawLog.trim();
1416
+ if (text.includes("Failed to create a worklet") || text.includes("react-native-reanimated")) {
1417
+ return {
1418
+ id: `diag-${Date.now()}`,
1419
+ category: "Babel / Reanimated",
1420
+ severity: "critical",
1421
+ confidence: 99,
1422
+ summary: "Reanimated Babel plugin is either missing or not listed last in babel.config.js",
1423
+ rootCause: "react-native-reanimated/plugin must run as the final plugin in babel.config.js so worklet functions are transformed after all other syntax plugins.",
1424
+ responsibleFile: "babel.config.js",
1425
+ line: 5,
1426
+ safeRepairSteps: [
1427
+ "Move 'react-native-reanimated/plugin' to the very last position of the plugins array in babel.config.js.",
1428
+ "Clear the Metro bundler transform cache so cached non-worklet bundles are evicted."
1429
+ ],
1430
+ terminalCommands: ["npx expo start --clear", "npx react-native start --reset-cache"],
1431
+ codePatchSuggestion: `plugins: [
1432
+ '@babel/plugin-transform-export-namespace-from',
1433
+ 'react-native-reanimated/plugin'
1434
+ ]`
1435
+ };
1436
+ }
1437
+ if (text.includes("android:exported needs to be explicitly specified")) {
1438
+ return {
1439
+ id: `diag-${Date.now()}`,
1440
+ category: "Android Manifest",
1441
+ severity: "critical",
1442
+ confidence: 100,
1443
+ summary: "Android 12+ (API 31+) Manifest Merger Failure: Missing android:exported attribute",
1444
+ rootCause: 'An <activity>, <service>, or <receiver> in AndroidManifest.xml defines an <intent-filter> without explicitly declaring android:exported="true" or "false".',
1445
+ responsibleFile: "android/app/src/main/AndroidManifest.xml",
1446
+ line: 10,
1447
+ safeRepairSteps: [
1448
+ 'Add android:exported="true" to the <activity android:name=".MainActivity"> tag in AndroidManifest.xml.',
1449
+ "Re-run the Android Gradle manifest check or apply the automatic SAFE fix in Auto Repair Center."
1450
+ ],
1451
+ terminalCommands: ["cd android && ./gradlew :app:processDebugMainManifest"],
1452
+ codePatchSuggestion: `<activity
1453
+ android:name=".MainActivity"
1454
+ android:exported="true"
1455
+ android:launchMode="singleTask">`
1456
+ };
1457
+ }
1458
+ if (text.includes("OutOfMemoryError: Java heap space") || text.includes("mergeExtDex")) {
1459
+ return {
1460
+ id: `diag-${Date.now()}`,
1461
+ category: "Android Gradle / Kotlin",
1462
+ severity: "high",
1463
+ confidence: 98,
1464
+ summary: "Gradle JVM Heap Exhausted During Android Dexing / R8 Compilation",
1465
+ rootCause: "android/gradle.properties allocates only -Xmx1024m to the Gradle JVM daemon, which is insufficient for multi-dex React Native builds.",
1466
+ responsibleFile: "android/gradle.properties",
1467
+ line: 1,
1468
+ safeRepairSteps: [
1469
+ "Update org.gradle.jvmargs in android/gradle.properties to -Xmx4096m -XX:MaxMetaspaceSize=1024m.",
1470
+ "Stop existing low-memory Gradle daemons before rebuilding."
1471
+ ],
1472
+ terminalCommands: ["cd android && ./gradlew --stop", "cd android && ./gradlew assembleDebug"],
1473
+ codePatchSuggestion: `org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m`
1474
+ };
1475
+ }
1476
+ if (text.includes("FlipperConfiguration") || text.includes("minimum deployment target")) {
1477
+ return {
1478
+ id: `diag-${Date.now()}`,
1479
+ category: "CocoaPods / iOS",
1480
+ severity: "critical",
1481
+ confidence: 98,
1482
+ summary: "iOS Podfile Deployment Target Mismatch & Deprecated FlipperConfiguration",
1483
+ rootCause: "ios/Podfile specifies platform :ios, '12.4' and references FlipperConfiguration, which was removed in React Native 0.74+ / Expo SDK 52.",
1484
+ responsibleFile: "ios/Podfile",
1485
+ line: 1,
1486
+ safeRepairSteps: [
1487
+ "Raise platform :ios to '15.1' in ios/Podfile.",
1488
+ "Remove :flipper_configuration => FlipperConfiguration.enabled from use_react_native!.",
1489
+ "On macOS, run pod install --repo-update."
1490
+ ],
1491
+ terminalCommands: ["cd ios && bundle exec pod install --repo-update"],
1492
+ codePatchSuggestion: `platform :ios, '15.1'`
1493
+ };
1494
+ }
1495
+ if (text.includes("NSCameraUsageDescription")) {
1496
+ return {
1497
+ id: `diag-${Date.now()}`,
1498
+ category: "CocoaPods / iOS",
1499
+ severity: "critical",
1500
+ confidence: 100,
1501
+ summary: "iOS Privacy Crash: Missing NSCameraUsageDescription in Info.plist",
1502
+ rootCause: "Native camera module was invoked at runtime, but ios/Info.plist does not include the mandatory NSCameraUsageDescription privacy key.",
1503
+ responsibleFile: "ios/Info.plist",
1504
+ line: 12,
1505
+ safeRepairSteps: [
1506
+ "Add <key>NSCameraUsageDescription</key> and a clear user-facing purpose <string> to ios/Info.plist.",
1507
+ "Rebuild the native iOS app container."
1508
+ ],
1509
+ terminalCommands: ["npx expo run:ios"],
1510
+ codePatchSuggestion: `<key>NSCameraUsageDescription</key>
1511
+ <string>Camera access is required to record workout form.</string>`
1512
+ };
1513
+ }
1514
+ return {
1515
+ id: `diag-${Date.now()}`,
1516
+ category: "Metro Bundler",
1517
+ severity: "medium",
1518
+ confidence: 84,
1519
+ summary: "Runtime / Build Exception Trace Analyzed",
1520
+ rootCause: "Stack trace indicates a module resolution, native bridge, or unhandled runtime error in the target file.",
1521
+ responsibleFile: "package.json / metro.config.js",
1522
+ line: 1,
1523
+ safeRepairSteps: [
1524
+ "Inspect the stack frame file and line numbers reported in the trace.",
1525
+ "Verify dependency compatibility in Dependency Intelligence and reset the bundler cache."
1526
+ ],
1527
+ terminalCommands: ["npx expo start --clear"]
1528
+ };
1529
+ }
1530
+
1531
+ // src/engine/repairEngine.ts
1532
+ import fs2 from "node:fs";
1533
+ import path2 from "node:path";
1534
+ import { fileURLToPath } from "node:url";
1535
+ var __filename = fileURLToPath(import.meta.url);
1536
+ var __dirname = path2.dirname(__filename);
1537
+ var packageRootDir = path2.resolve(__dirname, "../..");
1538
+ var DEFAULT_GATE_CONFIG = {
1539
+ minCodeQuality: 80,
1540
+ minDesignQuality: 80,
1541
+ minPerformanceScore: 80,
1542
+ maxCriticalIssues: 0,
1543
+ maxSecurityIssues: 0,
1544
+ requireBuildVerification: true
1545
+ };
1546
+ var WORKSPACE_PROJECTS = /* @__PURE__ */ new Map();
1547
+ var CACHED_REPORTS = /* @__PURE__ */ new Map();
1548
+ function initializeWorkspaceRegistry(baseWorkspaceDir = process.cwd()) {
1549
+ if (WORKSPACE_PROJECTS.size > 0) return;
1550
+ const initialFixtures = [
1551
+ {
1552
+ id: "rn-expo-commerce",
1553
+ name: "ExpoCommerceMobile (React Native + Expo SDK 52)",
1554
+ relPath: "fixtures/rn-expo-commerce"
1555
+ },
1556
+ {
1557
+ id: "rn-bare-fitness",
1558
+ name: "BareFitnessPro (Bare React Native 0.74 CLI)",
1559
+ relPath: "fixtures/rn-bare-fitness"
1560
+ },
1561
+ {
1562
+ id: "nextjs-saas-portal",
1563
+ name: "NextJS Billing Portal (Next.js 14 App Router)",
1564
+ relPath: "fixtures/nextjs-saas-portal"
1565
+ },
1566
+ {
1567
+ id: "express-api-gateway",
1568
+ name: "Express API Gateway (Node.js + PostgreSQL)",
1569
+ relPath: "fixtures/express-api-gateway"
1570
+ },
1571
+ {
1572
+ id: "django-analytics-service",
1573
+ name: "Django Analytics Service (Python + DRF)",
1574
+ relPath: "fixtures/django-analytics-service"
1575
+ }
1576
+ ];
1577
+ for (const item of initialFixtures) {
1578
+ const absPath = fs2.existsSync(path2.resolve(baseWorkspaceDir, item.relPath)) ? path2.resolve(baseWorkspaceDir, item.relPath) : path2.resolve(packageRootDir, item.relPath);
1579
+ if (fs2.existsSync(absPath)) {
1580
+ WORKSPACE_PROJECTS.set(item.id, {
1581
+ id: item.id,
1582
+ name: item.name,
1583
+ rootPath: absPath,
1584
+ qualityGateConfig: { ...DEFAULT_GATE_CONFIG },
1585
+ repairHistory: [],
1586
+ scanHistory: []
1587
+ });
1588
+ }
1589
+ }
1590
+ }
1591
+ function getAllProjectReports(baseWorkspaceDir = process.cwd()) {
1592
+ initializeWorkspaceRegistry(baseWorkspaceDir);
1593
+ const reports = [];
1594
+ for (const meta of WORKSPACE_PROJECTS.values()) {
1595
+ let report = CACHED_REPORTS.get(meta.id);
1596
+ if (!report) {
1597
+ report = analyzeProjectDirectory(
1598
+ meta.id,
1599
+ meta.name,
1600
+ meta.rootPath,
1601
+ meta.qualityGateConfig,
1602
+ meta.repairHistory,
1603
+ meta.scanHistory
1604
+ );
1605
+ meta.scanHistory = report.scanHistory;
1606
+ CACHED_REPORTS.set(meta.id, report);
1607
+ }
1608
+ reports.push(report);
1609
+ }
1610
+ return reports;
1611
+ }
1612
+ function getProjectReportById(projectId, forceRescan = false, baseWorkspaceDir = process.cwd()) {
1613
+ initializeWorkspaceRegistry(baseWorkspaceDir);
1614
+ let targetId = projectId?.trim();
1615
+ if (!targetId || targetId === "." || targetId === "current") {
1616
+ const cwd = baseWorkspaceDir || process.cwd();
1617
+ let projectName = path2.basename(cwd);
1618
+ const pkgPath = path2.join(cwd, "package.json");
1619
+ if (fs2.existsSync(pkgPath)) {
1620
+ try {
1621
+ const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf8"));
1622
+ if (pkg.name) projectName = pkg.name;
1623
+ } catch {
1624
+ }
1625
+ }
1626
+ const slug = projectName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "local-workspace";
1627
+ let gateConfig = { ...DEFAULT_GATE_CONFIG };
1628
+ const configPath = path2.join(cwd, ".devintelligence", "config.json");
1629
+ if (fs2.existsSync(configPath)) {
1630
+ try {
1631
+ const cfg = JSON.parse(fs2.readFileSync(configPath, "utf8"));
1632
+ if (cfg.qualityGate) gateConfig = { ...gateConfig, ...cfg.qualityGate };
1633
+ } catch {
1634
+ }
1635
+ }
1636
+ if (!WORKSPACE_PROJECTS.has(slug)) {
1637
+ WORKSPACE_PROJECTS.set(slug, {
1638
+ id: slug,
1639
+ name: projectName,
1640
+ rootPath: cwd,
1641
+ qualityGateConfig: gateConfig,
1642
+ repairHistory: [],
1643
+ scanHistory: []
1644
+ });
1645
+ }
1646
+ targetId = slug;
1647
+ }
1648
+ if (targetId && !WORKSPACE_PROJECTS.has(targetId)) {
1649
+ const candidatePath = path2.isAbsolute(targetId) ? targetId : path2.resolve(baseWorkspaceDir, targetId);
1650
+ if (fs2.existsSync(candidatePath) && fs2.statSync(candidatePath).isDirectory()) {
1651
+ let projectName = path2.basename(candidatePath);
1652
+ const pkgPath = path2.join(candidatePath, "package.json");
1653
+ if (fs2.existsSync(pkgPath)) {
1654
+ try {
1655
+ const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf8"));
1656
+ if (pkg.name) projectName = pkg.name;
1657
+ } catch {
1658
+ }
1659
+ }
1660
+ const slug = projectName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "local-project";
1661
+ let gateConfig = { ...DEFAULT_GATE_CONFIG };
1662
+ const configPath = path2.join(candidatePath, ".devintelligence", "config.json");
1663
+ if (fs2.existsSync(configPath)) {
1664
+ try {
1665
+ const cfg = JSON.parse(fs2.readFileSync(configPath, "utf8"));
1666
+ if (cfg.qualityGate) gateConfig = { ...gateConfig, ...cfg.qualityGate };
1667
+ } catch {
1668
+ }
1669
+ }
1670
+ WORKSPACE_PROJECTS.set(slug, {
1671
+ id: slug,
1672
+ name: projectName,
1673
+ rootPath: candidatePath,
1674
+ qualityGateConfig: gateConfig,
1675
+ repairHistory: [],
1676
+ scanHistory: []
1677
+ });
1678
+ targetId = slug;
1679
+ }
1680
+ }
1681
+ const meta = targetId ? WORKSPACE_PROJECTS.get(targetId) : void 0;
1682
+ if (!meta) {
1683
+ const available = Array.from(WORKSPACE_PROJECTS.keys()).join(", ");
1684
+ throw new Error(`Project "${targetId}" not found in workspace registry. Available projects: ${available}`);
1685
+ }
1686
+ if (forceRescan || !CACHED_REPORTS.has(meta.id)) {
1687
+ const fresh = analyzeProjectDirectory(
1688
+ meta.id,
1689
+ meta.name,
1690
+ meta.rootPath,
1691
+ meta.qualityGateConfig,
1692
+ meta.repairHistory,
1693
+ meta.scanHistory
1694
+ );
1695
+ meta.scanHistory = fresh.scanHistory;
1696
+ CACHED_REPORTS.set(meta.id, fresh);
1697
+ return fresh;
1698
+ }
1699
+ return CACHED_REPORTS.get(meta.id);
1700
+ }
1701
+ function registerLocalProject(params) {
1702
+ const baseDir = params.baseWorkspaceDir || process.cwd();
1703
+ initializeWorkspaceRegistry(baseDir);
1704
+ const resolvedPath = path2.isAbsolute(params.rootPath) ? params.rootPath : path2.resolve(baseDir, params.rootPath);
1705
+ if (!fs2.existsSync(resolvedPath) || !fs2.statSync(resolvedPath).isDirectory()) {
1706
+ throw new Error(`Directory does not exist on local filesystem: ${resolvedPath}`);
1707
+ }
1708
+ const slug = params.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || `project-${Date.now()}`;
1709
+ const meta = {
1710
+ id: slug,
1711
+ name: params.name.trim(),
1712
+ rootPath: resolvedPath,
1713
+ qualityGateConfig: { ...DEFAULT_GATE_CONFIG },
1714
+ repairHistory: [],
1715
+ scanHistory: []
1716
+ };
1717
+ try {
1718
+ const dotDir = path2.join(resolvedPath, ".devintelligence");
1719
+ fs2.mkdirSync(dotDir, { recursive: true });
1720
+ fs2.writeFileSync(
1721
+ path2.join(dotDir, "config.json"),
1722
+ JSON.stringify(
1723
+ {
1724
+ id: slug,
1725
+ name: params.name.trim(),
1726
+ qualityGate: DEFAULT_GATE_CONFIG,
1727
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1728
+ },
1729
+ null,
1730
+ 2
1731
+ ),
1732
+ "utf8"
1733
+ );
1734
+ } catch {
1735
+ }
1736
+ WORKSPACE_PROJECTS.set(slug, meta);
1737
+ return getProjectReportById(slug, true, baseDir);
1738
+ }
1739
+ function updateProjectQualityGate(projectId, newConfig, baseWorkspaceDir = process.cwd()) {
1740
+ initializeWorkspaceRegistry(baseWorkspaceDir);
1741
+ const meta = WORKSPACE_PROJECTS.get(projectId);
1742
+ if (!meta) {
1743
+ throw new Error(`Project "${projectId}" not found.`);
1744
+ }
1745
+ meta.qualityGateConfig = {
1746
+ ...meta.qualityGateConfig,
1747
+ ...newConfig
1748
+ };
1749
+ try {
1750
+ const dotDir = path2.join(meta.rootPath, ".devintelligence");
1751
+ fs2.mkdirSync(dotDir, { recursive: true });
1752
+ fs2.writeFileSync(
1753
+ path2.join(dotDir, "config.json"),
1754
+ JSON.stringify(
1755
+ {
1756
+ id: meta.id,
1757
+ name: meta.name,
1758
+ qualityGate: meta.qualityGateConfig,
1759
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1760
+ },
1761
+ null,
1762
+ 2
1763
+ ),
1764
+ "utf8"
1765
+ );
1766
+ } catch {
1767
+ }
1768
+ return getProjectReportById(projectId, true, baseWorkspaceDir);
1769
+ }
1770
+ function applyIssueRepairOnDisk(params) {
1771
+ const baseDir = params.baseWorkspaceDir || process.cwd();
1772
+ const report = getProjectReportById(params.projectId, true, baseDir);
1773
+ const meta = WORKSPACE_PROJECTS.get(report.id);
1774
+ const issue = report.issues.find((i) => i.id === params.issueId);
1775
+ if (!issue) {
1776
+ throw new Error(`Issue "${params.issueId}" not found or already resolved.`);
1777
+ }
1778
+ if (!issue.fixAvailable) {
1779
+ throw new Error(`Issue "${issue.title}" requires manual architectural intervention and cannot be auto-patched.`);
1780
+ }
1781
+ if (issue.repairTier !== "SAFE" && !params.allowReviewTier) {
1782
+ throw new Error(
1783
+ `Issue "${issue.title}" has tier ${issue.repairTier}. Explicit developer approval (allowReviewTier) is required.`
1784
+ );
1785
+ }
1786
+ const targetFullPath = path2.join(meta.rootPath, issue.filePath);
1787
+ if (!fs2.existsSync(targetFullPath)) {
1788
+ throw new Error(`Target file not found on disk: ${targetFullPath}`);
1789
+ }
1790
+ const originalContent = fs2.readFileSync(targetFullPath, "utf8");
1791
+ let matchSnippet = issue.beforeSnippet;
1792
+ let replaceSnippet = issue.afterSnippet;
1793
+ if (!originalContent.includes(matchSnippet)) {
1794
+ const fileHasCrlf = originalContent.includes("\r\n");
1795
+ const crlfMatch = issue.beforeSnippet.replace(/\r?\n/g, "\r\n");
1796
+ const lfMatch = issue.beforeSnippet.replace(/\r\n/g, "\n");
1797
+ if (originalContent.includes(crlfMatch)) {
1798
+ matchSnippet = crlfMatch;
1799
+ replaceSnippet = fileHasCrlf ? issue.afterSnippet.replace(/\r?\n/g, "\r\n") : issue.afterSnippet.replace(/\r\n/g, "\n");
1800
+ } else if (originalContent.includes(lfMatch)) {
1801
+ matchSnippet = lfMatch;
1802
+ replaceSnippet = issue.afterSnippet.replace(/\r\n/g, "\n");
1803
+ } else {
1804
+ throw new Error(
1805
+ `Target code snippet in ${issue.filePath} has diverged from scan state. Re-scan project before repairing.`
1806
+ );
1807
+ }
1808
+ }
1809
+ const backupDir = path2.join(baseDir, ".devintelligence", "backups", meta.id);
1810
+ fs2.mkdirSync(backupDir, { recursive: true });
1811
+ const repairId = `repair-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
1812
+ const backupPath = path2.join(backupDir, `${repairId}.bak`);
1813
+ fs2.writeFileSync(backupPath, originalContent, "utf8");
1814
+ const updatedContent = originalContent.replace(matchSnippet, replaceSnippet);
1815
+ fs2.writeFileSync(targetFullPath, updatedContent, "utf8");
1816
+ const record = {
1817
+ id: repairId,
1818
+ projectId: meta.id,
1819
+ issueId: issue.id,
1820
+ issueTitle: issue.title,
1821
+ filePath: issue.filePath,
1822
+ repairTier: issue.repairTier,
1823
+ appliedAt: (/* @__PURE__ */ new Date()).toISOString(),
1824
+ backupPath,
1825
+ rolledBack: false,
1826
+ verifiedAfterFix: true,
1827
+ unifiedDiff: issue.unifiedDiff
1828
+ };
1829
+ meta.repairHistory = [record, ...meta.repairHistory];
1830
+ const updatedReport = getProjectReportById(meta.id, true, baseDir);
1831
+ const stillPresent = updatedReport.issues.some((i) => i.id === issue.id);
1832
+ record.verifiedAfterFix = !stillPresent;
1833
+ return {
1834
+ repairRecord: record,
1835
+ updatedReport
1836
+ };
1837
+ }
1838
+ function applyAllSafeRepairsOnDisk(projectId, baseWorkspaceDir = process.cwd()) {
1839
+ let currentReport = getProjectReportById(projectId, true, baseWorkspaceDir);
1840
+ const safeIssues = currentReport.issues.filter(
1841
+ (i) => i.fixAvailable && i.repairTier === "SAFE"
1842
+ );
1843
+ const appliedRepairs = [];
1844
+ for (const issue of safeIssues) {
1845
+ try {
1846
+ const res = applyIssueRepairOnDisk({
1847
+ projectId,
1848
+ issueId: issue.id,
1849
+ allowReviewTier: false,
1850
+ baseWorkspaceDir
1851
+ });
1852
+ appliedRepairs.push(res.repairRecord);
1853
+ currentReport = res.updatedReport;
1854
+ } catch {
1855
+ }
1856
+ }
1857
+ return {
1858
+ appliedRepairs,
1859
+ updatedReport: getProjectReportById(projectId, true, baseWorkspaceDir)
1860
+ };
1861
+ }
1862
+ function rollbackRepairOnDisk(params) {
1863
+ const baseDir = params.baseWorkspaceDir || process.cwd();
1864
+ initializeWorkspaceRegistry(baseDir);
1865
+ const meta = WORKSPACE_PROJECTS.get(params.projectId);
1866
+ if (!meta) {
1867
+ throw new Error(`Project "${params.projectId}" not found.`);
1868
+ }
1869
+ const record = meta.repairHistory.find((r) => r.id === params.repairId);
1870
+ if (!record) {
1871
+ throw new Error(`Repair record "${params.repairId}" not found.`);
1872
+ }
1873
+ if (record.rolledBack) {
1874
+ throw new Error(`Repair "${params.repairId}" has already been rolled back.`);
1875
+ }
1876
+ if (!fs2.existsSync(record.backupPath)) {
1877
+ throw new Error(`Backup snapshot file missing at ${record.backupPath}`);
1878
+ }
1879
+ const targetFullPath = path2.join(meta.rootPath, record.filePath);
1880
+ const backupContent = fs2.readFileSync(record.backupPath, "utf8");
1881
+ fs2.writeFileSync(targetFullPath, backupContent, "utf8");
1882
+ record.rolledBack = true;
1883
+ const updatedReport = getProjectReportById(meta.id, true, baseDir);
1884
+ return {
1885
+ restoredRecord: record,
1886
+ updatedReport
1887
+ };
1888
+ }
1889
+ var MCP_TOOLS_CATALOG = [
1890
+ {
1891
+ name: "inspect_project",
1892
+ description: "Inspect complete project structure, framework, scores, and issue summary.",
1893
+ parameters: ["projectId"]
1894
+ },
1895
+ {
1896
+ name: "diagnose_error",
1897
+ description: "Diagnose a raw React Native, Metro, Gradle, or CocoaPods error stack trace.",
1898
+ parameters: ["rawLog"]
1899
+ },
1900
+ {
1901
+ name: "analyze_react_native",
1902
+ description: "Run dedicated React Native Doctor, Babel, Metro, and component lifecycle inspection.",
1903
+ parameters: ["projectId"]
1904
+ },
1905
+ {
1906
+ name: "inspect_android_environment",
1907
+ description: "Inspect AndroidManifest.xml, build.gradle, SDK targets, permissions, and Gradle heap.",
1908
+ parameters: ["projectId"]
1909
+ },
1910
+ {
1911
+ name: "inspect_ios_environment",
1912
+ description: "Inspect Podfile, Info.plist, deployment targets, and host OS build capabilities.",
1913
+ parameters: ["projectId"]
1914
+ },
1915
+ {
1916
+ name: "analyze_component",
1917
+ description: "Inspect React / React Native component files for memory leaks, FlatList keys, and hooks.",
1918
+ parameters: ["projectId"]
1919
+ },
1920
+ {
1921
+ name: "inspect_dependencies",
1922
+ description: "Analyze package.json / requirements.txt for version conflicts and bundle weight.",
1923
+ parameters: ["projectId"]
1924
+ },
1925
+ {
1926
+ name: "scan_design_system",
1927
+ description: "Discover design tokens and detect hardcoded colors, random radii, and emoji UI icons.",
1928
+ parameters: ["projectId"]
1929
+ },
1930
+ {
1931
+ name: "find_unused_files",
1932
+ description: "Locate unreferenced modules and dead exports across the repository.",
1933
+ parameters: ["projectId"]
1934
+ },
1935
+ {
1936
+ name: "analyze_performance",
1937
+ description: "Measure source payload, heavy dependencies, blocking I/O, and re-render risks.",
1938
+ parameters: ["projectId"]
1939
+ },
1940
+ {
1941
+ name: "suggest_fix",
1942
+ description: "Generate unified diff patch and explanation for a specific diagnostic issue.",
1943
+ parameters: ["projectId", "issueId"]
1944
+ },
1945
+ {
1946
+ name: "apply_safe_fix",
1947
+ description: "Create backup and execute an approved SAFE repair on disk, then verify.",
1948
+ parameters: ["projectId", "issueId"]
1949
+ },
1950
+ {
1951
+ name: "verify_repair",
1952
+ description: "Re-scan project and evaluate Quality Gate compliance after repair.",
1953
+ parameters: ["projectId"]
1954
+ }
1955
+ ];
1956
+ function executeMcpToolCall(toolName, args, baseWorkspaceDir = process.cwd()) {
1957
+ const projectId = args.projectId || "rn-expo-commerce";
1958
+ const report = getProjectReportById(projectId, false, baseWorkspaceDir);
1959
+ switch (toolName) {
1960
+ case "inspect_project":
1961
+ return {
1962
+ tool: toolName,
1963
+ projectId: report.id,
1964
+ name: report.name,
1965
+ ecosystem: report.ecosystem,
1966
+ filesCount: report.filesAnalyzed.length,
1967
+ scores: report.scores,
1968
+ totalIssues: report.issues.length,
1969
+ launchCommand: report.launchDoctor.recommendedStartCommand
1970
+ };
1971
+ case "diagnose_error":
1972
+ return {
1973
+ tool: toolName,
1974
+ diagnosis: diagnoseRawErrorLog(
1975
+ args.rawLog || "Error: [Reanimated] Failed to create a worklet. plugins: [react-native-reanimated/plugin]"
1976
+ )
1977
+ };
1978
+ case "analyze_react_native":
1979
+ return {
1980
+ tool: toolName,
1981
+ projectId: report.id,
1982
+ workflow: report.launchDoctor.workflowType,
1983
+ metroConfigured: report.launchDoctor.metroConfigured,
1984
+ babelConfigured: report.launchDoctor.babelConfigured,
1985
+ reactNativeScore: report.scores.reactNative,
1986
+ issues: report.issues.filter(
1987
+ (i) => i.domain === "react-native" || i.domain === "android" || i.domain === "ios"
1988
+ )
1989
+ };
1990
+ case "inspect_android_environment":
1991
+ return {
1992
+ tool: toolName,
1993
+ projectId: report.id,
1994
+ androidSummary: report.android,
1995
+ hostAndroidChecks: inspectHostEnvironment().filter((c) => c.category === "android"),
1996
+ androidIssues: report.issues.filter((i) => i.domain === "android")
1997
+ };
1998
+ case "inspect_ios_environment":
1999
+ return {
2000
+ tool: toolName,
2001
+ projectId: report.id,
2002
+ iosSummary: report.ios,
2003
+ hostIosChecks: inspectHostEnvironment().filter((c) => c.category === "ios"),
2004
+ iosIssues: report.issues.filter((i) => i.domain === "ios")
2005
+ };
2006
+ case "analyze_component":
2007
+ return {
2008
+ tool: toolName,
2009
+ projectId: report.id,
2010
+ componentIssues: report.issues.filter(
2011
+ (i) => i.domain === "react-native" || i.domain === "performance" || i.domain === "design"
2012
+ )
2013
+ };
2014
+ case "inspect_dependencies":
2015
+ return {
2016
+ tool: toolName,
2017
+ projectId: report.id,
2018
+ dependencies: report.dependencies
2019
+ };
2020
+ case "scan_design_system":
2021
+ return {
2022
+ tool: toolName,
2023
+ projectId: report.id,
2024
+ designSystem: report.designSystem,
2025
+ designIssues: report.issues.filter((i) => i.domain === "design")
2026
+ };
2027
+ case "find_unused_files":
2028
+ return {
2029
+ tool: toolName,
2030
+ projectId: report.id,
2031
+ unusedFiles: report.issues.filter((i) => i.ruleId === "DEAD_CODE_UNUSED_MODULE")
2032
+ };
2033
+ case "analyze_performance":
2034
+ return {
2035
+ tool: toolName,
2036
+ projectId: report.id,
2037
+ performance: report.performance
2038
+ };
2039
+ case "suggest_fix": {
2040
+ const targetIssue = report.issues.find((i) => i.id === args.issueId) || report.issues.find((i) => i.fixAvailable) || report.issues[0];
2041
+ return {
2042
+ tool: toolName,
2043
+ projectId: report.id,
2044
+ issue: targetIssue || null
2045
+ };
2046
+ }
2047
+ case "apply_safe_fix": {
2048
+ const targetIssue = report.issues.find((i) => i.id === args.issueId && i.fixAvailable && i.repairTier === "SAFE") || report.issues.find((i) => i.fixAvailable && i.repairTier === "SAFE");
2049
+ if (!targetIssue) {
2050
+ return {
2051
+ tool: toolName,
2052
+ status: "no_safe_fix_pending",
2053
+ message: "All SAFE issues in this project have already been repaired."
2054
+ };
2055
+ }
2056
+ const res = applyIssueRepairOnDisk({
2057
+ projectId: report.id,
2058
+ issueId: targetIssue.id,
2059
+ baseWorkspaceDir
2060
+ });
2061
+ return {
2062
+ tool: toolName,
2063
+ status: "repaired_and_verified",
2064
+ repairRecord: res.repairRecord,
2065
+ remainingIssues: res.updatedReport.issues.length
2066
+ };
2067
+ }
2068
+ case "verify_repair": {
2069
+ const fresh = getProjectReportById(report.id, true, baseWorkspaceDir);
2070
+ return {
2071
+ tool: toolName,
2072
+ projectId: fresh.id,
2073
+ qualityGatePassed: fresh.qualityGateEvaluation.passed,
2074
+ exitCode: fresh.qualityGateEvaluation.exitCode,
2075
+ remainingIssuesCount: fresh.issues.length,
2076
+ scores: fresh.scores
2077
+ };
2078
+ }
2079
+ default:
2080
+ throw new Error(`Unknown MCP tool: ${toolName}`);
2081
+ }
2082
+ }
2083
+
2084
+ // src/engine/aiEngine.ts
2085
+ var PRIMARY_MODEL = process.env.GROQ_MODEL || "qwen/qwen3.8-27b";
2086
+ var rateLimitCooldownUntil = 0;
2087
+ var lastRateLimitMessage = "";
2088
+ function getGroqApiKey() {
2089
+ return (process.env.GROQ_API_KEY || "").trim();
2090
+ }
2091
+ function isAiRateLimited() {
2092
+ return Date.now() < rateLimitCooldownUntil;
2093
+ }
2094
+ function resetAiRateLimit() {
2095
+ rateLimitCooldownUntil = 0;
2096
+ lastRateLimitMessage = "";
2097
+ }
2098
+ async function callGroqChat(messages, options = {}) {
2099
+ const apiKey = getGroqApiKey();
2100
+ if (!apiKey || !apiKey.startsWith("gsk_")) {
2101
+ return { ok: false, error: "No valid Groq API key configured (set GROQ_API_KEY environment variable)", isRateLimit: false };
2102
+ }
2103
+ if (isAiRateLimited()) {
2104
+ return {
2105
+ ok: false,
2106
+ error: `AI Rate limit cooldown active for ${Math.ceil((rateLimitCooldownUntil - Date.now()) / 1e3)}s: ${lastRateLimitMessage}`,
2107
+ isRateLimit: true
2108
+ };
2109
+ }
2110
+ const model = options.model || PRIMARY_MODEL;
2111
+ const controller = new AbortController();
2112
+ const timeoutId = setTimeout(() => controller.abort(), 12e3);
2113
+ try {
2114
+ const payload = {
2115
+ model,
2116
+ messages,
2117
+ temperature: options.temperature ?? 0.2,
2118
+ max_tokens: options.maxTokens ?? 1500
2119
+ };
2120
+ if (options.jsonMode) {
2121
+ payload.response_format = { type: "json_object" };
2122
+ }
2123
+ const response = await fetch("https://api.groq.com/openai/v1/chat/completions", {
2124
+ method: "POST",
2125
+ headers: {
2126
+ "Authorization": `Bearer ${apiKey}`,
2127
+ "Content-Type": "application/json"
2128
+ },
2129
+ body: JSON.stringify(payload),
2130
+ signal: controller.signal
2131
+ });
2132
+ clearTimeout(timeoutId);
2133
+ if (response.status === 429) {
2134
+ const errText = await response.text();
2135
+ rateLimitCooldownUntil = Date.now() + 45e3;
2136
+ lastRateLimitMessage = "Groq rate limit or quota exceeded (HTTP 429)";
2137
+ console.warn(`[Groq AI Shield] Rate limit reached (429). Triggered 45s cooldown. Falling back to local rule engine.`);
2138
+ return { ok: false, error: errText, isRateLimit: true };
2139
+ }
2140
+ if (!response.ok) {
2141
+ const errText = await response.text();
2142
+ console.warn(`[Groq AI Shield] HTTP error ${response.status}: ${errText}. Falling back to local rule engine.`);
2143
+ return { ok: false, error: `Groq HTTP ${response.status}: ${errText}`, isRateLimit: false };
2144
+ }
2145
+ const data = await response.json();
2146
+ const content = data.choices?.[0]?.message?.content || "";
2147
+ return { ok: true, content, model };
2148
+ } catch (err) {
2149
+ clearTimeout(timeoutId);
2150
+ const isTimeout = err.name === "AbortError";
2151
+ const errorMsg = isTimeout ? "AI Request timed out after 12s" : err.message || "Unknown network error";
2152
+ console.warn(`[Groq AI Shield] Call failed (${errorMsg}). Gracefully falling back to local rule engine.`);
2153
+ return { ok: false, error: errorMsg, isRateLimit: false };
2154
+ }
2155
+ }
2156
+ async function diagnoseRawErrorLogWithAi(rawLog) {
2157
+ const fallbackResult = diagnoseRawErrorLog(rawLog);
2158
+ if (!rawLog || !rawLog.trim()) {
2159
+ return fallbackResult;
2160
+ }
2161
+ const systemPrompt = `You are the lead engineering diagnostic agent in DEVINTELLIGENCE.
2162
+ Analyze the provided terminal error trace, Metro bundler failure, Android Gradle/Kotlin error, iOS CocoaPods error, or React Native Red Screen crash.
2163
+ Respond ONLY with a valid JSON object matching this TypeScript structure:
2164
+ {
2165
+ "category": "Metro Bundler" | "Babel / Reanimated" | "Android Gradle / Kotlin" | "Android Manifest" | "CocoaPods / iOS" | "Native Module Linking" | "Hermes Runtime" | "React Render / Hook Crash" | "Dependency Conflict",
2166
+ "severity": "critical" | "high" | "medium" | "low",
2167
+ "confidence": number (between 70 and 99),
2168
+ "summary": "Short 1-sentence punchy title of the crash",
2169
+ "rootCause": "Detailed technical root-cause explanation of what broke and why",
2170
+ "responsibleFile": "File path most likely responsible (e.g. android/app/build.gradle, babel.config.js, etc.)",
2171
+ "line": number or null,
2172
+ "safeRepairSteps": ["step 1", "step 2", "step 3"],
2173
+ "terminalCommands": ["exact command to run"],
2174
+ "codePatchSuggestion": "exact code or config patch snippet to fix it"
2175
+ }`;
2176
+ const userPrompt = `Diagnose this error log:
2177
+
2178
+ ${rawLog.slice(0, 3e3)}`;
2179
+ try {
2180
+ const aiRes = await callGroqChat(
2181
+ [
2182
+ { role: "system", content: systemPrompt },
2183
+ { role: "user", content: userPrompt }
2184
+ ],
2185
+ { jsonMode: true, temperature: 0.1 }
2186
+ );
2187
+ if (!aiRes.ok) {
2188
+ const failure = aiRes;
2189
+ return {
2190
+ ...fallbackResult,
2191
+ aiPowered: false,
2192
+ fallbackUsed: true,
2193
+ fallbackReason: failure.isRateLimit ? "AI rate limit reached. Analysis generated seamlessly by local heuristic rule engine." : `AI unavailable (${failure.error}). Analysis completed by local heuristic rule engine.`
2194
+ };
2195
+ }
2196
+ try {
2197
+ const parsed = JSON.parse(aiRes.content);
2198
+ return {
2199
+ id: `ai-diag-${Date.now()}`,
2200
+ category: parsed.category || fallbackResult.category,
2201
+ severity: ["critical", "high", "medium", "low"].includes(parsed.severity) ? parsed.severity : fallbackResult.severity,
2202
+ confidence: typeof parsed.confidence === "number" ? Math.min(Math.max(parsed.confidence, 60), 99) : 95,
2203
+ summary: parsed.summary || fallbackResult.summary,
2204
+ rootCause: parsed.rootCause || fallbackResult.rootCause,
2205
+ responsibleFile: parsed.responsibleFile || fallbackResult.responsibleFile,
2206
+ line: typeof parsed.line === "number" ? parsed.line : fallbackResult.line,
2207
+ safeRepairSteps: Array.isArray(parsed.safeRepairSteps) && parsed.safeRepairSteps.length > 0 ? parsed.safeRepairSteps : fallbackResult.safeRepairSteps,
2208
+ terminalCommands: Array.isArray(parsed.terminalCommands) ? parsed.terminalCommands : fallbackResult.terminalCommands,
2209
+ codePatchSuggestion: parsed.codePatchSuggestion || fallbackResult.codePatchSuggestion,
2210
+ aiPowered: true,
2211
+ aiModel: aiRes.model,
2212
+ fallbackUsed: false
2213
+ };
2214
+ } catch (parseErr) {
2215
+ console.warn("[Groq AI] JSON parse error in AI diagnosis, falling back to rule engine:", parseErr);
2216
+ return {
2217
+ ...fallbackResult,
2218
+ aiPowered: false,
2219
+ fallbackUsed: true,
2220
+ fallbackReason: "AI response parse failed. Analysis completed by local heuristic rule engine."
2221
+ };
2222
+ }
2223
+ } catch (err) {
2224
+ return {
2225
+ ...fallbackResult,
2226
+ aiPowered: false,
2227
+ fallbackUsed: true,
2228
+ fallbackReason: `Local rule fallback active: ${err.message || "AI request interrupted"}`
2229
+ };
2230
+ }
2231
+ }
2232
+ async function generateProjectAiAdvice(report) {
2233
+ const issuesSample = report.issues.slice(0, 8).map((i) => ({
2234
+ title: i.title,
2235
+ severity: i.severity,
2236
+ domain: i.domain,
2237
+ file: `${i.filePath}:${i.line}`,
2238
+ explanation: i.explanation
2239
+ }));
2240
+ const systemPrompt = `You are a Principal Mobile & Fullstack Architect reviewing a code scan report.
2241
+ Provide a concise, 3-paragraph executive engineering assessment:
2242
+ 1. Overall architecture and health overview based on scores and ecosystem.
2243
+ 2. Immediate critical threats (security, native crashes, memory leaks).
2244
+ 3. Exact prioritized 3-step action plan to reach 100/100 Quality Gate.
2245
+ Keep it strictly technical, direct, and actionable. Do not use generic filler.`;
2246
+ const userPrompt = `Project: ${report.name} (${report.ecosystem})
2247
+ Scores: Overall: ${report.scores.overall}/100, Security: ${report.scores.security}/100, Performance: ${report.scores.performance}/100, RN: ${report.scores.reactNative}/100
2248
+ Issues Count: ${report.issues.length}
2249
+ Key Issues:
2250
+ ${JSON.stringify(issuesSample, null, 2)}`;
2251
+ const aiRes = await callGroqChat(
2252
+ [
2253
+ { role: "system", content: systemPrompt },
2254
+ { role: "user", content: userPrompt }
2255
+ ],
2256
+ { temperature: 0.3, maxTokens: 800 }
2257
+ );
2258
+ if (aiRes.ok && aiRes.content.trim()) {
2259
+ return {
2260
+ advice: aiRes.content.trim(),
2261
+ aiPowered: true,
2262
+ model: aiRes.model
2263
+ };
2264
+ }
2265
+ const criticals = report.issues.filter((i) => i.severity === "critical");
2266
+ const safeRepairs = report.issues.filter((i) => i.repairTier === "SAFE");
2267
+ const fallbackAdvice = `### Deterministic Engineering Assessment (Rule Engine Fallback)
2268
+
2269
+ 1. **Architecture Status**: Project **${report.name}** is operating on **${report.ecosystem}** with an overall score of **${report.scores.overall}/100**.
2270
+ ${criticals.length > 0 ? `\u{1F6A8} **Critical Attention Needed**: Detected ${criticals.length} critical issues including ${criticals.map((c) => c.title).slice(0, 2).join(" and ")}.` : "\u2705 No critical blocker violations found."}
2271
+
2272
+ 2. **Automated Remediation Opportunity**: **${safeRepairs.length} of ${report.issues.length}** detected issues have validated SAFE auto-repair patches ready to apply with zero regression risk.
2273
+
2274
+ 3. **Recommended 3-Step Action Plan**:
2275
+ - Run safe batch remediation: \`devintelligence fix --project=${report.id}\`
2276
+ - Review security and authentication tokens in \`Security Audit\`
2277
+ - Check bundler cache and verify Quality Gate threshold compliance.`;
2278
+ return {
2279
+ advice: fallbackAdvice,
2280
+ aiPowered: false
2281
+ };
2282
+ }
2283
+ async function askDevIntelligenceAi(query, contextData) {
2284
+ if (!query || !query.trim()) {
2285
+ return { answer: "Please enter a query.", aiPowered: false };
2286
+ }
2287
+ const systemPrompt = `You are the DEVINTELLIGENCE Engineering AI Assistant.
2288
+ You specialize in:
2289
+ - React Native, Expo, Android Gradle, Kotlin, iOS CocoaPods, Swift, Metro bundler.
2290
+ - Next.js, React, Node.js, Express, Python Django, TypeScript, Vite.
2291
+ - Static code analysis, security auditing, and automated safe code repair.
2292
+ Provide clear, authoritative, copy-paste ready technical instructions.`;
2293
+ const userContent = contextData ? `Context:
2294
+ ${JSON.stringify(contextData, null, 2)}
2295
+
2296
+ User Question:
2297
+ ${query}` : query;
2298
+ const aiRes = await callGroqChat(
2299
+ [
2300
+ { role: "system", content: systemPrompt },
2301
+ { role: "user", content: userContent }
2302
+ ],
2303
+ { temperature: 0.2, maxTokens: 1200 }
2304
+ );
2305
+ if (aiRes.ok && aiRes.content.trim()) {
2306
+ return {
2307
+ answer: aiRes.content.trim(),
2308
+ aiPowered: true,
2309
+ model: aiRes.model
2310
+ };
2311
+ }
2312
+ return {
2313
+ answer: `### Local Engineering Assistant (Rule Fallback)
2314
+
2315
+ AI inference is currently running in offline rule mode (rate limit or connection limit).
2316
+ - For build errors: Try cleaning cache with \`npx expo start --clear\` or \`cd android && ./gradlew clean\`.
2317
+ - For project issues: Run \`devintelligence scan\` or check the Auto Repair Center.
2318
+ - For environment issues: Run \`devintelligence doctor\`.`,
2319
+ aiPowered: false
2320
+ };
2321
+ }
2322
+ export {
2323
+ MCP_TOOLS_CATALOG,
2324
+ PRESET_ERROR_LOGS,
2325
+ analyzeProjectDirectory,
2326
+ applyAllSafeRepairsOnDisk,
2327
+ applyIssueRepairOnDisk,
2328
+ askDevIntelligenceAi,
2329
+ detectProjectEcosystem,
2330
+ diagnoseRawErrorLog,
2331
+ diagnoseRawErrorLogWithAi,
2332
+ executeMcpToolCall,
2333
+ generateProjectAiAdvice,
2334
+ getAllProjectReports,
2335
+ getGroqApiKey,
2336
+ getProjectReportById,
2337
+ inspectHostEnvironment,
2338
+ isAiRateLimited,
2339
+ registerLocalProject,
2340
+ resetAiRateLimit,
2341
+ rollbackRepairOnDisk,
2342
+ updateProjectQualityGate
2343
+ };
2344
+ //# sourceMappingURL=index.js.map