catalyst-core-internal 0.1.0 → 0.1.2

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 (27) hide show
  1. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/BridgeMessageValidator.kt +10 -2
  2. package/dist/native/bridge/hooks.js +4 -4
  3. package/dist/native/bridge/useBaseHook.js +3 -4
  4. package/dist/native/bridge/utils/NativeBridge.js +4 -4
  5. package/dist/native/iosnativeWebView/Sources/Core/Utils/CacheManager.swift +2 -13
  6. package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +0 -36
  7. package/dist/native/iosnativeWebView/iosnativeWebView.xctestplan +0 -1
  8. package/dist/native/iosnativeWebView/iosnativeWebViewTests/FrameworkServerUtilsTests.swift +4 -14
  9. package/dist/native/iosnativeWebView/iosnativeWebViewTests/WebViewTests.swift +21 -9
  10. package/mcp_v2/conversion-tasks.json +371 -0
  11. package/mcp_v2/knowledge-base.json +1450 -0
  12. package/mcp_v2/lib/helpers.js +145 -0
  13. package/mcp_v2/mcp.js +366 -0
  14. package/mcp_v2/package.json +13 -0
  15. package/mcp_v2/schema.sql +88 -0
  16. package/mcp_v2/setup.js +262 -0
  17. package/mcp_v2/tools/build.js +449 -0
  18. package/mcp_v2/tools/config.js +262 -0
  19. package/mcp_v2/tools/conversion.js +492 -0
  20. package/mcp_v2/tools/debug.js +62 -0
  21. package/mcp_v2/tools/knowledge.js +213 -0
  22. package/mcp_v2/tools/sync.js +21 -0
  23. package/mcp_v2/tools/tasks.js +844 -0
  24. package/package.json +1 -1
  25. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/SecurityBridgeTest.kt +0 -199
  26. package/dist/native/iosnativeWebView/iosnativeWebViewTests/BridgeCommandHandlerSecurityTests.swift +0 -212
  27. package/dist/native/iosnativeWebView/iosnativeWebViewTests/ScreenSecureManagerTests.swift +0 -121
