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,1450 @@
1
+ [
2
+ {
3
+ "section": "framework_identity",
4
+ "title": "Catalyst Framework Philosophy",
5
+ "content": "Catalyst is a WebView-based universal app framework enabling a single codebase to ship natively on Android/iOS and on web. Zero user intervention required. Uses native WebView (WebKit on iOS, Chromium on Android) which owns its own JS engine — not React Native, not React Native Web.",
6
+ "layer": "Component",
7
+ "source": "static",
8
+ "tags": [
9
+ "core",
10
+ "architecture",
11
+ "universal"
12
+ ]
13
+ },
14
+ {
15
+ "section": "framework_identity",
16
+ "title": "Not React Native",
17
+ "content": "Catalyst is fundamentally incompatible with React Native (JSI, TurboModules). Cannot adopt JSI without replacing WebView entirely — would require rebuilding as React Native instead. JSI targets React Native's single-threaded JS runtime, not a WebView JS engine.",
18
+ "layer": "Component",
19
+ "source": "static",
20
+ "tags": [
21
+ "architecture",
22
+ "constraints"
23
+ ]
24
+ },
25
+ {
26
+ "section": "framework_identity",
27
+ "title": "Catalyst Competitive Advantage",
28
+ "content": "Best suited for form-heavy, content-rich apps. Wins on: UI consistency (identical HTML/CSS cross-platform), OTA web updates (web team ships mobile), custom security control (RootBeer/Frida/access control), no vendor lock-in, custom caching strategies.",
29
+ "layer": "Component",
30
+ "source": "static",
31
+ "tags": [
32
+ "positioning"
33
+ ]
34
+ },
35
+ {
36
+ "section": "framework_identity",
37
+ "title": "Known Performance Gap: WebView Cold Start",
38
+ "content": "WebView cold start is the #1 performance gap vs React Native. Mitigation: SplashViewModel.swift preloading is the correct fix direction (not yet implemented in core).",
39
+ "layer": "Runtime",
40
+ "source": "static",
41
+ "tags": [
42
+ "performance",
43
+ "ios"
44
+ ]
45
+ },
46
+ {
47
+ "section": "transport_architecture",
48
+ "title": "Tri-Transport System",
49
+ "content": "Catalyst uses three transports in priority order: 1) Native Bridge (primary, hardware/system access, low-latency), 2) Localhost Server (secondary, data-heavy operations, progress tracking, streaming), 3) Web Fallback (tertiary, browser APIs). Platform detection: const isWeb = !window.nativeBridge || typeof window.nativeBridge === 'undefined'",
50
+ "layer": "Runtime",
51
+ "source": "static",
52
+ "tags": [
53
+ "transport",
54
+ "architecture"
55
+ ]
56
+ },
57
+ {
58
+ "section": "transport_architecture",
59
+ "title": "Operation Hook Pattern",
60
+ "content": "All async operations follow pattern: const { data, loading, error, progress } = useOperation(params). Allows consistent error handling, progress tracking, and loading states across all three transports.",
61
+ "layer": "Runtime",
62
+ "source": "static",
63
+ "tags": [
64
+ "hooks",
65
+ "api"
66
+ ]
67
+ },
68
+ {
69
+ "section": "transport_architecture",
70
+ "title": "Operation Type Selection",
71
+ "content": "Five operation types determine transport choice: 1) Fire&Forget (bridge), 2) Simple Async (bridge), 3) Event-Driven (bridge), 4) Long-Running (localhost server), 5) Size-Dependent (hybrid selection based on payload). Size thresholds: ≤2MB = direct intent. 2MB-50MB = content provider or localhost server. >50MB = chunked streaming.",
72
+ "layer": "Runtime",
73
+ "source": "static",
74
+ "tags": [
75
+ "transport",
76
+ "operations"
77
+ ]
78
+ },
79
+ {
80
+ "section": "transport_architecture",
81
+ "title": "Intent Size Limits",
82
+ "content": "2MB hard limit for direct Intent dispatch. Files 2MB-50MB use content provider (Android) or alternate server transport. Files >50MB use chunked streaming. Exceeding limits causes silent failure on Android.",
83
+ "layer": "Runtime",
84
+ "source": "static",
85
+ "tags": [
86
+ "transport",
87
+ "constraints"
88
+ ]
89
+ },
90
+ {
91
+ "section": "transport_architecture",
92
+ "title": "Localhost Server Whitelist Requirement",
93
+ "content": "Localhost server transport requires accessControl.allowedUrls to include http://localhost:* pattern. If allowedUrls is empty, ALL URLs are blocked. If allowedUrls is defined, only whitelisted URLs allowed.",
94
+ "layer": "Config",
95
+ "source": "static",
96
+ "tags": [
97
+ "transport",
98
+ "security",
99
+ "config"
100
+ ]
101
+ },
102
+ {
103
+ "section": "transport_architecture",
104
+ "title": "URL Whitelist Wildcard Support",
105
+ "content": "URL whitelist supports wildcard patterns: *.1mg.com* matches all 1mg subdomains. Whitelist checking is thread-safe. URL decoding bypass prevention prevents encoded URLs from bypassing whitelist checks.",
106
+ "layer": "Config",
107
+ "source": "static",
108
+ "tags": [
109
+ "security",
110
+ "url"
111
+ ]
112
+ },
113
+ {
114
+ "section": "transport_architecture",
115
+ "title": "External Domain Handling",
116
+ "content": "isExternalDomain() function identifies URLs outside whitelist. External URLs open in system browser (Android: Intent.ACTION_VIEW, iOS: SFSafariViewController), not in WebView.",
117
+ "layer": "Runtime",
118
+ "source": "static",
119
+ "tags": [
120
+ "security",
121
+ "url"
122
+ ]
123
+ },
124
+ {
125
+ "section": "bridge_architecture",
126
+ "title": "Android Bridge Flow",
127
+ "content": "JS → window.NativeBridge[command](data) → Kotlin @JavascriptInterface method → native work → webView.evaluateJavascript(jsCode) → JS callback. Command and data passed as JSON. Response injected back as JS code execution.",
128
+ "layer": "Bridge",
129
+ "source": "static",
130
+ "tags": [
131
+ "android",
132
+ "bridge",
133
+ "ipc"
134
+ ]
135
+ },
136
+ {
137
+ "section": "bridge_architecture",
138
+ "title": "iOS Bridge Flow",
139
+ "content": "JS → webkit.messageHandlers.NativeBridge.postMessage(msg) → WKScriptMessageHandler Swift handler → native work → evaluateJavaScript(jsCode) → JS callback. Message passed as JSON object. Response injected as JS code execution.",
140
+ "layer": "Bridge",
141
+ "source": "static",
142
+ "tags": [
143
+ "ios",
144
+ "bridge",
145
+ "ipc"
146
+ ]
147
+ },
148
+ {
149
+ "section": "bridge_architecture",
150
+ "title": "Android Bridge Security",
151
+ "content": "@JavascriptInterface annotation + whitelist enforcement prevents injection. All data JSON-escaping required. NativeBridge.kt must use notifyWebJson + JSONObject, never string concatenation. Enforced at code review.",
152
+ "layer": "Bridge",
153
+ "source": "static",
154
+ "tags": [
155
+ "android",
156
+ "bridge",
157
+ "security"
158
+ ]
159
+ },
160
+ {
161
+ "section": "bridge_architecture",
162
+ "title": "iOS Bridge Security",
163
+ "content": "WKScriptMessageHandler + JSONSerialization (native JSON parsing). BridgeMessageValidator enforces message structure. Lazy handler initialization prevents zombie references. No string concatenation in response building.",
164
+ "layer": "Bridge",
165
+ "source": "static",
166
+ "tags": [
167
+ "ios",
168
+ "bridge",
169
+ "security"
170
+ ]
171
+ },
172
+ {
173
+ "section": "bridge_architecture",
174
+ "title": "Bridge File Structure",
175
+ "content": "Key files: WebBridge.js (client-side bridge), NativeBridge.js (utility wrapper), NativeInterfaces.js (command/event constants), hooks.js (React hooks). Located in src/native/bridge/.",
176
+ "layer": "Bridge",
177
+ "source": "static",
178
+ "tags": [
179
+ "bridge",
180
+ "files"
181
+ ]
182
+ },
183
+ {
184
+ "section": "bridge_architecture",
185
+ "title": "isNative() Runtime Check Bug",
186
+ "content": "Bug: isNative() constructed at module load (SSR), window.nativeBridge absent → nativeEnvironment=false permanently. Never recovers in browser. Fix: check window.nativeBridge and window.webkit?.messageHandlers?.NativeBridge lazily at call time, not at module init.",
187
+ "layer": "Bridge",
188
+ "source": "static",
189
+ "tags": [
190
+ "bridge",
191
+ "bug",
192
+ "ssr"
193
+ ]
194
+ },
195
+ {
196
+ "section": "bridge_architecture",
197
+ "title": "11 Native Commands Implemented",
198
+ "content": "Documented commands: OPEN_CAMERA, REQUEST_CAMERA_PERMISSION, PICK_FILE, OPEN_FILE_WITH_INTENT, GET_DEVICE_INFO, REQUEST_HAPTIC_FEEDBACK (+ 5 more). Full list in catalyst-docs.",
199
+ "layer": "Bridge",
200
+ "source": "static",
201
+ "tags": [
202
+ "bridge",
203
+ "commands"
204
+ ]
205
+ },
206
+ {
207
+ "section": "bridge_architecture",
208
+ "title": "14 Callback Events Implemented",
209
+ "content": "Camera: CAMERA_PERMISSION_STATUS. File: ON_FILE_PICKED, ON_FILE_PICK_ERROR, ON_FILE_DOWNLOADED, ON_DOWNLOAD_ERROR (+ 9 more events). Full list in catalyst-docs.",
210
+ "layer": "Bridge",
211
+ "source": "static",
212
+ "tags": [
213
+ "bridge",
214
+ "events"
215
+ ]
216
+ },
217
+ {
218
+ "section": "build_system",
219
+ "title": "WEBVIEW_CONFIG Location and Schema",
220
+ "content": "Location: config/config.json. Schema: { port, android: { buildType, sdkPath, emulatorName, cachePattern }, ios: { buildType, appBundleId, simulatorName, cachePattern } }. cachePattern is comma-separated globs: *.css,*.js,*.png,*.svg,*.jpg",
221
+ "layer": "Config",
222
+ "source": "static",
223
+ "tags": [
224
+ "config",
225
+ "build"
226
+ ]
227
+ },
228
+ {
229
+ "section": "build_system",
230
+ "title": "Android Build Pipeline",
231
+ "content": "1) Load WEBVIEW_CONFIG 2) Start dev server on port 3000 3) Validate SDK/ADB/emulator 4) Launch emulator 5) Copy web assets (dist) to native project 6) Gradle build 7) Install APK. Located in: src/native/androidProject/",
232
+ "layer": "Build",
233
+ "source": "static",
234
+ "tags": [
235
+ "android",
236
+ "build"
237
+ ]
238
+ },
239
+ {
240
+ "section": "build_system",
241
+ "title": "iOS Build Pipeline",
242
+ "content": "1) Generate ConfigConstants.swift from WEBVIEW_CONFIG 2) Launch simulator 3) Clean Xcode project 4) Compile with Xcode 5) Install app + launch. Located in: src/native/iosnativeWebView/",
243
+ "layer": "Build",
244
+ "source": "static",
245
+ "tags": [
246
+ "ios",
247
+ "build"
248
+ ]
249
+ },
250
+ {
251
+ "section": "build_system",
252
+ "title": "Build NPM Scripts",
253
+ "content": "All catalyst npm scripts to add to package.json scripts block (each maps to: catalyst <scriptName>): devBuild — dev web build. devServe — dev server. buildApp — builds native app (both platforms). buildApp:android — Android native app build. buildApp:ios — iOS native app build. buildApp:android:release — Android release build. buildApp:ios:release — iOS release build. setupEmulator — sets up both emulators. setupEmulator:android — sets up Android AVD. setupEmulator:ios — sets up iOS simulator. NOTE: build:android / build:android:release / build:ios do NOT exist in catalyst — common mistake. The correct commands are buildApp:android and buildApp:ios. Missing any script = that npm run command fails.",
254
+ "layer": "Build",
255
+ "source": "static",
256
+ "tags": [
257
+ "build",
258
+ "scripts",
259
+ "package.json",
260
+ "native"
261
+ ]
262
+ },
263
+ {
264
+ "section": "build_system",
265
+ "title": "Cross-Platform Directory Copying",
266
+ "content": "Custom copyDirectory() JS utility (cross-platform recursive copy with exclusions) replaced rsync. Reason: rsync caused Windows issues, required manual CI install. New approach works on macOS, Windows, Linux without dependencies.",
267
+ "layer": "Build",
268
+ "source": "static",
269
+ "tags": [
270
+ "build",
271
+ "cross-platform"
272
+ ]
273
+ },
274
+ {
275
+ "section": "build_system",
276
+ "title": "Catalyst Dev Copy Workflow",
277
+ "content": "1) Edit source in catalyst-core/src/ 2) npm run prepare 3) Copy dist/native to node_modules/catalyst-core/ in test app 4) Copy package.json for exports map. Test app uses node_modules/catalyst-core/dist, not src directly.",
278
+ "layer": "Build",
279
+ "source": "static",
280
+ "tags": [
281
+ "build",
282
+ "workflow",
283
+ "development"
284
+ ]
285
+ },
286
+ {
287
+ "section": "build_system",
288
+ "title": "ERR_PACKAGE_PATH_NOT_EXPORTED Fix",
289
+ "content": "Node 20 strict exports require ./package.json entry in exports map. Error occurs when test app imports from catalyst-core. Fix: copy catalyst-core/package.json to test app's node_modules/catalyst-core/package.json after each build.",
290
+ "layer": "Build",
291
+ "source": "static",
292
+ "tags": [
293
+ "build",
294
+ "nodejs",
295
+ "exports"
296
+ ]
297
+ },
298
+ {
299
+ "section": "security",
300
+ "title": "useDataProtection Hook",
301
+ "content": "Three primitives available: setScreenSecure (prevents screenshots/recents in Android, blur in iOS), getScreenSecure (internal only, read current state), clearWebData (clears all web storage). Called via bridge on app launch or user action.",
302
+ "layer": "Runtime",
303
+ "source": "static",
304
+ "tags": [
305
+ "security",
306
+ "hooks",
307
+ "dataprotection"
308
+ ]
309
+ },
310
+ {
311
+ "section": "security",
312
+ "title": "FLAG_SECURE Android Behavior",
313
+ "content": "setScreenSecure(true) sets FLAG_SECURE on WebView. Shows black/blank screenshot in Android recents. This is CORRECT behavior, not a bug. Emulators do NOT enforce FLAG_SECURE in recents — must test on real device to verify.",
314
+ "layer": "Runtime",
315
+ "source": "static",
316
+ "tags": [
317
+ "android",
318
+ "security",
319
+ "flag_secure"
320
+ ]
321
+ },
322
+ {
323
+ "section": "security",
324
+ "title": "allowBackup Configuration",
325
+ "content": "Location: WEBVIEW_CONFIG.android.security.allowBackup (true/false). Maps to android.security.allowBackup in AndroidManifest.xml via manifestPlaceholders[\"allowBackup\"] in defaultConfig. Controls whether app data backed up to Google cloud.",
326
+ "layer": "Config",
327
+ "source": "static",
328
+ "tags": [
329
+ "android",
330
+ "security",
331
+ "backup"
332
+ ]
333
+ },
334
+ {
335
+ "section": "security",
336
+ "title": "clearWebData Behavior",
337
+ "content": "clearWebData does NOT clear WebCacheManager/CacheManager by default — these are custom native disk+memory caches separate from WebView built-in cache (cookies, localStorage, etc). Both must be explicitly cleared via separate native calls.",
338
+ "layer": "Runtime",
339
+ "source": "static",
340
+ "tags": [
341
+ "security",
342
+ "cache",
343
+ "clearing"
344
+ ]
345
+ },
346
+ {
347
+ "section": "security",
348
+ "title": "iOS clearWebData Implementation",
349
+ "content": "WKWebsiteDataStore clearing must wrap completion in DispatchQueue.main.async. Confirmed working in production. Note: Safari Web Inspector does NOT auto-refresh storage panel after clearWebData — misleading but not a bug.",
350
+ "layer": "Runtime",
351
+ "source": "static",
352
+ "tags": [
353
+ "ios",
354
+ "security",
355
+ "clearing"
356
+ ]
357
+ },
358
+ {
359
+ "section": "security",
360
+ "title": "iOS setScreenSecure JSON Parsing",
361
+ "content": "setScreenSecure params arrive as JSON string from JS bridge in iOS. Requires JSON-parse branch: if let str = params as? String, let jsonData = str.data(...). Android params already parsed as JSONObject.",
362
+ "layer": "Bridge",
363
+ "source": "static",
364
+ "tags": [
365
+ "ios",
366
+ "security",
367
+ "json"
368
+ ]
369
+ },
370
+ {
371
+ "section": "security",
372
+ "title": "Security Checks Run Release-Only",
373
+ "content": "RootBeer, Frida, emulator checks run only when !BuildConfig.DEBUG. Prevents false positives during development (emulator, rooted for testing, Frida server running).",
374
+ "layer": "Runtime",
375
+ "source": "static",
376
+ "tags": [
377
+ "android",
378
+ "security",
379
+ "checks"
380
+ ]
381
+ },
382
+ {
383
+ "section": "security",
384
+ "title": "Security Check Flow",
385
+ "content": "App Launch → MainActivity.onCreate() → SecurityCheckScheduler.initialize() → background coroutine → SecurityCheckManager.performSecurityChecks() → rooting + emulator + Frida checks → callback → SecurityAlertHandler → recommendation returned to JS.",
386
+ "layer": "Runtime",
387
+ "source": "static",
388
+ "tags": [
389
+ "android",
390
+ "security",
391
+ "flow"
392
+ ]
393
+ },
394
+ {
395
+ "section": "security",
396
+ "title": "Security Compromise Detection",
397
+ "content": "isCompromised = true if ANY of: rooted OR emulator detected OR Frida detected. Single check triggers = compromise (strict, not majority vote). Recommendation: simple string 'BLOCK' or 'ALLOW' only.",
398
+ "layer": "Runtime",
399
+ "source": "static",
400
+ "tags": [
401
+ "android",
402
+ "security",
403
+ "detection"
404
+ ]
405
+ },
406
+ {
407
+ "section": "security",
408
+ "title": "RootBeer Version Requirement",
409
+ "content": "RootBeer 0.1.1 required for Android 15+ (handles 16KB page size alignment). Earlier versions fail on Android 15+.",
410
+ "layer": "Build",
411
+ "source": "static",
412
+ "tags": [
413
+ "android",
414
+ "security",
415
+ "dependencies"
416
+ ]
417
+ },
418
+ {
419
+ "section": "security",
420
+ "title": "Emulator Detection Threshold",
421
+ "content": "ANY single emulator check = emulator detection (not majority vote). Strict detection ensures no false negatives.",
422
+ "layer": "Runtime",
423
+ "source": "static",
424
+ "tags": [
425
+ "android",
426
+ "security",
427
+ "emulator"
428
+ ]
429
+ },
430
+ {
431
+ "section": "security",
432
+ "title": "Frida Detection Port Scan",
433
+ "content": "Scans ports 27042, 27043 for Frida server. 4 second latency acceptable — runs on background coroutine while WebView already showing on clean devices.",
434
+ "layer": "Runtime",
435
+ "source": "static",
436
+ "tags": [
437
+ "android",
438
+ "security",
439
+ "frida"
440
+ ]
441
+ },
442
+ {
443
+ "section": "security",
444
+ "title": "Security Response Structure",
445
+ "content": "{ security: { isRooted: boolean, isEmulator: boolean, isFridaDetected: boolean, isCompromised: boolean, recommendation: 'BLOCK'|'ALLOW', timestamp: long } }",
446
+ "layer": "Runtime",
447
+ "source": "static",
448
+ "tags": [
449
+ "security",
450
+ "response"
451
+ ]
452
+ },
453
+ {
454
+ "section": "security",
455
+ "title": "Security File Locations",
456
+ "content": "Security implementation files: src/native/androidProject/app/src/main/java/io/yourname/androidproject/security/. Includes SecurityCheckManager, RootBeer checks, Frida detection, SecurityAlertHandler.",
457
+ "layer": "Build",
458
+ "source": "static",
459
+ "tags": [
460
+ "android",
461
+ "security",
462
+ "files"
463
+ ]
464
+ },
465
+ {
466
+ "section": "security",
467
+ "title": "Play Integrity API Status",
468
+ "content": "Play Integrity integration deferred. All code commented with TODO markers. securityRoutes.js NOT committed. expressServer.js NOT committed. Local checks sufficient for now.",
469
+ "layer": "Runtime",
470
+ "source": "static",
471
+ "tags": [
472
+ "android",
473
+ "security",
474
+ "integrity"
475
+ ]
476
+ },
477
+ {
478
+ "section": "caching",
479
+ "title": "Cache Pattern Bug and Fix",
480
+ "content": "Bug: cache pattern matching matched against full URLs instead of filenames → only 7/64 files cached (9% hit rate). Fix: extract filename from URL before matching against cachePattern (*.css, *.js, etc). After fix: 100% cache hit rate.",
481
+ "layer": "Runtime",
482
+ "source": "static",
483
+ "tags": [
484
+ "caching",
485
+ "bug",
486
+ "fix"
487
+ ]
488
+ },
489
+ {
490
+ "section": "caching",
491
+ "title": "Custom Cache Implementation",
492
+ "content": "WebCacheManager.kt (Android) and CacheManager.swift (iOS) implement custom native disk+memory caches. Separate from WebView built-in cache (cookies, localStorage). Configured via WEBVIEW_CONFIG.*.cachePattern.",
493
+ "layer": "Runtime",
494
+ "source": "static",
495
+ "tags": [
496
+ "caching",
497
+ "android",
498
+ "ios"
499
+ ]
500
+ },
501
+ {
502
+ "section": "known_errors",
503
+ "title": "Silent Intent Failure on Size Exceeded",
504
+ "content": "Symptom: file open/share silently does nothing. Cause: Intent payload >2MB — Android drops it without error callback or exception. Fix: validate file size before bridge dispatch, use content provider or localhost server for files >2MB.",
505
+ "layer": "Runtime",
506
+ "source": "static",
507
+ "tags": [
508
+ "android",
509
+ "error",
510
+ "intent"
511
+ ]
512
+ },
513
+ {
514
+ "section": "known_errors",
515
+ "title": "isNative() Always Returns False (SSR)",
516
+ "content": "Symptom: bridge hooks do nothing in browser, native features not available. Cause: NativeBridgeUtil constructed at module load during SSR — window.NativeBridge absent at that point, isNativeEnvironment=false permanently. Fix: check window.NativeBridge and window.webkit?.messageHandlers?.NativeBridge lazily at hook call time, not at module init.",
517
+ "layer": "Bridge",
518
+ "source": "static",
519
+ "tags": [
520
+ "error",
521
+ "ssr",
522
+ "bridge",
523
+ "bug"
524
+ ]
525
+ },
526
+ {
527
+ "section": "known_errors",
528
+ "title": "Cache Hit Rate Near Zero",
529
+ "content": "Symptom: most static assets (CSS, JS, images) load from network every time despite cachePattern being configured. Cause: cache pattern matching against full URL instead of filename — URL contains path + query params that don't match *.css etc. Fix: extract filename from URL before pattern matching.",
530
+ "layer": "Runtime",
531
+ "source": "static",
532
+ "tags": [
533
+ "error",
534
+ "caching",
535
+ "android",
536
+ "ios"
537
+ ]
538
+ },
539
+ {
540
+ "section": "known_errors",
541
+ "title": "clearWebData Doesn't Clear Native Cache",
542
+ "content": "Symptom: clearWebData called but cached assets still serve from disk. Cause: clearWebData only clears WebView built-in storage (cookies, localStorage) — not WebCacheManager (Android) or CacheManager (iOS) which are separate custom caches. Fix: call WebCacheManager.clearAll() on Android and CacheManager.clearCache() on iOS in addition to clearWebData.",
543
+ "layer": "Runtime",
544
+ "source": "static",
545
+ "tags": [
546
+ "error",
547
+ "caching",
548
+ "security",
549
+ "clearing"
550
+ ]
551
+ },
552
+ {
553
+ "section": "known_errors",
554
+ "title": "Localhost Server Transport Not Working",
555
+ "content": "Symptom: file operations, downloads, or progress-tracked operations fail silently or 404. Cause: accessControl.allowedUrls is defined but doesn't include http://localhost:* — all localhost requests blocked. Fix: add http://localhost:* and https://localhost:* to accessControl.allowedUrls.",
556
+ "layer": "Config",
557
+ "source": "static",
558
+ "tags": [
559
+ "error",
560
+ "transport",
561
+ "server",
562
+ "config"
563
+ ]
564
+ },
565
+ {
566
+ "section": "known_errors",
567
+ "title": "ERR_PACKAGE_PATH_NOT_EXPORTED",
568
+ "content": "Symptom: import from catalyst-core fails with ERR_PACKAGE_PATH_NOT_EXPORTED in Node 20. Cause: Node 20 strict exports require ./package.json entry in exports map. Occurs when test app's node_modules/catalyst-core/package.json is outdated. Fix: copy catalyst-core/package.json to test app's node_modules/catalyst-core/ after each npm run prepare.",
569
+ "layer": "Build",
570
+ "source": "static",
571
+ "tags": [
572
+ "error",
573
+ "build",
574
+ "nodejs",
575
+ "exports"
576
+ ]
577
+ },
578
+ {
579
+ "section": "known_errors",
580
+ "title": "FLAG_SECURE Not Working on Emulator",
581
+ "content": "Symptom: setScreenSecure(true) called but screenshots still work, recents shows live preview. Cause: emulators do NOT enforce FLAG_SECURE — this is expected Android emulator behavior, not a bug. Fix: test FLAG_SECURE behavior on real device only.",
582
+ "layer": "Runtime",
583
+ "source": "static",
584
+ "tags": [
585
+ "error",
586
+ "android",
587
+ "security",
588
+ "emulator"
589
+ ]
590
+ },
591
+ {
592
+ "section": "known_errors",
593
+ "title": "iOS clearWebData Appears Not Working",
594
+ "content": "Symptom: clearWebData called on iOS, Safari Web Inspector still shows localStorage data. Cause: Safari Web Inspector does NOT auto-refresh its storage panel — the data is actually cleared. Fix: manually refresh the storage panel in Web Inspector to confirm.",
595
+ "layer": "Runtime",
596
+ "source": "static",
597
+ "tags": [
598
+ "error",
599
+ "ios",
600
+ "security",
601
+ "clearing"
602
+ ]
603
+ },
604
+ {
605
+ "section": "known_errors",
606
+ "title": "RootBeer Fails on Android 15+",
607
+ "content": "Symptom: app crashes or security checks fail on Android 15+ devices. Cause: older RootBeer versions don't support 16KB page size alignment required by Android 15. Fix: upgrade to RootBeer 0.1.1 minimum.",
608
+ "layer": "Build",
609
+ "source": "static",
610
+ "tags": [
611
+ "error",
612
+ "android",
613
+ "security",
614
+ "rootbeer"
615
+ ]
616
+ },
617
+ {
618
+ "section": "webview_config",
619
+ "title": "Real WEBVIEW_CONFIG Example",
620
+ "content": "Full WEBVIEW_CONFIG example (as of canary.19+): { port: '3005', LOCAL_IP: '192.168.0.11', useHttps: false, appInfo: 'android-5Feb2026-v2.1.0', cachePattern: '*.css', accessControl: { enabled: false, allowedUrls: ['*.yourdomain.com*', 'http://localhost:*'] }, android: { appName: 'MyApp', packageName: 'com.example.app', buildType: 'debug', sdkPath: '/Users/user/Android/', emulatorName: 'Pixel_5_API_30', cachePattern: '*.css,*.js,*.png', security: { mode: 'custom', allowBackup: true } }, ios: { appBundleId: 'com.example.app', appName: 'MyApp', buildType: 'debug', simulatorName: 'iPhone 17 Pro', cachePattern: '*.css,*.js', accessControl: { enabled: true, allowedUrls: ['*.yourdomain.com*', 'http://localhost:*'] } }, splashScreen: { backgroundColor: '#ffffff', duration: 2000, imageWidth: 400, imageHeight: 200, cornerRadius: 20 }, notifications: { enabled: false } }. Note: splashScreen is INSIDE WEBVIEW_CONFIG, not at top-level config.json. Note canary.19 change: iOS has its OWN accessControl block separate from top-level accessControl.",
621
+ "layer": "Config",
622
+ "source": "extracted:config/config.json",
623
+ "tags": [
624
+ "config",
625
+ "example"
626
+ ]
627
+ },
628
+ {
629
+ "section": "config_structure",
630
+ "title": "Full config/config.json Schema",
631
+ "content": "config/config.json contains all runtime config. Required keys (enforced by validator.js at startup — missing any = server crash): NODE_SERVER_HOSTNAME (string), NODE_SERVER_PORT (number), WEBPACK_DEV_SERVER_HOSTNAME (string), WEBPACK_DEV_SERVER_PORT (number), BUILD_OUTPUT_PATH (string, e.g. 'build'), PUBLIC_STATIC_ASSET_PATH (string, e.g. '/assets/'), PUBLIC_STATIC_ASSET_URL (string, CDN base URL), CLIENT_ENV_VARIABLES (array of env var names to expose to client bundle), ANALYZE_BUNDLE (boolean). Optional top-level keys: API_URL (string, app convention not enforced), WEBVIEW_CONFIG (object, required for native builds — contains splashScreen, appInfo, android, ios, etc.). Common mistake: CLIENT_ENV_KEYS is WRONG — the actual key is CLIENT_ENV_VARIABLES. Common mistake: splashScreen is NOT a top-level key — it lives inside WEBVIEW_CONFIG. Example: { NODE_SERVER_HOSTNAME: 'localhost', NODE_SERVER_PORT: 3000, WEBPACK_DEV_SERVER_HOSTNAME: 'localhost', WEBPACK_DEV_SERVER_PORT: 3001, BUILD_OUTPUT_PATH: 'build', PUBLIC_STATIC_ASSET_PATH: '/assets/', PUBLIC_STATIC_ASSET_URL: 'https://cdn.example.com', CLIENT_ENV_VARIABLES: ['API_URL'], ANALYZE_BUNDLE: false }",
632
+ "layer": "Config",
633
+ "source": "extracted:src/scripts/validator.js",
634
+ "tags": [
635
+ "config",
636
+ "schema",
637
+ "required-fields"
638
+ ]
639
+ },
640
+ {
641
+ "section": "config_structure",
642
+ "title": "WEBVIEW_CONFIG Required Fields",
643
+ "content": "WEBVIEW_CONFIG field reference (from buildAppAndroid.js + buildAppIos.js source):\n\nTOP-LEVEL (both platforms):\n- port (string, required) — web server port e.g. \"3005\"\n- LOCAL_IP (string, required) — machine LAN IP e.g. \"192.168.0.11\". NOT localhost — emulator cannot resolve it\n- appInfo (string, required) — build identifier e.g. \"android-5Feb2026-v2.1.0\". Missing = iOS build breaks\n- useHttps (boolean, optional) — default false\n- cachePattern (string, optional) — comma-separated globs e.g. \"*.css\"\n- accessControl.enabled (boolean, optional) — default false\n- accessControl.allowedUrls (array, required if enabled) — URL patterns\n\nANDROID (WEBVIEW_CONFIG.android):\n- sdkPath (string, REQUIRED) — abs path to Android SDK. Build exits immediately without it\n- emulatorName (string, REQUIRED for debug) — AVD name. Run: emulator -list-avds\n- appName (string, optional) — default \"Catalyst Application\"\n- packageName (string, optional) — e.g. com.company.app. Derived from appName if missing\n- buildType (string, optional) — \"debug\" or \"release\". Default \"debug\"\n- keystore or keystoreConfig (required for release buildType only)\n- buildOptimisation (boolean, optional) — default false\n- cachePattern (string, optional)\n\nIOS (WEBVIEW_CONFIG.ios):\n- appBundleId (string, optional) — default \"com.debug.webview\". Must set for distribution\n- appName (string, optional) — default \"Catalyst Application\"\n- simulatorName (string, optional) — auto-detected if missing. e.g. \"iPhone 17 Pro\"\n- buildType (string, optional, CASE-SENSITIVE) — \"Debug\" or \"Release\". NOT lowercase — common mistake\n- deviceUDID (string, optional) — physical device builds only\n- developmentTeam (string, required if deviceUDID set) — Apple Developer Team ID\n- cachePattern (string, optional)",
644
+ "layer": "Config",
645
+ "source": "static",
646
+ "tags": [
647
+ "config",
648
+ "WEBVIEW_CONFIG",
649
+ "android",
650
+ "ios",
651
+ "required",
652
+ "fields"
653
+ ]
654
+ },
655
+ {
656
+ "section": "config_structure",
657
+ "title": "splashScreen Config Structure",
658
+ "content": "splashScreen sits INSIDE WEBVIEW_CONFIG (not at top-level config.json). All fields are optional — omitting splashScreen entirely = native build proceeds but shows blank splash. Fields: backgroundColor (string hex, default '#ffffff'), duration (number ms, iOS only — controls how long splash shows), imageWidth (number px, default 120), imageHeight (number px, default 120), cornerRadius (number px, iOS only, default 20). Example: WEBVIEW_CONFIG.splashScreen = { backgroundColor: '#ffffff', duration: 2000, imageWidth: 400, imageHeight: 200, cornerRadius: 20 }. Asset placement: put image at public/android/splashscreen.{png|jpg|jpeg|gif|bmp|webp} for Android, public/ios/splashscreen.{png|jpg|jpeg} for iOS. Asset is auto-discovered by filename — NO path field in config. Missing asset file = build proceeds with no splash image (no error). backgroundColor is applied to Android themes.xml (windowSplashScreenBackground). Pfizer example: splashScreen: { imageHeight: 200, imageWidth: 400 } (backgroundColor omits = defaults to white).",
659
+ "layer": "Config",
660
+ "source": "static",
661
+ "tags": [
662
+ "splashscreen",
663
+ "splash",
664
+ "config",
665
+ "webview_config",
666
+ "android",
667
+ "ios",
668
+ "native",
669
+ "assets"
670
+ ]
671
+ },
672
+ {
673
+ "section": "config_structure",
674
+ "title": "accessControl Config",
675
+ "content": "WEBVIEW_CONFIG.accessControl: { enabled: boolean, allowedUrls: string[] }. When enabled=true and allowedUrls is non-empty, only whitelisted URLs load in WebView. When allowedUrls is empty, ALL URLs are blocked. Must include http://localhost:* for localhost server transport. Wildcard: *.domain.com* matches all subdomains.",
676
+ "layer": "Config",
677
+ "source": "static",
678
+ "tags": [
679
+ "config",
680
+ "security",
681
+ "access-control"
682
+ ]
683
+ },
684
+ {
685
+ "section": "routing",
686
+ "title": "Catalyst Routing Stack",
687
+ "content": "@tata1mg/router wraps React Router v6. Routes defined in src/js/routes/index.js as flat or nested array. RouterDataProvider in src/js/routes/utils.js wraps the entire app — required for data fetching to work. Without RouterDataProvider, serverFetcher/clientFetcher hooks silently return undefined.",
688
+ "layer": "Runtime",
689
+ "source": "static",
690
+ "tags": [
691
+ "routing",
692
+ "react-router",
693
+ "required"
694
+ ]
695
+ },
696
+ {
697
+ "section": "routing",
698
+ "title": "Route Definition Format",
699
+ "content": "Routes defined as array of objects: [{ path: '/', component: HomePage, exact: true }, { path: '/product/:id', component: ProductPage }]. Each component must export serverFetcher and/or clientFetcher as static properties for data loading. Route file location: src/js/routes/index.js.",
700
+ "layer": "Runtime",
701
+ "source": "static",
702
+ "tags": [
703
+ "routing",
704
+ "routes",
705
+ "format"
706
+ ]
707
+ },
708
+ {
709
+ "section": "routing",
710
+ "title": "RouterDataProvider Setup",
711
+ "content": "RouterDataProvider from @tata1mg/router wraps the app in src/js/routes/utils.js. Provides route-level data context to all child components. useCurrentRouteData() reads from this context. Missing RouterDataProvider = all useCurrentRouteData() calls return undefined, no error thrown.",
712
+ "layer": "Runtime",
713
+ "source": "static",
714
+ "tags": [
715
+ "routing",
716
+ "RouterDataProvider",
717
+ "context"
718
+ ]
719
+ },
720
+ {
721
+ "section": "routing",
722
+ "title": "App Shell with Outlet",
723
+ "content": "src/js/containers/App/index.js must render <Outlet /> from @tata1mg/router (not react-router-dom directly). Outlet renders the matched route component. Without Outlet, routing matches but nothing renders. App shell is also where global layout (nav, footer) lives.",
724
+ "layer": "Component",
725
+ "source": "static",
726
+ "tags": [
727
+ "routing",
728
+ "outlet",
729
+ "app-shell"
730
+ ]
731
+ },
732
+ {
733
+ "section": "data_fetching",
734
+ "title": "serverFetcher Pattern",
735
+ "content": "Static function attached to page component: HomePage.serverFetcher = async ({ params, searchParams, navigate }, { store }) => { const data = await fetchApi(); return data; }. Runs on server during SSR. Result available via useCurrentRouteData(). NEVER use useEffect for initial data load — serverFetcher is the catalyst way.",
736
+ "layer": "Runtime",
737
+ "source": "static",
738
+ "tags": [
739
+ "data-fetching",
740
+ "ssr",
741
+ "server-fetcher"
742
+ ]
743
+ },
744
+ {
745
+ "section": "data_fetching",
746
+ "title": "clientFetcher Pattern",
747
+ "content": "Static function attached to page component: HomePage.clientFetcher = async ({ params }, { store }, customArgs) => { ... }. Runs on client-side navigation (not initial SSR load). Receives customArgs for manual trigger. Complement to serverFetcher — server handles initial load, client handles navigations.",
748
+ "layer": "Runtime",
749
+ "source": "static",
750
+ "tags": [
751
+ "data-fetching",
752
+ "csr",
753
+ "client-fetcher"
754
+ ]
755
+ },
756
+ {
757
+ "section": "data_fetching",
758
+ "title": "useCurrentRouteData Hook",
759
+ "content": "import { useCurrentRouteData } from '@tata1mg/router'. Returns data resolved by serverFetcher/clientFetcher for the current route. Replaces all useState + useEffect data loading patterns. Available only inside RouterDataProvider tree. Returns undefined if RouterDataProvider missing.",
760
+ "layer": "Runtime",
761
+ "source": "static",
762
+ "tags": [
763
+ "data-fetching",
764
+ "hooks",
765
+ "useCurrentRouteData"
766
+ ]
767
+ },
768
+ {
769
+ "section": "data_fetching",
770
+ "title": "Migration: useEffect → serverFetcher",
771
+ "content": "Before: const [data, setData] = useState(null); useEffect(() => { fetchApi().then(setData) }, []). After: attach static HomePage.serverFetcher = async (...) => fetchApi(), then const data = useCurrentRouteData() in component body. Every page's data layer must be rewritten this way — there is no incremental path.",
772
+ "layer": "Runtime",
773
+ "source": "static",
774
+ "tags": [
775
+ "data-fetching",
776
+ "migration",
777
+ "useEffect"
778
+ ]
779
+ },
780
+ {
781
+ "section": "server_structure",
782
+ "title": "Required Server Files",
783
+ "content": "Catalyst requires 3 specific files in server/: server/index.js (lifecycle hooks), server/server.js (Express middleware), server/document.js (HTML template). These are NOT optional. Missing any one = server startup fails. Any repo using custom Express, Next.js, CRA, or Vite must rebuild all 3 from scratch.",
784
+ "layer": "Build",
785
+ "source": "static",
786
+ "tags": [
787
+ "server",
788
+ "required-files",
789
+ "structure"
790
+ ]
791
+ },
792
+ {
793
+ "section": "server_structure",
794
+ "title": "server/index.js Lifecycle Hooks",
795
+ "content": "server/index.js exports lifecycle hooks: { preServerInit, onRouteMatch, onServerError }. preServerInit runs before Express starts (DB connections, config loading). onRouteMatch runs on each request (auth checks, redirects). onServerError handles uncaught errors. Minimum: export default {} (empty object) is valid.",
796
+ "layer": "Build",
797
+ "source": "static",
798
+ "tags": [
799
+ "server",
800
+ "lifecycle",
801
+ "hooks"
802
+ ]
803
+ },
804
+ {
805
+ "section": "server_structure",
806
+ "title": "server/server.js Middleware",
807
+ "content": "server/server.js must export addMiddlewares(app) function: export const addMiddlewares = (app) => { app.use(cors()); app.use(helmet()); }. Catalyst calls addMiddlewares(expressApp) before routes are registered. Use this for auth middleware, rate limiting, request logging, CORS, etc.",
808
+ "layer": "Build",
809
+ "source": "static",
810
+ "tags": [
811
+ "server",
812
+ "middleware",
813
+ "express"
814
+ ]
815
+ },
816
+ {
817
+ "section": "server_structure",
818
+ "title": "server/document.js HTML Template",
819
+ "content": "server/document.js exports HTML shell for SSR responses: export default ({ html, head, scripts }) => `<!DOCTYPE html><html><head>${head}</head><body><div id='root'>${html}</div>${scripts}</body></html>`. Catalyst injects SSR HTML, CSS links, and script tags via the template params. Custom meta tags, fonts, and analytics go here.",
820
+ "layer": "Build",
821
+ "source": "static",
822
+ "tags": [
823
+ "server",
824
+ "ssr",
825
+ "html-template"
826
+ ]
827
+ },
828
+ {
829
+ "section": "server_structure",
830
+ "title": "Client Entry Files",
831
+ "content": "client/index.js hydrates SSR markup: import { hydrate } from '@tata1mg/router'; hydrate(<App />, document.getElementById('root')). client/styles.js imports global CSS: import '../src/styles/global.css'. Both files required. Missing client/index.js = blank page on web (no hydration). Missing client/styles.js = styles not bundled.",
832
+ "layer": "Build",
833
+ "source": "static",
834
+ "tags": [
835
+ "client",
836
+ "hydration",
837
+ "entry-files"
838
+ ]
839
+ },
840
+ {
841
+ "section": "native_hooks",
842
+ "title": "Native Hooks — Overview & Common Interface",
843
+ "content": "All catalyst-core hooks share a standardized base interface: { data, loading, progress, error, isWeb, isNative, execute, clear, clearError }. isNative is true when running inside the native shell (Android/iOS WebView). isWeb is true on browser. Hooks throw if window.WebBridge is not initialized — safe to call on web (isNative will be false). All return SSR-safe stubs when window is undefined. Import from catalyst-core/hooks.",
844
+ "layer": "Runtime",
845
+ "source": "static",
846
+ "tags": [
847
+ "native-hooks",
848
+ "overview",
849
+ "base-interface"
850
+ ]
851
+ },
852
+ {
853
+ "section": "native_hooks",
854
+ "title": "useCamera — return shape",
855
+ "content": "useCamera() returns: { data: { fileSrc, fileName, size, mimeType, transport }, loading, progress, error, isWeb, isNative, execute, clear, clearError, permission, takePhoto (=execute), photo (=data), isLoading (=loading), clearPhoto (=clear) }. Call execute()/takePhoto() to open camera. Data arrives via WebBridge callback ON_CAMERA_CAPTURE. transport: 'BRIDGE_BASE64' | 'FRAMEWORK_SERVER' | 'LEGACY'.",
856
+ "layer": "Runtime",
857
+ "source": "static",
858
+ "tags": [
859
+ "native-hooks",
860
+ "useCamera",
861
+ "return-shape"
862
+ ]
863
+ },
864
+ {
865
+ "section": "native_hooks",
866
+ "title": "useCameraPermission — return shape",
867
+ "content": "useCameraPermission() returns: { permission: string|null, isLoading: boolean }. permission values: 'granted' | 'denied' | 'not_determined'. Automatically requests permission on mount. Use before useCamera — check permission === 'granted' before showing camera UI.",
868
+ "layer": "Runtime",
869
+ "source": "static",
870
+ "tags": [
871
+ "native-hooks",
872
+ "useCameraPermission",
873
+ "return-shape"
874
+ ]
875
+ },
876
+ {
877
+ "section": "native_hooks",
878
+ "title": "useFilePicker — return shape",
879
+ "content": "useFilePicker() returns: { data, loading, progress, error, isWeb, isNative, execute, clear, clearError, getFileObject: async(index) => File, getAllFileObjects: async() => File[], canCreateFileObject: boolean, canCreateFileObjects: boolean[], selectedFile (=files[0]), selectedFiles, pickFile (=execute), isLoading, processingState, clearFile (=clear) }. Call execute(options)/pickFile(options) to open native file picker. options: { mimeType, multiple, maxFiles, maxFileSize }. getFileObject/getAllFileObjects convert native file data to JS File objects for FormData POST.",
880
+ "layer": "Runtime",
881
+ "source": "static",
882
+ "tags": [
883
+ "native-hooks",
884
+ "useFilePicker",
885
+ "return-shape"
886
+ ]
887
+ },
888
+ {
889
+ "section": "native_hooks",
890
+ "title": "useIntent — return shape",
891
+ "content": "useIntent() returns: { data: { result, success }, loading, progress, error, isWeb, isNative, execute, clear, clearError, openFile (=execute), isLoading, processingState, success, reset (=clear) }. Call execute(fileUrl, mimeType)/openFile(fileUrl, mimeType) to open a file with an external native app. Use for opening PDFs, docs in native viewers.",
892
+ "layer": "Runtime",
893
+ "source": "static",
894
+ "tags": [
895
+ "native-hooks",
896
+ "useIntent",
897
+ "return-shape"
898
+ ]
899
+ },
900
+ {
901
+ "section": "native_hooks",
902
+ "title": "useGoogleSignIn — return shape",
903
+ "content": "useGoogleSignIn(defaultOptions?) returns: { data: { idToken, ...user }, loading, progress, error, isWeb, isNative, signIn (=execute), execute, clear, clearError }. Call signIn(options)/execute(options) to trigger Google sign-in. options merged with defaultOptions. Requires google-services.json (Android) / GoogleService-Info.plist (iOS). On web: isNative false.",
904
+ "layer": "Runtime",
905
+ "source": "static",
906
+ "tags": [
907
+ "native-hooks",
908
+ "useGoogleSignIn",
909
+ "return-shape"
910
+ ]
911
+ },
912
+ {
913
+ "section": "native_hooks",
914
+ "title": "useHapticFeedback — return shape",
915
+ "content": "useHapticFeedback() returns: { data, loading, progress, error, isWeb, isNative, execute, clear, clearError, trigger (=execute), triggerHaptic (=execute), light/medium/heavy/success/warning/errorHaptic/selection/impact (shortcuts), capabilities: { isSupported, availableTypes, platform }, isSupported, isAvailable (=isSupported), availableTypes, HAPTIC_TYPES }. Call execute(type)/trigger(type). Types: 'light'|'medium'|'heavy'|'success'|'warning'|'error'|'selection'|'impact'. On web: falls back to navigator.vibrate if available.",
916
+ "layer": "Runtime",
917
+ "source": "static",
918
+ "tags": [
919
+ "native-hooks",
920
+ "useHapticFeedback",
921
+ "return-shape"
922
+ ]
923
+ },
924
+ {
925
+ "section": "native_hooks",
926
+ "title": "useNotification — return shape",
927
+ "content": "useNotification() returns: { data, loading, progress, error, execute (=scheduleLocal), clear, clearError, permissionStatus, pushToken, badges, lastNotification, subscribedTopics, scheduleLocal(notification), cancelLocal(id), registerForPush(), updateBadge(count), subscribeToTopic(topic), unsubscribeFromTopic(topic), getSubscribedTopics() }. Requires WEBVIEW_CONFIG.notifications.enabled=true + Firebase files.",
928
+ "layer": "Runtime",
929
+ "source": "static",
930
+ "tags": [
931
+ "native-hooks",
932
+ "useNotification",
933
+ "return-shape"
934
+ ]
935
+ },
936
+ {
937
+ "section": "native_hooks",
938
+ "title": "useNotificationPermission — return shape",
939
+ "content": "useNotificationPermission() returns: { permission: string|null, isLoading: boolean }. permission values: 'granted' | 'denied' | 'not_determined'. Automatically requests permission on mount. Use before useNotification push registration.",
940
+ "layer": "Runtime",
941
+ "source": "static",
942
+ "tags": [
943
+ "native-hooks",
944
+ "useNotificationPermission",
945
+ "return-shape"
946
+ ]
947
+ },
948
+ {
949
+ "section": "native_hooks",
950
+ "title": "useNetworkStatus — return shape",
951
+ "content": "useNetworkStatus() returns: { online: boolean, type: string|null, error: string|null }. type: 'wifi'|'cellular'|null. On web: initializes from navigator.onLine, no live updates. On native: live updates via WebBridge ON_NETWORK_STATUS_CHANGED. No isNative guard needed — works on both platforms.",
952
+ "layer": "Runtime",
953
+ "source": "static",
954
+ "tags": [
955
+ "native-hooks",
956
+ "useNetworkStatus",
957
+ "return-shape"
958
+ ]
959
+ },
960
+ {
961
+ "section": "native_hooks",
962
+ "title": "useSafeArea — return shape",
963
+ "content": "useSafeArea() returns: { top: number, right: number, bottom: number, left: number } — inset values in pixels. On web/SSR: all values 0. On native: live updates via WebBridge ON_SAFE_AREA_INSETS_UPDATED. No isNative guard needed — safe to use on both platforms.",
964
+ "layer": "Runtime",
965
+ "source": "static",
966
+ "tags": [
967
+ "native-hooks",
968
+ "useSafeArea",
969
+ "return-shape"
970
+ ]
971
+ },
972
+ {
973
+ "section": "native_hooks",
974
+ "title": "Platform Detection — isNative vs window.NativeBridge",
975
+ "content": "Three ways to detect native context. (1) Hook-level (preferred in React components): every hook exposes isNative boolean — e.g. const { isNative, execute } = useFilePicker(). (2) From WebBridge.init() return value: const result = WebBridge.init(); const isNative = result.platform !== 'web'. result.platform is 'android' | 'ios' | 'web' — set by NativeBridge environment detection. Do NOT invent result.isNative — that property does not exist on the init return. (3) Raw global check (outside hooks and WebBridge): const isAndroid = !!window.NativeBridge; const isIOS = !!window.webkit?.messageHandlers?.NativeBridge; const isNative = isAndroid || isIOS. Do NOT use navigator.userAgent — unreliable inside WebView.",
976
+ "layer": "Runtime",
977
+ "source": "static",
978
+ "tags": [
979
+ "native-hooks",
980
+ "platform-detection",
981
+ "NativeBridge",
982
+ "isNative"
983
+ ]
984
+ },
985
+ {
986
+ "section": "native_hooks",
987
+ "title": "Imperative APIs (non-hook)",
988
+ "content": "Three promise-based imperative functions for use outside React components: requestCameraPermission() => Promise<string>, requestNotificationPermission() => Promise<string>, requestHapticFeedback(type) => Promise<string>. Both throw if window.WebBridge is not initialized.",
989
+ "layer": "Runtime",
990
+ "source": "static",
991
+ "tags": [
992
+ "native-hooks",
993
+ "imperative",
994
+ "requestCameraPermission",
995
+ "requestNotificationPermission",
996
+ "requestHapticFeedback"
997
+ ]
998
+ },
999
+ {
1000
+ "section": "config_structure",
1001
+ "title": "moduleAliases — Required Path Aliases in package.json",
1002
+ "content": "package.json must have a _moduleAliases key (used by module-alias package) with these 6 required entries (enforced by validator.js — missing any = server crash): @api -> api/ (WARNING: common bug — @api accidentally set to src/static/css in legacy repos, causes all API imports to break silently), @containers -> src/js/containers, @server -> server, @config -> config, @css -> src/static/css, @routes -> src/js/routes/. Additional aliases (@store, @utils, @components, etc.) are optional but recommended. Dollar-prefix aliases (, , , , ) are server-side only — do not import in client code. Restriction: alias names containing catalyst are reserved. Migration check: always verify @api points to the actual API module, not a static asset path.",
1003
+ "layer": "Config",
1004
+ "source": "extracted:src/scripts/validator.js",
1005
+ "tags": [
1006
+ "module-aliases",
1007
+ "package-json",
1008
+ "required",
1009
+ "config"
1010
+ ]
1011
+ },
1012
+ {
1013
+ "section": "webview_config",
1014
+ "title": "appInfo Key — Build Identifier String",
1015
+ "content": "WEBVIEW_CONFIG.appInfo is a REQUIRED string (despite being named optional in older docs). Missing appInfo = iOS build breaks at compile time. Format convention: {platform}-{date}-v{version}, e.g. android-5Feb2026-v2.1.0. Place at WEBVIEW_CONFIG top level (not inside android/ios blocks). Both android and ios builds read this field.",
1016
+ "layer": "Config",
1017
+ "source": "extracted:config/config.json+changelog",
1018
+ "tags": [
1019
+ "config",
1020
+ "appInfo",
1021
+ "required",
1022
+ "ios",
1023
+ "build"
1024
+ ]
1025
+ },
1026
+ {
1027
+ "section": "build_system",
1028
+ "title": "Offline Fallback — public/offline.html",
1029
+ "content": "Added in canary.19. catalyst-core auto-packages public/offline.html into both Android and iOS native bundles. When the device loses connectivity inside the WebView, the native shell shows offline.html automatically, with a retry mechanism when connectivity is restored. To enable: create public/offline.html at project root. Minimum content: <!DOCTYPE html><html><body><h1>No internet connection</h1><p>Please check your network and try again.</p></body></html>. Missing the file: native build proceeds but shows a blank screen on connectivity loss (no error). File is served from the native bundle — not from the web server — so it works fully offline.",
1030
+ "layer": "Native",
1031
+ "source": "extracted:changelog",
1032
+ "tags": [
1033
+ "offline",
1034
+ "fallback",
1035
+ "native",
1036
+ "canary.19",
1037
+ "public"
1038
+ ]
1039
+ },
1040
+ {
1041
+ "section": "framework_identity",
1042
+ "title": "Mono-Repository Support (canary.2)",
1043
+ "content": "Added in canary.2. catalyst-core supports mono-repo project structures where the catalyst app lives as a sub-package (e.g. packages/mweb/) rather than at the repo root. setup.js and MCP findCatalystRoot() both support this: they walk up from __dirname to find the nearest package.json with catalyst-core in dependencies, not assuming project root = cwd. For mono-repos: run 'node .catalyst/mcp/setup.js' from inside the sub-package directory (e.g. cd packages/mweb && node .catalyst/mcp/setup.js), NOT from the mono-repo root. The MCP will anchor to that sub-package as its project root.",
1044
+ "layer": "Framework",
1045
+ "source": "extracted:changelog",
1046
+ "tags": [
1047
+ "mono-repo",
1048
+ "monorepo",
1049
+ "setup",
1050
+ "canary.2",
1051
+ "project-structure"
1052
+ ]
1053
+ },
1054
+ {
1055
+ "section": "native_hooks",
1056
+ "title": "WebBridge imperative methods — source reference",
1057
+ "content": "WebBridge.init() returns { bridge, platform, getDeviceInfo } — not a WebBridge instance directly. bridge = the WebBridge instance (also stored as window.WebBridge). platform = 'android' | 'ios' | 'web'. getDeviceInfo = bound method from bridge, callable directly from the return value. Two equivalent ways to call getDeviceInfo: (a) const result = WebBridge.init(); result.getDeviceInfo().then(...) (b) WebBridge.init(); window.WebBridge.getDeviceInfo().then(...) — window.WebBridge is the bridge instance set during init. Other imperative methods (openCamera, requestHapticFeedback, register, unregister, callback) are on the bridge instance only — access via window.WebBridge.openCamera() or result.bridge.openCamera(). Full source: node_modules/catalyst-core/src/native/bridge/WebBridge.js",
1058
+ "layer": "Runtime",
1059
+ "source": "extracted:WebBridge.js",
1060
+ "tags": [
1061
+ "WebBridge",
1062
+ "getDeviceInfo",
1063
+ "openCamera",
1064
+ "imperative",
1065
+ "native-bridge",
1066
+ "source-reference",
1067
+ "node_modules/catalyst-core/src/native/bridge/WebBridge.js"
1068
+ ]
1069
+ },
1070
+ {
1071
+ "section": "native_hooks",
1072
+ "title": "Native hooks source — hooks.js and standardizedHooks.js",
1073
+ "content": "All React hooks for native capabilities (useCamera, useFilePicker, useIntent, useGoogleSignIn, useHapticFeedback, useNotification, useNotificationPermission, useCameraPermission, useNetworkStatus, useSafeArea) are defined in node_modules/catalyst-core/src/native/bridge/hooks.js. Standardized hook wrappers (new interface with execute/state pattern) are in node_modules/catalyst-core/src/native/bridge/standardizedHooks.js. For any hook signature, input params, or return shape — read these files directly.",
1074
+ "layer": "Runtime",
1075
+ "source": "extracted:hooks.js",
1076
+ "tags": [
1077
+ "useCamera",
1078
+ "useFilePicker",
1079
+ "useIntent",
1080
+ "useGoogleSignIn",
1081
+ "useHapticFeedback",
1082
+ "useNotification",
1083
+ "useNetworkStatus",
1084
+ "useSafeArea",
1085
+ "native-hooks",
1086
+ "source-reference",
1087
+ "node_modules/catalyst-core/src/native/bridge/hooks.js"
1088
+ ]
1089
+ },
1090
+ {
1091
+ "section": "native_hooks",
1092
+ "title": "NativeBridge utility — source reference",
1093
+ "content": "Low-level bridge plumbing lives in node_modules/catalyst-core/src/native/bridge/utils/NativeBridge.js. Handles message passing between JS and native layer. WebBridge.js wraps this. Read this file for understanding how postMessage, register/unregister, and native event handling work under the hood.",
1094
+ "layer": "Runtime",
1095
+ "source": "extracted:NativeBridge.js",
1096
+ "tags": [
1097
+ "NativeBridge",
1098
+ "postMessage",
1099
+ "register",
1100
+ "unregister",
1101
+ "native-bridge",
1102
+ "source-reference",
1103
+ "node_modules/catalyst-core/src/native/bridge/utils/NativeBridge.js"
1104
+ ]
1105
+ },
1106
+ {
1107
+ "section": "server_structure",
1108
+ "title": "SSR renderer — source reference",
1109
+ "content": "rendertopipeable rendertopipeablestream: The full SSR render pipeline is in node_modules/catalyst-core/src/server/renderer/handler.js. This is the most important server file — read it for renderToPipeableStream usage, route matching, serverSideFunction calls, data fetching flow, and CompleteDocument assembly. Supporting files: src/server/renderer/document/ (Head.js, Body.js, index.js) for HTML document structure, src/server/renderer/extract.js for asset extraction.",
1110
+ "layer": "Server",
1111
+ "source": "extracted:handler.js",
1112
+ "tags": [
1113
+ "renderer",
1114
+ "handler",
1115
+ "SSR",
1116
+ "renderToPipeableStream",
1117
+ "serverSideFunction",
1118
+ "source-reference",
1119
+ "node_modules/catalyst-core/src/server/renderer/handler.js",
1120
+ "rendertopipeable",
1121
+ "rendertopipeablestream",
1122
+ "pipeable"
1123
+ ]
1124
+ },
1125
+ {
1126
+ "section": "server_structure",
1127
+ "title": "renderToPipeableStream in catalyst — how and why",
1128
+ "content": "rendertopipeable rendertopipeablestream: Catalyst uses React 18 renderToPipeableStream (not renderToString) for main SSR. This enables streaming HTML: the shell (head + above-fold) is flushed immediately via onShellReady → pipe(res), while deferred content streams as Suspense boundaries resolve. Flow: (1) onShellReady — sets content-type header, calls pipe(res) to start streaming. (2) onAllReady — writes firstFoldCss + firstFoldJS scripts, calls res.end(), cleans up globalThis. (3) onError — logs error, calls user-defined onRenderError callback, rejects promise. renderToString is still used internally for route matching / asset extraction (ChunkExtractor pass) but NOT for the final HTML response. Client hydration must use hydrateRoot (not createRoot) to attach to server-rendered HTML.",
1129
+ "layer": "Server",
1130
+ "source": "extracted:handler.js",
1131
+ "tags": [
1132
+ "renderToPipeableStream",
1133
+ "SSR",
1134
+ "streaming",
1135
+ "onShellReady",
1136
+ "onAllReady",
1137
+ "onError",
1138
+ "hydrateRoot",
1139
+ "pipe",
1140
+ "server-rendering",
1141
+ "rendertopipeable",
1142
+ "rendertopipeablestream",
1143
+ "pipeable",
1144
+ "streaming-ssr"
1145
+ ]
1146
+ },
1147
+ {
1148
+ "section": "server_structure",
1149
+ "title": "SSR pipeline — full request flow",
1150
+ "content": "Full SSR request lifecycle: (1) Express receives request → expressServer.js routes to renderer handler. (2) handler.js: matches routes via getMatchRoutes (uses react-router matchPath). (3) Calls serverSideFunction on matched route component for data fetching. (4) Assembles CompleteDocument (Head + Body with fetched data injected). (5) renderToPipeableStream streams HTML. onShellReady pipes shell immediately. onAllReady appends CSS/JS chunks. (6) Client receives streamed HTML, hydrateRoot attaches React. Key: serverSideFunction runs on server per-request — this is where route-level data fetching happens, result is passed as intialData (note: typo in source) to ServerRouter.",
1151
+ "layer": "Server",
1152
+ "source": "extracted:handler.js",
1153
+ "tags": [
1154
+ "SSR",
1155
+ "server-side-function",
1156
+ "serverSideFunction",
1157
+ "request-flow",
1158
+ "pipeline",
1159
+ "data-fetching",
1160
+ "hydration",
1161
+ "CompleteDocument"
1162
+ ]
1163
+ },
1164
+ {
1165
+ "section": "bridge_architecture",
1166
+ "title": "getDeviceInfo — what it does and when to use it",
1167
+ "content": "getDeviceInfo() is an imperative WebBridge method (not a React hook). Returns a Promise resolving to device metadata: model, manufacturer, platform, screenWidth, screenHeight, screenDensity, appInfo. \n\nWebBridge.init() return shape: { bridge: WebBridgeInstance, platform: 'android'|'ios'|'web', getDeviceInfo: boundFn }. bridge is also stored as window.WebBridge. \n\nCorrect usage pattern: const result = WebBridge.init(); const isNative = result.platform !== 'web'; result.getDeviceInfo().then(info => console.log(info)).catch(err => console.error(err)); \n\nDo NOT use result.isNative — that property does not exist. Do NOT check window.NativeBridge to gate the call — getDeviceInfo() handles web gracefully: on web it resolves immediately with browser info (navigator.userAgent, screen dimensions) instead of throwing. \n\nOn native: registers ON_DEVICE_INFO_SUCCESS / ON_DEVICE_INFO_ERROR callbacks, calls nativeBridge.device.getDeviceInfo(), resolves when native responds. \n\nAlternative call: window.WebBridge.getDeviceInfo() — works if init() was already called earlier (window.WebBridge is set by init).",
1168
+ "layer": "Runtime",
1169
+ "source": "extracted:WebBridge.js",
1170
+ "tags": [
1171
+ "getDeviceInfo",
1172
+ "WebBridge",
1173
+ "device-info",
1174
+ "imperative",
1175
+ "native-only",
1176
+ "isNative"
1177
+ ]
1178
+ },
1179
+ {
1180
+ "section": "config_structure",
1181
+ "title": "APPLICATION_UID — What it is and where to get it",
1182
+ "content": "APPLICATION_UID is a UUID string in config/config.json that uniquely identifies the app to the catalyst runtime. Required field — missing or empty = catalyst server startup error. Format: standard UUID v4 e.g. 9ba95d50-6396-42fb-956f-2d1ce98be8cb. How to generate: node -e \"const {randomUUID}=require(crypto); console.log(randomUUID())\" or use any UUID generator. Each app (mweb, dweb, pfizer etc.) must have a unique APPLICATION_UID — do not copy between projects. Also add APPLICATION_ID (integer, e.g. 1) alongside it.",
1183
+ "layer": "Config",
1184
+ "source": "static",
1185
+ "tags": [
1186
+ "config",
1187
+ "required",
1188
+ "APPLICATION_UID",
1189
+ "startup"
1190
+ ]
1191
+ },
1192
+ {
1193
+ "section": "config_structure",
1194
+ "title": "Migration Phase 0 — Bare Minimum to Boot App (No Bridge)",
1195
+ "content": "Minimum steps to get a catalyst webview app running without any native bridge/hooks: (1) catalyst-core in package.json dependencies. (2) All native scripts in package.json scripts: buildApp:android, buildApp:ios, setupEmulator:android, setupEmulator:ios, devBuild, devServe. (3) config/config.json WEBVIEW_CONFIG with LOCAL_IP (not localhost), port, APPLICATION_UID, android block (appName, packageName, buildType, sdkPath, emulatorName), ios block (appName, appBundleId, simulatorName, buildType). (4) _moduleAliases correct in package.json — especially @api pointing to the right path. (5) accessControl.allowedUrls includes your API domains. App will load as a webview at this point — bridge features (camera, notifications, device info) will not work until Phase 2 (WebBridge init). This is the correct first milestone for any repo migration.",
1196
+ "layer": "Config",
1197
+ "source": "static",
1198
+ "tags": [
1199
+ "migration",
1200
+ "bare-minimum",
1201
+ "config",
1202
+ "getting-started",
1203
+ "phase-0"
1204
+ ]
1205
+ },
1206
+ {
1207
+ "section": "bridge_architecture",
1208
+ "title": "Migrating from custom webviewkit bridge to catalyst hooks",
1209
+ "content": "Legacy apps (e.g. mweb) use a custom webviewkit bridge: window.AppCallbacks (Android), window.webkit.messageHandlers (iOS). Catalyst replaces this entirely. Migration phases: Phase 1 (bare minimum — app boots, no bridge needed). Phase 2 (WebBridge init): add WebBridge.init() to client entry file (src/client.js or src/app.js) — this registers the NativeBridge globally, replaces window.AppCallbacks pattern. Phase 3 (hook migration, file by file): replace webviewkit.isAndroid()/isIOS() with const { isNative } = useNetworkStatus() in React components, or window.NativeBridge presence check in imperative code. Replace webviewkit.call(methodName, info) with the relevant catalyst hook (useCamera, useIntent, useFilePicker etc.). Do NOT migrate all files at once — pick one component, validate it works natively, then proceed. webviewkit alias in package.json (webviewkit: src/js/webviewkit) can be kept during migration and removed once all usages replaced.",
1210
+ "layer": "Bridge",
1211
+ "source": "static",
1212
+ "tags": [
1213
+ "migration",
1214
+ "webviewkit",
1215
+ "bridge",
1216
+ "WebBridge",
1217
+ "hooks",
1218
+ "isNative"
1219
+ ]
1220
+ },
1221
+ {
1222
+ "title": "App Icon File Placement (No Config Required)",
1223
+ "content": "App icons are FILESYSTEM ONLY — do NOT add any appIcons key to config.json or WEBVIEW_CONFIG. The build auto-discovers icons by hardcoded path and filename pattern. No configuration needed. \n\nANDROID — place files at public/android/appIcons/ with naming icon-{density}.{ext}: icon-mdpi.png, icon-hdpi.png, icon-xhdpi.png, icon-xxhdpi.png, icon-xxxhdpi.png. Extensions: png, jpg, jpeg. Build copies each to androidProject/app/src/main/res/mipmap-{density}/icon.{ext} and sets android:icon='@mipmap/icon' in AndroidManifest. If no custom icons found → falls back to bundled catalyst icon (xxxhdpi only). \n\nIOS — place files at public/ios/appIcons/ with naming icon-{WxH}-{scale}.{ext}: icon-20x20-2x.png, icon-20x20-3x.png, icon-29x29-2x.png, icon-29x29-3x.png, icon-40x40-2x.png, icon-40x40-3x.png, icon-60x60-2x.png, icon-60x60-3x.png, icon-1024x1024-1x.png. Partial sets are fine — only provided sizes get updated in Assets.xcassets/AppIcon.appiconset/Contents.json. \n\nSPLASH SCREEN ASSETS (also filesystem, no config path field): Android → public/android/splashscreen.{png|jpg|jpeg|gif|bmp|webp}. iOS → public/ios/splashscreen.{png|jpg|jpeg}. WEBVIEW_CONFIG.splashScreen controls appearance only (backgroundColor, duration, imageWidth, imageHeight, cornerRadius) — NOT the asset path. \n\nNOTIFICATION ICONS (both platforms, filesystem only): public/notification-icon.{ext} (small), public/notification-large.{ext} (large). Android → drawable/ic_notification. iOS → Assets.xcassets/NotificationIcon.imageset.",
1224
+ "tags": [
1225
+ "app-icon",
1226
+ "icon",
1227
+ "assets",
1228
+ "android",
1229
+ "ios",
1230
+ "mipmap",
1231
+ "appicons",
1232
+ "launcher",
1233
+ "native",
1234
+ "public",
1235
+ "filesystem",
1236
+ "no-config"
1237
+ ],
1238
+ "section": "native_assets",
1239
+ "layer": "Assets",
1240
+ "source": "static"
1241
+ },
1242
+ {
1243
+ "section": "observability",
1244
+ "title": "Sentry Setup in Catalyst",
1245
+ "content": "Catalyst provides built-in Sentry support via catalyst-core/sentry.\n\nSETUP: Create config/sentry.config.json in your project. Catalyst's loadEnvironmentVariables.js auto-reads it at startup and sets process.env.SENTRY_CONFIG. No manual env wiring needed.\n\nCONFIG SHAPE:\n{\n \"dsn\": \"https://<key>@sentry.example.com/<id>\",\n \"tracesSampleRate\": 1.0,\n \"clientOptions\": { \"environment\": \"production\", ... },\n \"serverOptions\": { \"environment\": \"production\", \"profilesSampleRate\": 1.0, ... }\n}\n\nUSAGE: Import and call init() yourself — Catalyst does NOT auto-initialize Sentry.\nimport { init, captureException, captureMessage, addBreadcrumb } from 'catalyst-core/sentry'\n\n// Call early in client/index.js or server/index.js\ninit()\n\nSERVER vs CLIENT: init() auto-detects environment. Server-side loads @sentry/node, client-side loads @sentry/react. Same call works in both.\n\nPEER DEPS: Install @sentry/node and @sentry/react in your project — not bundled by catalyst-core.\n\nKILL SWITCH PATTERN (common in projects): Guard init() with an env flag like ENABLE_SENTRY=true so Sentry can be disabled without config changes.",
1246
+ "layer": "Observability",
1247
+ "source": "static",
1248
+ "tags": [
1249
+ "sentry",
1250
+ "error-tracking",
1251
+ "observability",
1252
+ "monitoring",
1253
+ "setup",
1254
+ "config",
1255
+ "init",
1256
+ "SENTRY_CONFIG",
1257
+ "integration"
1258
+ ]
1259
+ },
1260
+ {
1261
+ "section": "observability",
1262
+ "title": "OpenTelemetry Setup in Catalyst",
1263
+ "content": "Catalyst provides built-in OpenTelemetry support via catalyst-core/otel. Server-side only.\n\nUSAGE: Call Otel.init() inside preServerInit() in server/index.js. Guard with an ENABLE flag and skip in development.\n\nimport Otel from 'catalyst-core/otel'\n\nexport const preServerInit = () => {\n const otelConfig = JSON.parse(process.env.OTEL)\n if (otelConfig.ENABLE && process.env.NODE_ENV !== 'development') {\n Otel.init({\n serviceName: 'my-catalyst-app',\n serviceVersion: process.env.RELEASE_TAG,\n environment: process.env.ENV_NAME,\n traceUrl: otelConfig.ENDPOINT,\n instrumentations: [ new HttpInstrumentation({ ... }) ],\n })\n }\n}\n\nCONFIG SHAPE (all optional, defaults shown):\n- serviceName: 'catalyst-server'\n- serviceVersion: '1.0.0'\n- environment: 'development'\n- traceUrl: 'http://localhost:4318/v1/traces'\n- metricUrl: 'http://localhost:4318/v1/metrics'\n- traceHeaders: {}\n- metricHeaders: {}\n- exportIntervalMillis: 10000\n- exportTimeoutMillis: 10000\n- instrumentations: [getNodeAutoInstrumentations()] by default\n\nAUTO METRICS: init() automatically collects process_cpu_usage_percent, process_memory_usage_bytes, process_memory_heap_used_bytes, process_memory_heap_total_bytes — no extra config needed.\n\nGRACEFUL SHUTDOWN: SIGTERM and SIGINT handlers registered automatically — sdk.shutdown() is called on process exit.\n\nRETURN VALUE: { sdk, meter } — use meter (OTel Meter instance) to create your own custom metrics via meter.createObservableGauge(), createCounter(), etc.\n\nPEER DEPS: Install @opentelemetry/* packages yourself — not bundled by catalyst-core.",
1264
+ "layer": "Observability",
1265
+ "source": "static",
1266
+ "tags": [
1267
+ "otel",
1268
+ "opentelemetry",
1269
+ "observability",
1270
+ "tracing",
1271
+ "metrics",
1272
+ "monitoring",
1273
+ "setup",
1274
+ "server",
1275
+ "preServerInit",
1276
+ "OTLP",
1277
+ "integration"
1278
+ ]
1279
+ },
1280
+ {
1281
+ "section": "observability",
1282
+ "title": "Sentry ErrorBoundary",
1283
+ "content": "catalyst-core/sentry exports an ErrorBoundary React component for catching render errors.\n\nUSAGE:\nimport { ErrorBoundary } from 'catalyst-core/sentry'\n\n<ErrorBoundary fallback={<MyErrorUI />}>\n <App />\n</ErrorBoundary>\n\nBEHAVIOR: On error, calls captureException with the error and componentStack. Renders fallback prop if provided, otherwise renders <h1>Something went wrong.</h1>.\n\nNo fallback prop needed — it has a default. Wrapping the top-level app or critical subtrees is the typical usage.",
1284
+ "layer": "Observability",
1285
+ "source": "static",
1286
+ "tags": [
1287
+ "sentry",
1288
+ "error-boundary",
1289
+ "ErrorBoundary",
1290
+ "react",
1291
+ "error-handling",
1292
+ "observability"
1293
+ ]
1294
+ },
1295
+ {
1296
+ "section": "build_system",
1297
+ "title": "Webpack Customization via webpackConfig.js",
1298
+ "content": "Every catalyst project has a webpackConfig.js at the project root. This is the ONLY supported way to customize webpack — do not modify catalyst-core internals.\n\nEXPORTED KEYS:\n- developmentPlugins[]: merged into dev client webpack (webpack-dev-server run)\n- ssrPlugins[]: merged into both dev SSR watcher and production SSR build\n- clientPlugins[]: merged into production client build\n- splitChunksConfig: object — replaces the default commonVendor+utilityVendor split chunk strategy entirely\n- transpileModules[]: array of package names to whitelist from nodeExternals in SSR builds (use for ESM-only deps that need transpilation)\n\nDEFAULT (from template): all arrays empty, no splitChunksConfig — uses catalyst-core defaults.\n\nREAL USAGE PATTERNS:\nmweb clientPlugins: HTMLWebpackPlugin (offline.html), InjectManifest/Workbox (service worker), RoutesManifestPlugin (routes manifest from src/js/routes/index.js), UploadAssetsPlugin (S3 upload of assets + build output post-build).\nmweb splitChunksConfig: custom commonVendor test regex, minChunks:2 on utilityVendor, minRemainingSize:20KB.\npsp-pfizer clientPlugins: dual CompressionPlugin (gzip + brotli for JS/CSS/HTML/SVG). splitChunksConfig only applied when NODE_ENV=production (conditional spread pattern).\n\nCSS MODULE RULES (base.babel.js, not overridable):\n- .scss under src/static/css/base and node_modules → global (no css-loader modules)\n- all other .scss → CSS Modules, localIdentName from catalystConfig.cssModulesIdentifierDev/Prod\n- SCSS globals injected via sass-loader additionalData: $font_url, $url_for (from scssParams.js)\n\nBASE CONFIG (src/webpack/base.babel.js) handles: JS/TS via babel-loader, SCSS, CSS, images (url-loader+img-loader), SVG (@svgr/webpack), fonts (url-loader+file-loader), HTML (html-loader).\nDefinePlugin exposes CLIENT_ENV_VARIABLES from config/config.json plus BUILD_OUTPUT_PATH, PUBLIC_STATIC_ASSET_PATH, PUBLIC_STATIC_ASSET_URL, SENTRY_CONFIG to client bundle.",
1299
+ "layer": "Build",
1300
+ "source": "static",
1301
+ "tags": [
1302
+ "webpack",
1303
+ "webpackConfig",
1304
+ "build",
1305
+ "customization",
1306
+ "plugins",
1307
+ "splitChunks",
1308
+ "clientPlugins",
1309
+ "ssrPlugins",
1310
+ "developmentPlugins",
1311
+ "transpileModules",
1312
+ "css-modules",
1313
+ "scss",
1314
+ "service-worker",
1315
+ "S3",
1316
+ "compression"
1317
+ ]
1318
+ },
1319
+ {
1320
+ "section": "build_system",
1321
+ "title": "React Compiler (opt-in via webpackConfig.js)",
1322
+ "content": "React Compiler is opt-in. Disabled by default — no babel-plugin-react-compiler is added unless explicitly enabled.\n\nENABLE: Add reactCompiler key to webpackConfig.js:\n reactCompiler: true // uses default options: { target: '18' }\n reactCompiler: { target: '18' } // custom options object\n\nHOW IT WORKS: babel.config.client.js and babel.config.ssr.js both read customWebpackConfig.reactCompiler. If truthy, babel-plugin-react-compiler is added as a babel plugin to BOTH client and SSR builds. If the value is an object, it's passed as plugin options directly; otherwise defaults to { target: '18' }.\n\nPEER DEPS (already in catalyst-core, no install needed in app):\n- babel-plugin-react-compiler@^19.0.0-beta\n- react-compiler-runtime@^19.0.0-beta\n- eslint-plugin-react-compiler@^19.0.0-beta\n\nCURRENT USAGE: Neither mweb nor psp-pfizer enable React Compiler — both ship without the reactCompiler key.",
1323
+ "layer": "Build",
1324
+ "source": "static",
1325
+ "tags": [
1326
+ "react-compiler",
1327
+ "babel",
1328
+ "webpackConfig",
1329
+ "opt-in",
1330
+ "build",
1331
+ "babel-plugin-react-compiler",
1332
+ "performance",
1333
+ "react"
1334
+ ]
1335
+ },
1336
+ {
1337
+ "section": "build_system",
1338
+ "title": "CLI Commands and Package Scripts",
1339
+ "content": "The catalyst binary (bin/catalyst.js) is the sole entrypoint for all framework commands. Projects call it via npm scripts.\n\nWEB COMMANDS:\n- catalyst start -> dev mode: webpack-dev-server (HMR) + node server\n- catalyst build -> production webpack build (client + SSR)\n- catalyst serve -> start node server (after build)\n- catalyst devBuild -> production webpack build but assets served from local node server (IS_DEV_COMMAND=true)\n- catalyst devServe -> same as devBuild but with webpack watch mode\n\nNATIVE COMMANDS:\n- catalyst buildApp -> builds iOS + Android sequentially\n- catalyst buildApp:ios -> iOS app build only\n- catalyst buildApp:android -> Android app build only\n- catalyst setupEmulator -> sets up iOS + Android emulators sequentially\n- catalyst setupEmulator:ios -> iOS emulator setup only\n- catalyst setupEmulator:android -> Android emulator setup only\n\nINTERNALS:\n- Web commands resolve to dist/scripts/<command>.js (build.js, start.js, serve.js, devBuild.js, devServe.js)\n- Native commands resolve to dist/native/ (buildAppIos.js, buildAppAndroid.js, setupEmulatorIos.js, androidSetup.js)\n\nTYPICAL PROJECT package.json PATTERN:\n \"start\": \"catalyst start\"\n \"build\": \"export NODE_OPTIONS=\\\"--max-old-space-size=6020\\\" && catalyst build\"\n \"serve\": \"catalyst serve\"\n \"buildApp\": \"catalyst buildApp\"\n\nNOTE: serve:prod / serve:stag in project scripts are pm2-runtime commands — not catalyst CLI commands. These are project-level and not part of the framework.",
1340
+ "layer": "Build",
1341
+ "source": "static",
1342
+ "tags": [
1343
+ "cli",
1344
+ "catalyst",
1345
+ "scripts",
1346
+ "build",
1347
+ "start",
1348
+ "serve",
1349
+ "devBuild",
1350
+ "devServe",
1351
+ "buildApp",
1352
+ "setupEmulator",
1353
+ "native",
1354
+ "commands",
1355
+ "bin"
1356
+ ]
1357
+ },
1358
+ {
1359
+ "section": "framework_identity",
1360
+ "title": "Project File Conventions and Structure",
1361
+ "content": "Standard catalyst project layout:\n\nproject-root/\n├── client/\n│ ├── index.js <- webpack entry: hydrates React app (hydrateRoot + loadableReady)\n│ └── styles.js <- global CSS imports\n├── server/\n│ ├── index.js <- exports preServerInit() — runs before express starts (use for OTel, DB init, Sentry)\n│ ├── server.js <- express server setup\n│ └── document.js <- HTML document shell (Head, Body)\n├── src/\n│ └── js/\n│ ├── pages/ <- route-level components, one folder per page\n│ ├── containers/ <- data-connected components (mweb uses these as page-level too)\n│ ├── components/ <- presentational/shared components\n│ ├── layouts/ <- layout wrappers used as nested route parents\n│ └── routes/ <- index.js exports routes array; utils.js for route helpers\n│ └── static/\n│ ├── css/base/ <- global SCSS — NOT CSS-moduled (applied globally)\n│ └── css/resources/ <- SCSS variables/mixins auto-injected into all CSS modules via sass-resources-loader\n├── config/\n│ └── config.json <- app config: CLIENT_ENV_VARIABLES, WEBVIEW_CONFIG, ANALYZE_BUNDLE, etc.\n├── api.js <- API layer (aliased as @api)\n├── webpackConfig.js <- webpack customization hook\n└── package.json <- _moduleAliases defines path aliases for webpack + node\n\nMODULE ALIASES (defined in package.json._moduleAliases, resolved by webpack and registerAliases.js):\n @api -> api.js\n @containers -> src/js/containers\n @server -> server\n @config -> config\n @css -> src/static/css\n @routes -> src/js/routes/\n\nKEY CONVENTIONS:\n- Pages use @loadable/component for code splitting. ssr:true enables SSR for that page; ssr:false serves fallback only.\n- client/index.js always uses hydrateRoot (not ReactDOM.render) wrapped in loadableReady.\n- server/index.js exports preServerInit() — the correct place for server-side init (OTel, Sentry, DB connections).\n- Routes in src/js/routes/index.js use nested structure: layout component as parent, pages as children.\n- Build output: build/public/ (client JS/CSS chunks), build/renderer/ (SSR handler.js + loadable-stats.json).\n- CSS co-located with components as .scss files (CSS Modules). Global styles go in src/static/css/base/.",
1362
+ "layer": "Framework",
1363
+ "source": "static",
1364
+ "tags": [
1365
+ "project-structure",
1366
+ "file-conventions",
1367
+ "layout",
1368
+ "routes",
1369
+ "pages",
1370
+ "containers",
1371
+ "components",
1372
+ "client",
1373
+ "server",
1374
+ "preServerInit",
1375
+ "moduleAliases",
1376
+ "loadable",
1377
+ "css-modules",
1378
+ "config"
1379
+ ]
1380
+ },
1381
+ {
1382
+ "section": "native_hooks",
1383
+ "title": "Native Hooks — Full List and Standard Interface",
1384
+ "content": "Catalyst provides React hooks for native device APIs via two files:\n- src/native/bridge/hooks.js — legacy hooks (direct WebBridge usage)\n- src/native/bridge/standardizedHooks.js — new superhooks built on useBaseHook (preferred)\n\nSTANDARD HOOK INTERFACE (all standardized hooks return):\n{ data, loading, progress, error, isWeb, isNative, execute, clear, clearError }\n\nprogress shape: { state, percentage, message, phase, transport, bytesLoaded, bytesTotal }\n\nENVIRONMENT DETECTION (useBaseHook):\nisNative = window.WebBridge && (window.NativeBridge || window.webkit?.messageHandlers?.NativeBridge)\nisWeb = !isNative\n\nHOOKS IN standardizedHooks.js (prefer these):\n- useCamera(options) — camera capture + permission management. execute(operation) where operation = takePhoto | requestPermission | checkPermission. Also exposes takePhoto(), requestPermission(), checkPermission() as aliases.\n- useFilePicker(options) — native file picker. execute(mimeType) to open. getFileObject() converts picked file to JS File object for FormData/fetch upload (lazy, cached). canCreateFileObject flag indicates if conversion is possible.\n- useIntent(options) — open file with external app. execute(url, mimeType).\n\nHOOKS IN hooks.js (legacy, check github_content for full current list):\n- useGoogleSignIn — Google sign-in via native\n- useCamera — legacy camera (prefer standardizedHooks version)\n- useIntent — legacy intent (prefer standardizedHooks version)\n- useFilePicker — legacy file picker (prefer standardizedHooks version)\n- useCameraPermission / requestCameraPermission\n- useNotificationPermission / requestNotificationPermission\n- useHapticFeedback / requestHapticFeedback(feedbackType)\n- useNotification — push notification handling\n- useNetworkStatus — network connectivity state\n- useDataProtection — data protection/encryption\n- useSafeArea — safe area insets\n\nNOTE: hooks.js is actively developed. Always check github_content for the latest hook list — new hooks may have been added since this KB entry was written.",
1385
+ "layer": "Bridge",
1386
+ "source": "static",
1387
+ "tags": [
1388
+ "native-hooks",
1389
+ "hooks",
1390
+ "useCamera",
1391
+ "useFilePicker",
1392
+ "useIntent",
1393
+ "useNotification",
1394
+ "useNetworkStatus",
1395
+ "useSafeArea",
1396
+ "useHapticFeedback",
1397
+ "useDataProtection",
1398
+ "useCameraPermission",
1399
+ "useNotificationPermission",
1400
+ "useGoogleSignIn",
1401
+ "standardizedHooks",
1402
+ "useBaseHook",
1403
+ "native",
1404
+ "bridge"
1405
+ ],
1406
+ "github_files": [
1407
+ "src/native/bridge/hooks.js",
1408
+ "src/native/bridge/standardizedHooks.js",
1409
+ "src/native/bridge/useBaseHook.js"
1410
+ ]
1411
+ },
1412
+ {
1413
+ "section": "seo_metadata",
1414
+ "title": "Server-Side Meta Tags — setMetaData()",
1415
+ "content": "Each page component can define a static `setMetaData` function that returns an array of JSX meta elements. These are collected server-side and injected into the HTML <head> before the page is streamed to the client.\n\nHOW IT WORKS:\n1. Route components define `Component.setMetaData = (fetcherData) => [...]`\n2. handler.js calls `getMetaData(allMatches, fetcherData)` from @tata1mg/router — collects setMetaData from all matched route components, passes fetcherData so tags can be dynamic.\n3. The resulting `allTags` array is passed to `renderMarkUp()` → `<Head metaTags={allTags} />`.\n4. `Head` renders them inside `<head>` before pageCss and preloadJSLinks.\n\nEXAMPLE (static title):\n```js\nHome.setMetaData = () => {\n return [<title key=\"title\">Home</title>]\n}\n```\n\nEXAMPLE (dynamic OG tags using fetcherData):\n```js\nBreedDetails.setMetaData = ({ params, data }) => [\n <title key=\"title\">{params.breed} — Dog Breeds</title>,\n <meta key=\"og:title\" property=\"og:title\" content={params.breed} />,\n <meta key=\"og:description\" property=\"og:description\" content={data?.message} />,\n]\n```\n\nNOTES:\n- Always add `key` prop to each tag (React list reconciliation).\n- fetcherData shape matches what `serverFetcher` returned for that route.\n- Tags from parent + child routes are merged — order follows route nesting.\n- `isBot` flag is passed to Head; use for bot-specific meta if needed.",
1416
+ "layer": "Component",
1417
+ "source": "static",
1418
+ "tags": [
1419
+ "seo",
1420
+ "meta",
1421
+ "metadata",
1422
+ "setMetaData",
1423
+ "title",
1424
+ "og",
1425
+ "open-graph",
1426
+ "head",
1427
+ "ssr",
1428
+ "server-side"
1429
+ ]
1430
+ },
1431
+ {
1432
+ "section": "seo_metadata",
1433
+ "title": "Client-Side Meta Updates — MetaTag + custom document",
1434
+ "content": "On the client side, meta tags are kept in sync across navigations via `<MetaTag />` from @tata1mg/router. For global head additions (fonts, preconnect, etc.), use the custom `server/document.js`.\n\nMETATAG COMPONENT (client SPA navigation):\n- Placed inside `RouterDataProvider` in `preparedRoutes` (src/js/routes/utils.js).\n- Automatically reads `setMetaData` from the newly matched route component and updates the DOM <head> on every client-side navigation.\n- No manual setup needed beyond adding `<MetaTag />` once in the route tree.\n\nCUSTOM DOCUMENT (server/document.js):\n- Optional file at `server/document.js` in the project root.\n- Receives all Head/Body props and lets you wrap or extend the default HTML shell.\n- Use for: global fonts, preconnect links, custom <html> attributes.\n\nEXAMPLE (server/document.js):\n```js\nimport React from 'react'\nimport { Head, Body } from 'catalyst-core'\n\nexport default function Document(props) {\n return (\n <html lang=\"en\">\n <Head {...props}>\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\" />\n <link href=\"https://fonts.googleapis.com/css2?family=Poppins\" rel=\"stylesheet\" />\n </Head>\n <Body {...props} />\n </html>\n )\n}\n```\n\nWhen `Head` receives `children`, it renders them first, then `metaTags`, then pageCss, then preloadJSLinks — giving you full control over ordering.\n\nNOTES:\n- `MetaTag` is client-only; SSR uses `setMetaData` → `getMetaData` pipeline.\n- If no `server/document.js` is present, handler.js uses the default Head+Body layout.",
1435
+ "layer": "Component",
1436
+ "source": "static",
1437
+ "tags": [
1438
+ "seo",
1439
+ "meta",
1440
+ "metadata",
1441
+ "MetaTag",
1442
+ "document",
1443
+ "custom-document",
1444
+ "head",
1445
+ "client-side",
1446
+ "fonts",
1447
+ "preconnect"
1448
+ ]
1449
+ }
1450
+ ]