@@ -0,0 +1,492 @@
1
+ 'use strict';
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const { makeProjectHelpers, versionOlderThan } = require('../lib/helpers');
5
+
6
+ // Tasks that require a minimum catalyst-core version.
7
+ // Key = task ID, value = { minVersion, reason, upgradeNote }
8
+ const VERSION_GATES = {
9
+ T7b_NATIVE_SCRIPTS: {
10
+ minVersion: '0.0.3-canary.7',
11
+ reason: 'Native build scripts (buildApp:android, buildApp:ios, setupEmulator) were added in canary.7.',
12
+ upgradeNote: 'Update catalyst-core to ^0.0.3-canary.7 or later and run npm install.',
13
+ },
14
+ T8_CLIENT_ENTRY: {
15
+ minVersion: '0.1.0-canary.1',
16
+ reason: 'WebBridge and the universal client entry pattern (WebBridge.init() + clientRouter) require canary.1+.',
17
+ upgradeNote: 'Update catalyst-core to ^0.1.0-canary.1 or later and run npm install. Also ensure client/index.js follows the new template pattern.',
18
+ },
19
+ };
20
+
21
+ let _projectInfo;
22
+ let _conversionTasks;
23
+
24
+ function init(projectInfo, conversionTasks) {
25
+ _projectInfo = projectInfo;
26
+ _conversionTasks = conversionTasks;
27
+ }
28
+
29
+ // ── get_conversion_status ────────────────────────────────────────────────────
30
+
31
+ function handle_get_conversion_status({ project_path, include_not_applicable = false } = {}) {
32
+ const root = project_path || _projectInfo.dir;
33
+ const { fileExists, readJson, readText, grepSrc } = makeProjectHelpers(root);
34
+
35
+ // ── Version gate: read installed catalyst-core version ────────────────────
36
+ const installedVersion = _projectInfo.installedVersion || null;
37
+
38
+ // ── Load project state once ────────────────────────────────────────────────
39
+ const config = readJson('config/config.json');
40
+ const webviewConfig = config && config.WEBVIEW_CONFIG ? config.WEBVIEW_CONFIG : null;
41
+ const pkg = readJson('package.json') || _projectInfo.pkg;
42
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
43
+
44
+ // ── Per-task detectors (3-step model) ─────────────────────────────────────
45
+ //
46
+ // Each detector returns one of:
47
+ // { status: 'completed', note? }
48
+ // { status: 'gap', native_risk, reason, files? }
49
+ // { status: 'needs_review', native_risk, reason, review_context }
50
+ // { status: 'not_applicable', reason }
51
+ // { status: 'blocked', blocked_by }
52
+ //
53
+ // Step 1 — feature_present: is this web feature used at all?
54
+ // Step 2 — risk_assessment: what's the usage pattern? (derives native_risk from code)
55
+ // Step 3 — mitigation_check: is the risk already handled?
56
+
57
+ const detectors = {
58
+
59
+ // ── Tier 1: Critical (always applicable — these are structural) ──────────
60
+
61
+ T1_CONFIG: () => {
62
+ if (!config) return { status: 'gap', native_risk: 'App will not start — catalyst-core requires config/config.json at boot.', reason: 'config/config.json not found' };
63
+ const missing = ['NODE_SERVER_PORT', 'WEBVIEW_CONFIG', 'API_URL'].filter(k => config[k] === undefined);
64
+ if (missing.length) return { status: 'gap', native_risk: `Missing required fields will cause build or runtime failures: ${missing.join(', ')}.`, reason: `Missing fields: ${missing.join(', ')}` };
65
+ return { status: 'completed' };
66
+ },
67
+
68
+ T2_ROUTER_DEP: () => {
69
+ if (!allDeps['@tata1mg/router']) return { status: 'gap', native_risk: 'react-router-dom does not support SSR + hydration required by catalyst-core. App routing will break on native.', reason: '@tata1mg/router not in package.json dependencies' };
70
+ return { status: 'completed' };
71
+ },
72
+
73
+ T3_ROUTES_FILE: () => {
74
+ const text = readText('src/js/routes/index.js');
75
+ if (!text) return { status: 'gap', native_risk: 'catalyst-core expects a static route array for SSR. Without it, server-side rendering fails and native app shell cannot hydrate.', reason: 'src/js/routes/index.js not found' };
76
+ const hasArray = /const\s+\w+\s*=\s*\[[\s\S]*?\]\s*\n[\s\S]*export default\s+\w+|export default\s*\[|module\.exports\s*=\s*\[/.test(text);
77
+ if (!hasArray) return { status: 'gap', native_risk: 'Route file exists but no route array found — RouterDataProvider cannot build the route tree.', reason: 'src/js/routes/index.js exists but does not export a route array' };
78
+ return { status: 'completed' };
79
+ },
80
+
81
+ T4_DATA_FETCHING: () => {
82
+ const useEffectFiles = grepSrc('useEffect.*fetch|useEffect.*axios|useEffect.*\\.get\\(|useEffect.*\\.post\\(');
83
+ const fetcherFiles = grepSrc('\\.serverFetcher\\s*=|\\.clientFetcher\\s*=');
84
+ if (useEffectFiles.length === 0) return { status: 'completed' };
85
+ return {
86
+ status: 'needs_review',
87
+ native_risk: 'useEffect+fetch for page data works on web but breaks SSR hydration in catalyst-core. Native app will have blank pages or hydration mismatches on first load.',
88
+ reason: `${useEffectFiles.length} file(s) with useEffect+fetch patterns. Review: are these unconverted page-level data loads or legitimate side effects?`,
89
+ review_context: {
90
+ question: 'Do the files using useEffect for data fetching represent unconverted page-level data loading, or are they legitimate non-page effects (timers, subscriptions, animations)?',
91
+ what_correct_looks_like: 'Page components have .serverFetcher or .clientFetcher static functions attached. useEffect is only used for side effects (subscriptions, DOM mutations, timers), not initial data loads.',
92
+ what_gap_looks_like: 'Page components (in src/js/pages/ or src/js/containers/) call fetch/axios inside useEffect to load initial data, with no .serverFetcher or .clientFetcher attached.',
93
+ signal_files: {
94
+ old_pattern_files: useEffectFiles,
95
+ converted_files: fetcherFiles,
96
+ },
97
+ },
98
+ };
99
+ },
100
+
101
+ T5_ROUTER_DATA_PROVIDER: () => {
102
+ const text = readText('src/js/routes/utils.js');
103
+ if (!text) return { status: 'gap', native_risk: 'Without RouterDataProvider, useCurrentRouteData() returns undefined in all components. Data-dependent pages will crash.', reason: 'src/js/routes/utils.js not found' };
104
+ if (!text.includes('RouterDataProvider')) return { status: 'gap', native_risk: 'utils.js exists but RouterDataProvider not wired in — same crash risk as missing file.', reason: 'utils.js exists but RouterDataProvider not used' };
105
+ return { status: 'completed' };
106
+ },
107
+
108
+ T6_APP_SHELL: () => {
109
+ const text = readText('src/js/containers/App/index.js');
110
+ if (!text) return { status: 'gap', native_risk: 'App shell is required by catalyst-core SSR. Missing file means native app renders blank screen.', reason: 'src/js/containers/App/index.js not found' };
111
+ if (!text.includes('Outlet')) return { status: 'gap', native_risk: '<Outlet /> is how catalyst-core injects SSR-rendered route content. Without it, all pages render empty inside the native shell.', reason: 'App/index.js exists but <Outlet /> not rendered' };
112
+ return { status: 'completed' };
113
+ },
114
+
115
+ T7_SERVER_FILES: () => {
116
+ const missing = ['server/index.js', 'server/server.js', 'server/document.js'].filter(f => !fileExists(f));
117
+ if (missing.length) return { status: 'gap', native_risk: 'Missing server files prevent catalyst-core SSR from running. Native app will fail at boot with module-not-found errors.', reason: `Missing: ${missing.join(', ')}` };
118
+ return { status: 'completed' };
119
+ },
120
+
121
+ T8_CLIENT_ENTRY: () => {
122
+ const missing = ['client/index.js', 'client/styles.js'].filter(f => !fileExists(f));
123
+ if (missing.length) return { status: 'gap', native_risk: 'Client entry files are required for hydration. Without them the native WebView loads server HTML but JS never hydrates — app is non-interactive.', reason: `Missing: ${missing.join(', ')}` };
124
+ return { status: 'completed' };
125
+ },
126
+
127
+ // ── Tier 2: Native Build (always applicable once T1 passes) ─────────────
128
+
129
+ T9_WEBVIEW_ANDROID: () => {
130
+ if (!webviewConfig) return { status: 'gap', native_risk: 'No WEBVIEW_CONFIG — Android build cannot start.', reason: 'WEBVIEW_CONFIG missing (see T1_CONFIG)' };
131
+ const android = webviewConfig.android;
132
+ if (!android) return { status: 'gap', native_risk: 'WEBVIEW_CONFIG.android block missing — catalyst-core cannot generate the Android project.', reason: 'WEBVIEW_CONFIG.android block missing' };
133
+ const missing = ['buildType', 'sdkPath', 'emulatorName', 'appName'].filter(k => android[k] === undefined);
134
+ if (missing.length) return { status: 'gap', native_risk: `Android build will fail or produce unnamed app. Missing: ${missing.join(', ')}.`, reason: `WEBVIEW_CONFIG.android missing fields: ${missing.join(', ')}` };
135
+ return { status: 'completed' };
136
+ },
137
+
138
+ T10_WEBVIEW_IOS: () => {
139
+ if (!webviewConfig) return { status: 'gap', native_risk: 'No WEBVIEW_CONFIG — iOS build cannot start.', reason: 'WEBVIEW_CONFIG missing (see T1_CONFIG)' };
140
+ const ios = webviewConfig.ios;
141
+ if (!ios) return { status: 'gap', native_risk: 'WEBVIEW_CONFIG.ios block missing — catalyst-core cannot generate the Xcode project.', reason: 'WEBVIEW_CONFIG.ios block missing' };
142
+ const missing = ['buildType', 'appBundleId', 'simulatorName', 'appName'].filter(k => ios[k] === undefined);
143
+ if (missing.length) return { status: 'gap', native_risk: `iOS build will fail or be rejected by App Store. Missing: ${missing.join(', ')}.`, reason: `WEBVIEW_CONFIG.ios missing fields: ${missing.join(', ')}` };
144
+ return { status: 'completed' };
145
+ },
146
+
147
+ T11_ACCESS_CONTROL: () => {
148
+ if (!webviewConfig) return { status: 'gap', native_risk: 'No WEBVIEW_CONFIG — access control cannot be configured.', reason: 'WEBVIEW_CONFIG missing (see T1_CONFIG)' };
149
+ const ac = webviewConfig.accessControl;
150
+ if (!ac) return { status: 'gap', native_risk: 'Without accessControl, the WebView allows navigation to any URL. Deep links or redirects could take users outside your app with no way back.', reason: 'WEBVIEW_CONFIG.accessControl block missing' };
151
+ if (!ac.enabled) return { status: 'gap', native_risk: 'accessControl.enabled=false means URL allowlist is ignored — same risk as missing block.', reason: 'accessControl.enabled is not true' };
152
+ if (!Array.isArray(ac.allowedUrls) || ac.allowedUrls.length === 0)
153
+ return { status: 'gap', native_risk: 'allowedUrls is empty — ALL navigation URLs will be blocked including your own API calls. App will appear to hang on every network request.', reason: 'accessControl.allowedUrls is empty — ALL URLs will be blocked' };
154
+ return { status: 'completed' };
155
+ },
156
+
157
+ T12_SPLASH_SCREEN: () => {
158
+ if (!config) return { status: 'gap', native_risk: 'No config — splash screen config cannot be read.', reason: 'config/config.json not found' };
159
+ if (!config.splashScreen) return { status: 'gap', native_risk: 'Without splashScreen config, native app shows a blank white screen during JS load. Not a crash, but poor UX and noticeable on slow devices.', reason: 'splashScreen key missing from top level of config/config.json (not inside WEBVIEW_CONFIG)' };
160
+ const missing = ['public/android/splashscreen.png', 'public/ios/splashscreen.png'].filter(f => !fileExists(f));
161
+ if (missing.length) return { status: 'gap', native_risk: 'Splash config exists but asset files missing — build will fail when packaging the native app.', reason: `Splash asset files missing: ${missing.join(', ')}` };
162
+ return { status: 'completed' };
163
+ },
164
+
165
+ T13_ANDROID_ICONS: () => {
166
+ const required = ['mdpi', 'hdpi', 'xhdpi', 'xxhdpi', 'xxxhdpi'].map(d => `public/android/appIcons/icon-${d}.png`);
167
+ const missing = required.filter(f => !fileExists(f));
168
+ if (missing.length) return { status: 'gap', native_risk: 'Missing Android icon densities — build will fall back to bundled catalyst icon. Expected naming: icon-mdpi.png, icon-hdpi.png, icon-xhdpi.png, icon-xxhdpi.png, icon-xxxhdpi.png inside public/android/appIcons/.', reason: `Missing Android icons: ${missing.join(', ')}` };
169
+ return { status: 'completed' };
170
+ },
171
+
172
+ T14_IOS_ICONS: () => {
173
+ const required = [
174
+ 'icon-20x20-2x', 'icon-20x20-3x',
175
+ 'icon-29x29-2x', 'icon-29x29-3x',
176
+ 'icon-40x40-2x', 'icon-40x40-3x',
177
+ 'icon-60x60-2x', 'icon-60x60-3x',
178
+ 'icon-1024x1024-1x',
179
+ ].map(name => `public/ios/appIcons/${name}.png`);
180
+ const missing = required.filter(f => !fileExists(f));
181
+ if (missing.length) return { status: 'gap', native_risk: 'Missing iOS icon sizes — Xcode build may fail or App Store submission will be rejected. Expected naming: icon-{WxH}-{scale}.png e.g. icon-60x60-3x.png inside public/ios/appIcons/.', reason: `Missing iOS icons: ${missing.join(', ')}` };
182
+ return { status: 'completed' };
183
+ },
184
+
185
+ T15_OFFLINE_HTML: () => {
186
+ if (!fileExists('public/offline.html')) return { status: 'gap', native_risk: 'Without offline.html, the native WebView shows a system error page when the device goes offline. Bad UX; should show a branded offline screen.', reason: 'public/offline.html not found' };
187
+ return { status: 'completed' };
188
+ },
189
+
190
+ // ── Tier 3: Enhancements (feature-presence gated) ───────────────────────
191
+
192
+ T17a_USE_FILEPICKER: () => {
193
+ const oldPatternFiles = grepSrc('<input[^>]*type=[\'"]file[\'"]');
194
+ if (oldPatternFiles.length === 0) return { status: 'not_applicable', reason: 'No <input type="file"> found in project — file upload feature not used.' };
195
+ const hookUsageFiles = grepSrc('useFilePicker');
196
+ return {
197
+ status: 'needs_review',
198
+ native_risk: '<input type="file"> may work in some WebView configs but behaviour is inconsistent across Android/iOS versions. On Android, file picker may silently do nothing without proper WebView flags. useFilePicker guarantees native picker on both platforms.',
199
+ reason: `${oldPatternFiles.length} file(s) with <input type="file"> found. Review: does useFilePicker exist in the component tree (hook may be in a parent)?`,
200
+ review_context: {
201
+ question: 'Do files with <input type=\'file\'> have a native branch using useFilePicker, or is the hook used somewhere in the component tree (parent passes execute/isNative as props)?',
202
+ what_correct_looks_like: 'Component uses useFilePicker from catalyst-core/hooks. Checks isNative and calls execute() on native. May keep <input type=\'file\'> as web fallback only. Hook may live in a parent and be passed down as props.',
203
+ what_gap_looks_like: '<input type=\'file\'> used directly with no useFilePicker anywhere in the component tree. No isNative check. No native branch.',
204
+ signal_files: {
205
+ old_pattern_files: oldPatternFiles,
206
+ hook_usage_files: hookUsageFiles,
207
+ },
208
+ },
209
+ };
210
+ },
211
+
212
+ T17b_USE_CAMERA: () => {
213
+ const oldPatternFiles = grepSrc('accept=[\'"]image[^>]*capture|<input[^>]+capture');
214
+ if (oldPatternFiles.length === 0) return { status: 'not_applicable', reason: 'No camera capture inputs (<input capture>) found — camera feature not used.' };
215
+ const hookUsageFiles = grepSrc('useCamera');
216
+ return {
217
+ status: 'needs_review',
218
+ native_risk: 'Camera capture via <input capture> does NOT work reliably in native WebView. On iOS it may trigger the permission flow but fail silently. On Android it often does nothing. useCamera is required for guaranteed camera access with proper permission handling.',
219
+ reason: `${oldPatternFiles.length} file(s) with camera capture inputs. Review: does useCamera exist in the component tree?`,
220
+ review_context: {
221
+ question: 'Do files with <input accept=\'image/*\' capture> have a native branch using useCamera, or is the hook in the component tree?',
222
+ what_correct_looks_like: 'Component uses useCamera from catalyst-core/hooks. Checks isNative and calls takePhoto() on native. May keep capture input as web fallback.',
223
+ what_gap_looks_like: '<input capture> used with no useCamera in component tree. No isNative check. No native camera branch.',
224
+ signal_files: {
225
+ old_pattern_files: oldPatternFiles,
226
+ hook_usage_files: hookUsageFiles,
227
+ },
228
+ },
229
+ };
230
+ },
231
+
232
+ T18_USE_HAPTIC: () => {
233
+ const oldPatternFiles = grepSrc('navigator\\.vibrate');
234
+ if (oldPatternFiles.length === 0) return { status: 'not_applicable', reason: 'No navigator.vibrate() calls found — haptic feedback not used.' };
235
+ const hookUsageFiles = grepSrc('useHapticFeedback');
236
+ return {
237
+ status: 'needs_review',
238
+ native_risk: 'navigator.vibrate() is a web API that silently does nothing inside native WebView on both Android and iOS. The vibration call succeeds but the device never vibrates. useHapticFeedback routes to the native haptic engine.',
239
+ reason: `${oldPatternFiles.length} file(s) with navigator.vibrate. Review: are these in active component code (gap) or test/polyfill files (acceptable)?`,
240
+ review_context: {
241
+ question: 'Are navigator.vibrate() calls unconverted haptic triggers, or are they in test files, comments, or polyfills?',
242
+ what_correct_looks_like: 'useHapticFeedback imported from catalyst-core/hooks. trigger() called with type string. navigator.vibrate only appears in web polyfills (if at all).',
243
+ what_gap_looks_like: 'navigator.vibrate() called directly in component or utility code with no useHapticFeedback import.',
244
+ signal_files: {
245
+ old_pattern_files: oldPatternFiles,
246
+ hook_usage_files: hookUsageFiles,
247
+ },
248
+ },
249
+ };
250
+ },
251
+
252
+ T19_USE_NOTIFICATIONS: () => {
253
+ if (!webviewConfig || !webviewConfig.notifications || !webviewConfig.notifications.enabled) {
254
+ const pushFiles = grepSrc('firebase|FCM|pushNotif|useNotification');
255
+ if (pushFiles.length === 0) return { status: 'not_applicable', reason: 'notifications.enabled not set and no push notification code found.' };
256
+ return {
257
+ status: 'gap',
258
+ native_risk: 'Project appears to use push notifications but notifications.enabled is not set in WEBVIEW_CONFIG. Firebase integration will not be wired into the native build — push notifications will silently not work.',
259
+ reason: `Push notification code found (${pushFiles.length} file(s)) but WEBVIEW_CONFIG.notifications.enabled is not true.`,
260
+ files: pushFiles,
261
+ };
262
+ }
263
+ const missing = ['google-services.json', 'GoogleService-Info.plist'].filter(f => !fileExists(f));
264
+ if (missing.length) return { status: 'gap', native_risk: 'notifications.enabled=true but Firebase config files missing — native build will fail at compile time.', reason: `notifications.enabled=true but Firebase files missing: ${missing.join(', ')}` };
265
+ return { status: 'completed' };
266
+ },
267
+
268
+ T20_USE_DEVICE_INFO: () => {
269
+ const oldPatternFiles = grepSrc('navigator\\.userAgent|/Android/i\\.test|/iPhone/i\\.test|/iPad/i\\.test');
270
+ if (oldPatternFiles.length === 0) return { status: 'not_applicable', reason: 'No navigator.userAgent / UA-sniffing found — platform detection not used.' };
271
+ const correctPatternFiles = grepSrc('isNative|window\\.NativeBridge|nativeBridge\\.isAndroid|nativeBridge\\.isIOS');
272
+ return {
273
+ status: 'needs_review',
274
+ native_risk: 'navigator.userAgent inside native WebView returns the WebView UA string, not Android/iOS. UA-sniffing for platform branching gives wrong results — features guarded by /Android/i.test() may never activate on native.',
275
+ reason: `${oldPatternFiles.length} file(s) with UA-sniffing. Review: is this platform detection in component code (gap) or analytics/server code (acceptable)?`,
276
+ review_context: {
277
+ question: 'Are navigator.userAgent patterns used for platform detection in component/utility code, or in analytics, SSR utilities, or third-party code where UA is acceptable?',
278
+ what_correct_looks_like: 'Platform detection uses isNative from catalyst hooks (e.g. const { isNative } = useNetworkStatus()), or window.NativeBridge presence for imperative code. navigator.userAgent only in analytics or server-side code.',
279
+ what_gap_looks_like: 'Component or utility code branches UI/behavior on navigator.userAgent. Unreliable inside WebView.',
280
+ signal_files: {
281
+ old_pattern_files: oldPatternFiles,
282
+ correct_pattern_files: correctPatternFiles,
283
+ },
284
+ },
285
+ };
286
+ },
287
+ };
288
+
289
+ // ── Run all detectors, respect depends_on + version gates ─────────────────
290
+ const taskMap = Object.fromEntries(_conversionTasks.map(t => [t.id, t]));
291
+ const results = {};
292
+
293
+ function runTask(id) {
294
+ if (results[id]) return results[id];
295
+ const task = taskMap[id];
296
+
297
+ // Check version gate before anything else
298
+ const gate = VERSION_GATES[id];
299
+ if (gate && installedVersion && versionOlderThan(installedVersion, gate.minVersion)) {
300
+ results[id] = {
301
+ status: 'blocked_by_version',
302
+ installed_version: installedVersion,
303
+ required_version: gate.minVersion,
304
+ reason: gate.reason,
305
+ upgrade_note: gate.upgradeNote,
306
+ };
307
+ return results[id];
308
+ }
309
+
310
+ for (const dep of task.depends_on) {
311
+ const depResult = runTask(dep);
312
+ if (depResult.status !== 'completed' && depResult.status !== 'not_applicable') {
313
+ results[id] = { status: 'blocked', blocked_by: dep };
314
+ return results[id];
315
+ }
316
+ }
317
+ const detector = detectors[id];
318
+ results[id] = detector ? detector() : { status: 'gap', native_risk: 'Unknown', reason: 'No detector implemented' };
319
+ return results[id];
320
+ }
321
+
322
+ for (const task of _conversionTasks) runTask(task.id);
323
+
324
+ // ── Build output ───────────────────────────────────────────────────────────
325
+ const completed = [];
326
+ const gaps = [];
327
+ const needs_review = [];
328
+ const blocked = [];
329
+ const blocked_by_version = [];
330
+ const not_applicable = [];
331
+
332
+ for (const task of _conversionTasks) {
333
+ const r = results[task.id];
334
+ switch (r.status) {
335
+ case 'completed':
336
+ completed.push({ id: task.id, tier: task.tier, title: task.title, note: r.note || null });
337
+ break;
338
+ case 'blocked':
339
+ blocked.push({ id: task.id, tier: task.tier, title: task.title, blocked_by: r.blocked_by });
340
+ break;
341
+ case 'blocked_by_version':
342
+ blocked_by_version.push({
343
+ id: task.id, tier: task.tier, title: task.title,
344
+ installed_version: r.installed_version,
345
+ required_version: r.required_version,
346
+ reason: r.reason,
347
+ upgrade_note: r.upgrade_note,
348
+ });
349
+ break;
350
+ case 'needs_review':
351
+ needs_review.push({ id: task.id, tier: task.tier, title: task.title, native_risk: r.native_risk, reason: r.reason, review_context: r.review_context, fix_guide: task.fix_guide, verify_hint: task.verify_hint || null, depends_on: task.depends_on });
352
+ break;
353
+ case 'not_applicable':
354
+ not_applicable.push({ id: task.id, tier: task.tier, title: task.title, reason: r.reason });
355
+ break;
356
+ default: // gap
357
+ gaps.push({ id: task.id, tier: task.tier, title: task.title, native_risk: r.native_risk, reason: r.reason, files: r.files || null, fix_guide: task.fix_guide, verify_hint: task.verify_hint || null, depends_on: task.depends_on });
358
+ }
359
+ }
360
+
361
+ const applicableTotal = _conversionTasks.length - not_applicable.length;
362
+ const tierSummary = { 1: { total: 0, done: 0 }, 2: { total: 0, done: 0 }, 3: { total: 0, done: 0 } };
363
+ for (const task of _conversionTasks) {
364
+ if (results[task.id].status === 'not_applicable') continue;
365
+ tierSummary[task.tier].total++;
366
+ if (results[task.id].status === 'completed') tierSummary[task.tier].done++;
367
+ }
368
+
369
+ // Version banner — shown when any tasks are blocked by version
370
+ const versionBanner = blocked_by_version.length > 0 ? {
371
+ warning: `⚠️ catalyst-core version too old for ${blocked_by_version.length} task(s)`,
372
+ installed: installedVersion || 'unknown',
373
+ tasks_blocked: blocked_by_version.map(t => t.id),
374
+ action: `Update catalyst-core in package.json to the required version and run npm install. After upgrading, run node .catalyst/mcp/setup.js to re-sync the knowledge base.`,
375
+ upgrade_options: [
376
+ 'npm install catalyst-core@latest',
377
+ 'Or pin to specific: npm install github:tata1mg/catalyst-core#v0.1.0-canary.4',
378
+ ],
379
+ } : null;
380
+
381
+ // ── Project context — raw signals for LLM to infer source framework ─────────
382
+ let topLevelFiles = [];
383
+ try { topLevelFiles = fs.readdirSync(root).filter(f => !['node_modules', '.git', 'build', 'dist'].includes(f)); } catch {}
384
+ let srcStructure = [];
385
+ try { srcStructure = fs.readdirSync(path.join(root, 'src')); } catch {}
386
+
387
+ const project_context = {
388
+ top_level_files: topLevelFiles,
389
+ src_structure: srcStructure,
390
+ dependencies: Object.keys(pkg.dependencies || {}),
391
+ dev_dependencies: Object.keys(pkg.devDependencies || {}),
392
+ };
393
+
394
+ const output = {
395
+ project: pkg.name || root,
396
+ scanned_at: new Date().toISOString(),
397
+ catalyst_core_version: installedVersion || 'unknown',
398
+ project_context,
399
+ ...(versionBanner ? { version_warning: versionBanner } : {}),
400
+ summary: {
401
+ applicable_total: applicableTotal,
402
+ completed: completed.length,
403
+ gaps: gaps.length,
404
+ needs_review: needs_review.length,
405
+ blocked: blocked.length,
406
+ blocked_by_version: blocked_by_version.length,
407
+ not_applicable: not_applicable.length,
408
+ tier_1: `${tierSummary[1].done}/${tierSummary[1].total} critical`,
409
+ tier_2: `${tierSummary[2].done}/${tierSummary[2].total} native build`,
410
+ tier_3: `${tierSummary[3].done}/${tierSummary[3].total} enhancements`,
411
+ },
412
+ gaps,
413
+ needs_review,
414
+ blocked,
415
+ blocked_by_version,
416
+ completed,
417
+ };
418
+
419
+ if (include_not_applicable) output.not_applicable = not_applicable;
420
+ return output;
421
+ }
422
+
423
+ // ── get_conversion_tasks ──────────────────────────────────────────────────────
424
+
425
+ function handle_get_conversion_tasks({ project_path, filter = 'all', include_not_applicable = false } = {}) {
426
+ const status = handle_get_conversion_status({ project_path, include_not_applicable });
427
+
428
+ const TIER_FILTERS = { all: null, critical: [1], native: [2], enhancements: [3] };
429
+ const tierFilter = TIER_FILTERS[filter] || null;
430
+ const taskMap = Object.fromEntries(_conversionTasks.map(t => [t.id, t]));
431
+
432
+ function applyFilter(arr) {
433
+ if (!tierFilter) return arr;
434
+ return arr.filter(item => tierFilter.includes(taskMap[item.id]?.tier));
435
+ }
436
+
437
+ const actionable = applyFilter(status.gaps).map(g => {
438
+ const task = taskMap[g.id];
439
+ return { id: g.id, tier: task.tier, title: task.title, status: 'gap', native_risk: g.native_risk, reason: g.reason, files: g.files || null, fix_guide: task.fix_guide, verify_hint: task.verify_hint || null, how_to_check: task.how_to_check, depends_on: task.depends_on, blocked_by: null };
440
+ });
441
+
442
+ const reviewList = applyFilter(status.needs_review).map(r => {
443
+ const task = taskMap[r.id];
444
+ return { id: r.id, tier: task.tier, title: task.title, status: 'needs_review', native_risk: r.native_risk, reason: r.reason, review_context: r.review_context, fix_guide: task.fix_guide, verify_hint: task.verify_hint || null, how_to_check: task.how_to_check, depends_on: task.depends_on, blocked_by: null };
445
+ });
446
+
447
+ const blockedList = applyFilter(status.blocked).map(b => {
448
+ const task = taskMap[b.id];
449
+ return { id: b.id, tier: task.tier, title: task.title, status: 'blocked', reason: `Blocked by ${b.blocked_by} — fix that first`, files: null, fix_guide: task.fix_guide, how_to_check: task.how_to_check, depends_on: task.depends_on, blocked_by: b.blocked_by };
450
+ });
451
+
452
+ const blockedByVersionList = applyFilter(status.blocked_by_version || []).map(b => {
453
+ const task = taskMap[b.id];
454
+ return {
455
+ id: b.id, tier: task.tier, title: task.title,
456
+ status: 'blocked_by_version',
457
+ reason: b.reason,
458
+ installed_version: b.installed_version,
459
+ required_version: b.required_version,
460
+ upgrade_note: b.upgrade_note,
461
+ fix_guide: task.fix_guide,
462
+ how_to_check: task.how_to_check,
463
+ depends_on: task.depends_on,
464
+ };
465
+ });
466
+
467
+ const notApplicableList = include_not_applicable
468
+ ? applyFilter(status.not_applicable || []).map(n => {
469
+ const task = taskMap[n.id];
470
+ return { id: n.id, tier: task.tier, title: task.title, status: 'not_applicable', reason: n.reason };
471
+ })
472
+ : [];
473
+
474
+ return {
475
+ project: status.project,
476
+ scanned_at: status.scanned_at,
477
+ filter,
478
+ summary: {
479
+ total_gaps: actionable.length,
480
+ total_needs_review: reviewList.length,
481
+ total_blocked: blockedList.length,
482
+ total_blocked_by_version: blockedByVersionList.length,
483
+ total_not_applicable: (status.not_applicable || []).length,
484
+ completed: status.completed.length,
485
+ overall: `${status.summary.completed}/${status.summary.applicable_total} applicable done`,
486
+ },
487
+ ...(status.version_warning ? { version_warning: status.version_warning } : {}),
488
+ tasks: [...actionable, ...reviewList, ...blockedList, ...blockedByVersionList, ...notApplicableList],
489
+ };
490
+ }
491
+
492
+ module.exports = { init, handle_get_conversion_status, handle_get_conversion_tasks };
@@ -0,0 +1,62 @@
1
+ 'use strict';
2
+
3
+ let _db;
4
+
5
+ function init(db) {
6
+ _db = db;
7
+ }
8
+
9
+ function handle_debug_issue({ symptom, layer } = {}) {
10
+ const NOISE = new Set(['the','a','an','is','are','was','not','does','do','on','in','at','to','for','of','and','or','with','my','i','its','it','this','that','when','why','how','what','where','after','before','but','can','cant','cannot','will','wont','still','always','never','app','apps']);
11
+ const tokens = symptom.toLowerCase().replace(/[^a-z0-9\s]/g, ' ').split(/\s+/).filter(t => t.length > 2 && !NOISE.has(t));
12
+
13
+ function scoreRow(text) {
14
+ const lower = text.toLowerCase();
15
+ let score = 0;
16
+ for (const token of tokens) {
17
+ if (lower.includes(token)) score++;
18
+ }
19
+ return score;
20
+ }
21
+
22
+ const allErrors = layer
23
+ ? _db.prepare(`SELECT * FROM known_errors WHERE layer = ?`).all(layer)
24
+ : _db.prepare(`SELECT * FROM known_errors`).all();
25
+
26
+ const scored = allErrors
27
+ .map(r => ({ ...r, _score: scoreRow(r.symptom + ' ' + r.cause + ' ' + r.tags) }))
28
+ .sort((a, b) => b._score - a._score);
29
+
30
+ const matched = scored.filter(r => r._score > 0).slice(0, 5);
31
+ const fallback = matched.length === 0 ? scored.slice(0, 3) : [];
32
+
33
+ const matchedLayers = [...new Set(matched.map(r => r.layer))];
34
+ const knowledgeLayers = layer ? [layer] : matchedLayers.length ? matchedLayers : null;
35
+
36
+ let knowledge = [];
37
+ if (knowledgeLayers && knowledgeLayers.length) {
38
+ const placeholders = knowledgeLayers.map(() => '?').join(',');
39
+ knowledge = _db.prepare(`
40
+ SELECT title, content, layer FROM framework_knowledge
41
+ WHERE source = 'static' AND layer IN (${placeholders})
42
+ ORDER BY layer, id
43
+ LIMIT 8
44
+ `).all(...knowledgeLayers);
45
+ }
46
+
47
+ const result = { symptom, tokens_used: tokens, layer: layer || 'auto' };
48
+
49
+ if (matched.length > 0) {
50
+ result.matched_errors = matched.map(({ _score, ...r }) => ({ ...r, match_score: _score }));
51
+ result.relevant_knowledge = knowledge;
52
+ } else {
53
+ result.matched_errors = [];
54
+ result.fallback_errors = fallback.map(({ _score, ...r }) => r);
55
+ result.relevant_knowledge = knowledge;
56
+ result.note = `No strong keyword matches for "${symptom}". Showing top known_errors entries as fallback. Try rephrasing with more specific terms (e.g. "android build sdkPath", "localhost blocked", "clearWebData cache").`;
57
+ }
58
+
59
+ return result;
60
+ }
61
+
62
+ module.exports = { init, handle_debug_issue };