@jmruthers/pace-core 0.6.2 → 0.6.4

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 (299) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/cursor-rules/00-pace-core-compliance.mdc +34 -2
  3. package/dist/{AuthService-BPvc3Ka0.d.ts → AuthService-Cb34EQs3.d.ts} +9 -1
  4. package/dist/{DataTable-TPTKCX4D.js → DataTable-E7YQZD7D.js} +9 -8
  5. package/dist/{PublicPageProvider-DC6kCaqf.d.ts → PublicPageProvider-DEMpysFR.d.ts} +45 -67
  6. package/dist/{UnifiedAuthProvider-CVcTjx-d.d.ts → UnifiedAuthProvider-CKvHP1MK.d.ts} +1 -8
  7. package/dist/{UnifiedAuthProvider-CH6Z342H.js → UnifiedAuthProvider-QPXO24B4.js} +5 -4
  8. package/dist/{api-MVVQZLJI.js → api-6LVZTHDS.js} +10 -10
  9. package/dist/{audit-B5P6FFIR.js → audit-V53FV5AG.js} +2 -2
  10. package/dist/chunk-36LVWXB2.js +227 -0
  11. package/dist/chunk-36LVWXB2.js.map +1 -0
  12. package/dist/{chunk-24UVZUZG.js → chunk-3LPHPB62.js} +129 -387
  13. package/dist/chunk-3LPHPB62.js.map +1 -0
  14. package/dist/{chunk-2UOI2FG5.js → chunk-5EC5MEWX.js} +4 -4
  15. package/dist/{chunk-3XC4CPTD.js → chunk-7JPAB3T5.js} +244 -5727
  16. package/dist/chunk-7JPAB3T5.js.map +1 -0
  17. package/dist/{chunk-6J4GEEJR.js → chunk-ATKZM7RX.js} +53 -27
  18. package/dist/chunk-ATKZM7RX.js.map +1 -0
  19. package/dist/{chunk-EHMR7VYL.js → chunk-AVMLPIM7.js} +443 -189
  20. package/dist/chunk-AVMLPIM7.js.map +1 -0
  21. package/dist/chunk-DGUM43GV.js +11 -0
  22. package/dist/{chunk-NECFR5MM.js → chunk-I6DAQMWX.js} +575 -647
  23. package/dist/chunk-I6DAQMWX.js.map +1 -0
  24. package/dist/{chunk-F2IMUDXZ.js → chunk-M7MPQISP.js} +2 -2
  25. package/dist/{chunk-XWQCNGTQ.js → chunk-NN6WWZ5U.js} +173 -79
  26. package/dist/chunk-NN6WWZ5U.js.map +1 -0
  27. package/dist/{chunk-MMZ7JXPU.js → chunk-OEWDTMG7.js} +13 -21
  28. package/dist/{chunk-MMZ7JXPU.js.map → chunk-OEWDTMG7.js.map} +1 -1
  29. package/dist/{chunk-SFZUDBL5.js → chunk-YKRAFF5K.js} +70 -56
  30. package/dist/chunk-YKRAFF5K.js.map +1 -0
  31. package/dist/components.d.ts +2 -2
  32. package/dist/components.js +12 -13
  33. package/dist/contextValidator-OOPCLPZW.js +9 -0
  34. package/dist/contextValidator-OOPCLPZW.js.map +1 -0
  35. package/dist/eslint-rules/pace-core-compliance.cjs +106 -0
  36. package/dist/hooks.d.ts +2 -2
  37. package/dist/hooks.js +7 -6
  38. package/dist/hooks.js.map +1 -1
  39. package/dist/index.d.ts +7 -7
  40. package/dist/index.js +21 -16
  41. package/dist/index.js.map +1 -1
  42. package/dist/providers.d.ts +3 -3
  43. package/dist/providers.js +4 -3
  44. package/dist/rbac/index.d.ts +67 -27
  45. package/dist/rbac/index.js +15 -8
  46. package/dist/styles/index.js +1 -1
  47. package/dist/theming/runtime.js +1 -1
  48. package/dist/types.js +1 -1
  49. package/dist/{usePublicRouteParams-1oMokgLF.d.ts → usePublicRouteParams-i3qtoBgg.d.ts} +7 -16
  50. package/dist/utils.js +5 -7
  51. package/dist/utils.js.map +1 -1
  52. package/docs/api/README.md +14 -16
  53. package/docs/api/modules.md +3796 -2513
  54. package/docs/components/context-selector.md +126 -0
  55. package/docs/migration/RBAC_SCOPE_MIGRATION.md +385 -0
  56. package/docs/pace-mint-fix-auto-selection.md +218 -0
  57. package/docs/pace-mint-rbac-setup.md +391 -0
  58. package/docs/rbac/secure-client-protection.md +330 -0
  59. package/package.json +10 -5
  60. package/scripts/audit/core/checks/compliance.cjs +72 -0
  61. package/scripts/audit/core/checks/dependencies.cjs +568 -28
  62. package/scripts/audit/core/checks/documentation.cjs +68 -3
  63. package/scripts/audit/core/checks/environment.cjs +2 -14
  64. package/scripts/audit/core/checks/error-handling.cjs +47 -6
  65. package/src/components/ContextSelector/ContextSelector.tsx +384 -0
  66. package/src/components/ContextSelector/index.ts +3 -0
  67. package/src/components/DataTable/components/RowComponent.tsx +19 -19
  68. package/src/components/DataTable/components/UnifiedTableBody.tsx +2 -2
  69. package/src/components/DataTable/hooks/useDataTablePermissions.ts +8 -6
  70. package/src/components/Dialog/Dialog.tsx +29 -1
  71. package/src/components/FileDisplay/FileDisplay.tsx +42 -10
  72. package/src/components/Header/Header.test.tsx +43 -73
  73. package/src/components/Header/Header.tsx +44 -45
  74. package/src/components/PaceAppLayout/PaceAppLayout.integration.test.tsx +10 -19
  75. package/src/components/PaceAppLayout/PaceAppLayout.performance.test.tsx +2 -2
  76. package/src/components/PaceAppLayout/PaceAppLayout.security.test.tsx +5 -5
  77. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +9 -9
  78. package/src/components/PaceAppLayout/PaceAppLayout.tsx +157 -36
  79. package/src/components/PaceAppLayout/README.md +14 -17
  80. package/src/components/PaceAppLayout/test-setup.tsx +2 -2
  81. package/src/components/index.ts +5 -5
  82. package/src/eslint-rules/pace-core-compliance.cjs +106 -0
  83. package/src/hooks/__tests__/useAppConfig.unit.test.ts +4 -98
  84. package/src/hooks/useAppConfig.ts +15 -30
  85. package/src/hooks/useFileDisplay.ts +77 -50
  86. package/src/index.ts +4 -5
  87. package/src/providers/services/AuthServiceProvider.tsx +17 -7
  88. package/src/providers/services/EventServiceProvider.tsx +33 -5
  89. package/src/providers/services/UnifiedAuthProvider.tsx +90 -134
  90. package/src/rbac/__tests__/adapters.comprehensive.test.tsx +1 -1
  91. package/src/rbac/adapters.tsx +2 -2
  92. package/src/rbac/api.test.ts +59 -51
  93. package/src/rbac/api.ts +178 -132
  94. package/src/rbac/components/PagePermissionGuard.tsx +38 -10
  95. package/src/rbac/hooks/__tests__/useSecureSupabase.test.ts +32 -21
  96. package/src/rbac/hooks/permissions/useAccessLevel.ts +1 -1
  97. package/src/rbac/hooks/permissions/useCan.ts +41 -11
  98. package/src/rbac/hooks/permissions/useHasAllPermissions.ts +1 -1
  99. package/src/rbac/hooks/permissions/useHasAnyPermission.ts +1 -1
  100. package/src/rbac/hooks/permissions/useMultiplePermissions.ts +1 -1
  101. package/src/rbac/hooks/useCan.test.ts +0 -9
  102. package/src/rbac/hooks/useRBAC.test.ts +1 -5
  103. package/src/rbac/hooks/useRBAC.ts +36 -37
  104. package/src/rbac/hooks/useResolvedScope.test.ts +120 -35
  105. package/src/rbac/hooks/useResolvedScope.ts +35 -40
  106. package/src/rbac/hooks/useSecureSupabase.ts +7 -7
  107. package/src/rbac/index.ts +7 -0
  108. package/src/rbac/secureClient.test.ts +22 -18
  109. package/src/rbac/secureClient.ts +103 -16
  110. package/src/rbac/security.ts +0 -17
  111. package/src/rbac/types.ts +1 -0
  112. package/src/rbac/utils/__tests__/contextValidator.test.ts +64 -86
  113. package/src/rbac/utils/clientSecurity.ts +93 -0
  114. package/src/rbac/utils/contextValidator.ts +77 -168
  115. package/src/services/AuthService.ts +39 -7
  116. package/src/services/EventService.ts +285 -56
  117. package/src/services/OrganisationService.ts +81 -14
  118. package/src/services/__tests__/EventService.test.ts +1 -2
  119. package/src/services/base/BaseService.ts +3 -0
  120. package/src/utils/dynamic/dynamicUtils.ts +7 -4
  121. package/dist/chunk-24UVZUZG.js.map +0 -1
  122. package/dist/chunk-3XC4CPTD.js.map +0 -1
  123. package/dist/chunk-6J4GEEJR.js.map +0 -1
  124. package/dist/chunk-7D4SUZUM.js +0 -38
  125. package/dist/chunk-EHMR7VYL.js.map +0 -1
  126. package/dist/chunk-NECFR5MM.js.map +0 -1
  127. package/dist/chunk-SFZUDBL5.js.map +0 -1
  128. package/dist/chunk-XWQCNGTQ.js.map +0 -1
  129. package/docs/api/classes/ColumnFactory.md +0 -243
  130. package/docs/api/classes/InvalidScopeError.md +0 -73
  131. package/docs/api/classes/Logger.md +0 -178
  132. package/docs/api/classes/MissingUserContextError.md +0 -66
  133. package/docs/api/classes/OrganisationContextRequiredError.md +0 -66
  134. package/docs/api/classes/PermissionDeniedError.md +0 -73
  135. package/docs/api/classes/RBACAuditManager.md +0 -297
  136. package/docs/api/classes/RBACCache.md +0 -322
  137. package/docs/api/classes/RBACEngine.md +0 -171
  138. package/docs/api/classes/RBACError.md +0 -76
  139. package/docs/api/classes/RBACNotInitializedError.md +0 -66
  140. package/docs/api/classes/SecureSupabaseClient.md +0 -163
  141. package/docs/api/classes/StorageUtils.md +0 -328
  142. package/docs/api/enums/FileCategory.md +0 -184
  143. package/docs/api/enums/LogLevel.md +0 -54
  144. package/docs/api/enums/RBACErrorCode.md +0 -228
  145. package/docs/api/enums/RPCFunction.md +0 -118
  146. package/docs/api/interfaces/AddressFieldProps.md +0 -241
  147. package/docs/api/interfaces/AddressFieldRef.md +0 -94
  148. package/docs/api/interfaces/AggregateConfig.md +0 -43
  149. package/docs/api/interfaces/AutocompleteOptions.md +0 -75
  150. package/docs/api/interfaces/AvatarProps.md +0 -128
  151. package/docs/api/interfaces/BadgeProps.md +0 -34
  152. package/docs/api/interfaces/ButtonProps.md +0 -56
  153. package/docs/api/interfaces/CalendarProps.md +0 -73
  154. package/docs/api/interfaces/CardProps.md +0 -69
  155. package/docs/api/interfaces/ColorPalette.md +0 -7
  156. package/docs/api/interfaces/ColorShade.md +0 -66
  157. package/docs/api/interfaces/ComplianceResult.md +0 -30
  158. package/docs/api/interfaces/DataAccessRecord.md +0 -96
  159. package/docs/api/interfaces/DataRecord.md +0 -11
  160. package/docs/api/interfaces/DataTableAction.md +0 -252
  161. package/docs/api/interfaces/DataTableColumn.md +0 -504
  162. package/docs/api/interfaces/DataTableProps.md +0 -625
  163. package/docs/api/interfaces/DataTableToolbarButton.md +0 -96
  164. package/docs/api/interfaces/DatabaseComplianceResult.md +0 -85
  165. package/docs/api/interfaces/DatabaseIssue.md +0 -41
  166. package/docs/api/interfaces/EmptyStateConfig.md +0 -61
  167. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +0 -235
  168. package/docs/api/interfaces/ErrorBoundaryProps.md +0 -147
  169. package/docs/api/interfaces/ErrorBoundaryProviderProps.md +0 -36
  170. package/docs/api/interfaces/ErrorBoundaryState.md +0 -75
  171. package/docs/api/interfaces/EventAppRoleData.md +0 -71
  172. package/docs/api/interfaces/ExportColumn.md +0 -90
  173. package/docs/api/interfaces/ExportOptions.md +0 -126
  174. package/docs/api/interfaces/FileDisplayProps.md +0 -249
  175. package/docs/api/interfaces/FileMetadata.md +0 -129
  176. package/docs/api/interfaces/FileReference.md +0 -118
  177. package/docs/api/interfaces/FileSizeLimits.md +0 -7
  178. package/docs/api/interfaces/FileUploadOptions.md +0 -139
  179. package/docs/api/interfaces/FileUploadProps.md +0 -296
  180. package/docs/api/interfaces/FooterProps.md +0 -107
  181. package/docs/api/interfaces/FormFieldProps.md +0 -166
  182. package/docs/api/interfaces/FormProps.md +0 -113
  183. package/docs/api/interfaces/GrantEventAppRoleParams.md +0 -122
  184. package/docs/api/interfaces/InactivityWarningModalProps.md +0 -115
  185. package/docs/api/interfaces/InputProps.md +0 -56
  186. package/docs/api/interfaces/LabelProps.md +0 -107
  187. package/docs/api/interfaces/LoggerConfig.md +0 -62
  188. package/docs/api/interfaces/LoginFormProps.md +0 -187
  189. package/docs/api/interfaces/NavigationAccessRecord.md +0 -107
  190. package/docs/api/interfaces/NavigationContextType.md +0 -164
  191. package/docs/api/interfaces/NavigationGuardProps.md +0 -139
  192. package/docs/api/interfaces/NavigationItem.md +0 -120
  193. package/docs/api/interfaces/NavigationMenuProps.md +0 -221
  194. package/docs/api/interfaces/NavigationProviderProps.md +0 -117
  195. package/docs/api/interfaces/Organisation.md +0 -140
  196. package/docs/api/interfaces/OrganisationContextType.md +0 -388
  197. package/docs/api/interfaces/OrganisationMembership.md +0 -140
  198. package/docs/api/interfaces/OrganisationProviderProps.md +0 -76
  199. package/docs/api/interfaces/OrganisationSecurityError.md +0 -62
  200. package/docs/api/interfaces/PaceAppLayoutProps.md +0 -409
  201. package/docs/api/interfaces/PaceLoginPageProps.md +0 -49
  202. package/docs/api/interfaces/PageAccessRecord.md +0 -85
  203. package/docs/api/interfaces/PagePermissionContextType.md +0 -140
  204. package/docs/api/interfaces/PagePermissionGuardProps.md +0 -153
  205. package/docs/api/interfaces/PagePermissionProviderProps.md +0 -119
  206. package/docs/api/interfaces/PaletteData.md +0 -41
  207. package/docs/api/interfaces/ParsedAddress.md +0 -120
  208. package/docs/api/interfaces/PermissionEnforcerProps.md +0 -153
  209. package/docs/api/interfaces/ProgressProps.md +0 -42
  210. package/docs/api/interfaces/ProtectedRouteProps.md +0 -78
  211. package/docs/api/interfaces/PublicPageFooterProps.md +0 -112
  212. package/docs/api/interfaces/PublicPageHeaderProps.md +0 -125
  213. package/docs/api/interfaces/PublicPageLayoutProps.md +0 -185
  214. package/docs/api/interfaces/QuickFix.md +0 -52
  215. package/docs/api/interfaces/RBACAccessValidateParams.md +0 -52
  216. package/docs/api/interfaces/RBACAccessValidateResult.md +0 -41
  217. package/docs/api/interfaces/RBACAuditLogParams.md +0 -85
  218. package/docs/api/interfaces/RBACAuditLogResult.md +0 -52
  219. package/docs/api/interfaces/RBACConfig.md +0 -133
  220. package/docs/api/interfaces/RBACContext.md +0 -52
  221. package/docs/api/interfaces/RBACLogger.md +0 -112
  222. package/docs/api/interfaces/RBACPageAccessCheckParams.md +0 -74
  223. package/docs/api/interfaces/RBACPerformanceMetrics.md +0 -138
  224. package/docs/api/interfaces/RBACPermissionCheckParams.md +0 -74
  225. package/docs/api/interfaces/RBACPermissionCheckResult.md +0 -52
  226. package/docs/api/interfaces/RBACPermissionsGetParams.md +0 -63
  227. package/docs/api/interfaces/RBACPermissionsGetResult.md +0 -63
  228. package/docs/api/interfaces/RBACResult.md +0 -58
  229. package/docs/api/interfaces/RBACRoleGrantParams.md +0 -63
  230. package/docs/api/interfaces/RBACRoleGrantResult.md +0 -52
  231. package/docs/api/interfaces/RBACRoleRevokeParams.md +0 -63
  232. package/docs/api/interfaces/RBACRoleRevokeResult.md +0 -52
  233. package/docs/api/interfaces/RBACRoleValidateParams.md +0 -52
  234. package/docs/api/interfaces/RBACRoleValidateResult.md +0 -63
  235. package/docs/api/interfaces/RBACRolesListParams.md +0 -52
  236. package/docs/api/interfaces/RBACRolesListResult.md +0 -74
  237. package/docs/api/interfaces/RBACSessionTrackParams.md +0 -74
  238. package/docs/api/interfaces/RBACSessionTrackResult.md +0 -52
  239. package/docs/api/interfaces/ResourcePermissions.md +0 -155
  240. package/docs/api/interfaces/RevokeEventAppRoleParams.md +0 -100
  241. package/docs/api/interfaces/RoleBasedRouterContextType.md +0 -151
  242. package/docs/api/interfaces/RoleBasedRouterProps.md +0 -156
  243. package/docs/api/interfaces/RoleManagementResult.md +0 -52
  244. package/docs/api/interfaces/RouteAccessRecord.md +0 -107
  245. package/docs/api/interfaces/RouteConfig.md +0 -134
  246. package/docs/api/interfaces/RuntimeComplianceResult.md +0 -55
  247. package/docs/api/interfaces/SecureDataContextType.md +0 -168
  248. package/docs/api/interfaces/SecureDataProviderProps.md +0 -132
  249. package/docs/api/interfaces/SessionRestorationLoaderProps.md +0 -34
  250. package/docs/api/interfaces/SetupIssue.md +0 -41
  251. package/docs/api/interfaces/StorageConfig.md +0 -41
  252. package/docs/api/interfaces/StorageFileInfo.md +0 -74
  253. package/docs/api/interfaces/StorageFileMetadata.md +0 -151
  254. package/docs/api/interfaces/StorageListOptions.md +0 -99
  255. package/docs/api/interfaces/StorageListResult.md +0 -41
  256. package/docs/api/interfaces/StorageUploadOptions.md +0 -101
  257. package/docs/api/interfaces/StorageUploadResult.md +0 -63
  258. package/docs/api/interfaces/StorageUrlOptions.md +0 -60
  259. package/docs/api/interfaces/StyleImport.md +0 -19
  260. package/docs/api/interfaces/SwitchProps.md +0 -34
  261. package/docs/api/interfaces/TabsContentProps.md +0 -9
  262. package/docs/api/interfaces/TabsListProps.md +0 -9
  263. package/docs/api/interfaces/TabsProps.md +0 -9
  264. package/docs/api/interfaces/TabsTriggerProps.md +0 -50
  265. package/docs/api/interfaces/TextareaProps.md +0 -53
  266. package/docs/api/interfaces/ToastActionElement.md +0 -12
  267. package/docs/api/interfaces/ToastProps.md +0 -9
  268. package/docs/api/interfaces/UnifiedAuthContextType.md +0 -823
  269. package/docs/api/interfaces/UnifiedAuthProviderProps.md +0 -173
  270. package/docs/api/interfaces/UseFormDialogOptions.md +0 -62
  271. package/docs/api/interfaces/UseFormDialogReturn.md +0 -117
  272. package/docs/api/interfaces/UseInactivityTrackerOptions.md +0 -138
  273. package/docs/api/interfaces/UseInactivityTrackerReturn.md +0 -123
  274. package/docs/api/interfaces/UsePublicEventLogoOptions.md +0 -87
  275. package/docs/api/interfaces/UsePublicEventLogoReturn.md +0 -84
  276. package/docs/api/interfaces/UsePublicEventOptions.md +0 -34
  277. package/docs/api/interfaces/UsePublicEventReturn.md +0 -71
  278. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +0 -47
  279. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +0 -123
  280. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +0 -97
  281. package/docs/api/interfaces/UseResolvedScopeOptions.md +0 -47
  282. package/docs/api/interfaces/UseResolvedScopeReturn.md +0 -47
  283. package/docs/api/interfaces/UseResourcePermissionsOptions.md +0 -34
  284. package/docs/api/interfaces/UserEventAccess.md +0 -121
  285. package/docs/api/interfaces/UserMenuProps.md +0 -88
  286. package/docs/api/interfaces/UserProfile.md +0 -63
  287. package/src/components/EventSelector/EventSelector.test.tsx +0 -720
  288. package/src/components/EventSelector/EventSelector.tsx +0 -423
  289. package/src/components/EventSelector/index.ts +0 -3
  290. package/src/components/OrganisationSelector/OrganisationSelector.test.tsx +0 -784
  291. package/src/components/OrganisationSelector/OrganisationSelector.tsx +0 -327
  292. package/src/components/OrganisationSelector/index.ts +0 -9
  293. /package/dist/{DataTable-TPTKCX4D.js.map → DataTable-E7YQZD7D.js.map} +0 -0
  294. /package/dist/{UnifiedAuthProvider-CH6Z342H.js.map → UnifiedAuthProvider-QPXO24B4.js.map} +0 -0
  295. /package/dist/{api-MVVQZLJI.js.map → api-6LVZTHDS.js.map} +0 -0
  296. /package/dist/{audit-B5P6FFIR.js.map → audit-V53FV5AG.js.map} +0 -0
  297. /package/dist/{chunk-2UOI2FG5.js.map → chunk-5EC5MEWX.js.map} +0 -0
  298. /package/dist/{chunk-7D4SUZUM.js.map → chunk-DGUM43GV.js.map} +0 -0
  299. /package/dist/{chunk-F2IMUDXZ.js.map → chunk-M7MPQISP.js.map} +0 -0
@@ -11,6 +11,11 @@
11
11
  * - Unused dependencies
12
12
  * - Missing peer dependencies
13
13
  * - Version mismatches
14
+ * - Redundant dependencies (duplicates of pace-core deps)
15
+ * - Missing dependencies (used but not declared)
16
+ * - Misclassified dependencies (wrong section)
17
+ * - Version mismatches across workspace
18
+ * - Optional dependencies build configuration
14
19
  */
15
20
 
16
21
  const fs = require('fs');
@@ -87,6 +92,59 @@ function getPaceCorePeerDeps(projectRoot) {
87
92
  return {};
88
93
  }
89
94
 
95
+ /**
96
+ * Get pace-core dependencies (runtime dependencies provided by pace-core)
97
+ */
98
+ function getPaceCoreDeps(projectRoot) {
99
+ try {
100
+ // Try to find pace-core in node_modules
101
+ const paceCorePath = path.join(projectRoot, 'node_modules', '@jmruthers', 'pace-core', 'package.json');
102
+ if (fs.existsSync(paceCorePath)) {
103
+ const paceCorePkg = JSON.parse(fs.readFileSync(paceCorePath, 'utf8'));
104
+ return paceCorePkg.dependencies || {};
105
+ }
106
+ } catch (error) {
107
+ // If we can't find it, return empty object
108
+ }
109
+ return {};
110
+ }
111
+
112
+ /**
113
+ * Get pace-core package.json (for workspace checks)
114
+ */
115
+ function getPaceCorePackageJson(projectRoot) {
116
+ try {
117
+ // Try to find pace-core in node_modules
118
+ const paceCorePath = path.join(projectRoot, 'node_modules', '@jmruthers', 'pace-core', 'package.json');
119
+ if (fs.existsSync(paceCorePath)) {
120
+ return JSON.parse(fs.readFileSync(paceCorePath, 'utf8'));
121
+ }
122
+ // Also try packages/core if we're in the workspace
123
+ const packagesCorePath = path.join(projectRoot, 'packages', 'core', 'package.json');
124
+ if (fs.existsSync(packagesCorePath)) {
125
+ return JSON.parse(fs.readFileSync(packagesCorePath, 'utf8'));
126
+ }
127
+ } catch (error) {
128
+ // If we can't find it, return null
129
+ }
130
+ return null;
131
+ }
132
+
133
+ /**
134
+ * Get workspace root package.json (for monorepo checks)
135
+ */
136
+ function getWorkspaceRootPackageJson(projectRoot) {
137
+ try {
138
+ const rootPath = path.join(projectRoot, 'package.json');
139
+ if (fs.existsSync(rootPath)) {
140
+ return JSON.parse(fs.readFileSync(rootPath, 'utf8'));
141
+ }
142
+ } catch (error) {
143
+ // If we can't find it, return null
144
+ }
145
+ return null;
146
+ }
147
+
90
148
  /**
91
149
  * Check if a dependency is used in config files
92
150
  */
@@ -180,9 +238,99 @@ function checkNonJsFiles(projectRoot, depName) {
180
238
  return false;
181
239
  }
182
240
 
241
+ /**
242
+ * Find all test files in the project
243
+ */
244
+ function findTestFiles(projectRoot) {
245
+ const testFiles = [];
246
+ const testPattern = /\.(test|spec)\.(ts|tsx|js|jsx)$/;
247
+ const ignoreDirs = ['node_modules', 'dist', 'build', '.next', 'coverage'];
248
+
249
+ const walkDir = (dir) => {
250
+ try {
251
+ const items = fs.readdirSync(dir);
252
+ items.forEach(item => {
253
+ const fullPath = path.join(dir, item);
254
+ const stat = fs.statSync(fullPath);
255
+
256
+ if (stat.isDirectory()) {
257
+ if (!ignoreDirs.includes(item) && !item.startsWith('.')) {
258
+ walkDir(fullPath);
259
+ }
260
+ } else if (stat.isFile() && testPattern.test(item)) {
261
+ testFiles.push(fullPath);
262
+ }
263
+ });
264
+ } catch (error) {
265
+ // Skip directories we can't read
266
+ }
267
+ };
268
+
269
+ try {
270
+ const srcPath = path.join(projectRoot, 'src');
271
+ if (fs.existsSync(srcPath)) {
272
+ walkDir(srcPath);
273
+ }
274
+ } catch (error) {
275
+ // Skip if can't read
276
+ }
277
+
278
+ return testFiles;
279
+ }
280
+
281
+ /**
282
+ * Check if a dependency is used in package.json scripts
283
+ */
284
+ function checkPackageScripts(packageJson, depName) {
285
+ if (!packageJson || !packageJson.scripts) {
286
+ return false;
287
+ }
288
+
289
+ const scripts = packageJson.scripts;
290
+ const scriptContent = Object.values(scripts).join(' ');
291
+
292
+ // Check if the package name appears in any script
293
+ // This is a simple check - package names in scripts are usually the command name
294
+ const depNameEscaped = depName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
295
+ const scriptPattern = new RegExp(`\\b${depNameEscaped}\\b`, 'i');
296
+
297
+ return scriptPattern.test(scriptContent);
298
+ }
299
+
300
+ /**
301
+ * Check if a dependency is only used in test files
302
+ */
303
+ function isOnlyUsedInTests(projectRoot, depName, testFiles) {
304
+ if (testFiles.length === 0) {
305
+ return false;
306
+ }
307
+
308
+ // Check if it's used in any test file
309
+ let foundInTests = false;
310
+ for (const testFile of testFiles) {
311
+ try {
312
+ const content = fs.readFileSync(testFile, 'utf8');
313
+ const depNameEscaped = depName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
314
+
315
+ // Check for imports
316
+ const importPattern = new RegExp(`from\\s+['"]${depNameEscaped}`, 'g');
317
+ const requirePattern = new RegExp(`require\\(['"]${depNameEscaped}`, 'g');
318
+
319
+ if (importPattern.test(content) || requirePattern.test(content)) {
320
+ foundInTests = true;
321
+ break;
322
+ }
323
+ } catch (error) {
324
+ // Skip files we can't read
325
+ }
326
+ }
327
+
328
+ return foundInTests;
329
+ }
330
+
183
331
  const dependenciesCheck = {
184
332
  name: 'dependencies',
185
- description: 'Dependency analysis (outdated versions, security, unused deps)',
333
+ description: 'Dependency analysis (outdated versions, security, unused deps, redundant deps, misclassified deps, version mismatches)',
186
334
  severity: 'warning',
187
335
 
188
336
  async run(context) {
@@ -207,9 +355,115 @@ const dependenciesCheck = {
207
355
  // Get pace-core peer dependencies
208
356
  const paceCorePeerDeps = getPaceCorePeerDeps(projectRoot);
209
357
 
358
+ // Get pace-core version (used in multiple checks)
359
+ const paceCoreVersion = !isPaceCoreRepository ? allDeps['@jmruthers/pace-core'] : null;
360
+
361
+ // Check for required React version (19.2.3) - do this early to ensure it always runs
362
+ // This check MUST run for all consuming apps (not pace-core repository itself)
363
+ if (!isPaceCoreRepository) {
364
+ const requiredReactVersion = '19.2.3';
365
+
366
+ // Read React versions from package.json - check both dependencies and devDependencies
367
+ const reactVersion = (packageJson.dependencies && packageJson.dependencies.react) ||
368
+ (packageJson.devDependencies && packageJson.devDependencies.react) ||
369
+ null;
370
+ const reactDomVersion = (packageJson.dependencies && packageJson.dependencies['react-dom']) ||
371
+ (packageJson.devDependencies && packageJson.devDependencies['react-dom']) ||
372
+ null;
373
+
374
+ // Normalize version for comparison (remove ^, ~, >=, <=, etc.)
375
+ const normalizeVersion = (v) => {
376
+ if (!v || typeof v !== 'string') {
377
+ return null;
378
+ }
379
+ // Remove range prefixes (^, ~, >=, <=, >, <, =)
380
+ let normalized = v.replace(/^[\^~>=<]/, '');
381
+ // Remove any whitespace
382
+ normalized = normalized.trim();
383
+ // Extract base version (e.g., "19.2.3" from "19.2.3" or "19.2.3-alpha.1")
384
+ const match = normalized.match(/^(\d+\.\d+\.\d+)/);
385
+ if (match && match[1]) {
386
+ return match[1];
387
+ }
388
+ // Fallback: try to extract any version pattern
389
+ const fallbackMatch = normalized.match(/(\d+\.\d+\.\d+)/);
390
+ return fallbackMatch ? fallbackMatch[1] : null;
391
+ };
392
+
393
+ // Check React version
394
+ if (reactVersion) {
395
+ const normalizedReactVersion = normalizeVersion(reactVersion);
396
+ const normalizedRequiredVersion = normalizeVersion(requiredReactVersion);
397
+
398
+ // Check if versions match - always add issue if they don't match
399
+ let versionMismatch = false;
400
+
401
+ if (normalizedReactVersion && normalizedRequiredVersion) {
402
+ // Both normalized successfully - compare normalized versions
403
+ versionMismatch = normalizedReactVersion !== normalizedRequiredVersion;
404
+ } else {
405
+ // Normalization failed - do a simple string check
406
+ const cleanReactVersion = reactVersion.replace(/^[\^~>=<]/, '').trim();
407
+ const cleanRequiredVersion = requiredReactVersion.replace(/^[\^~>=<]/, '').trim();
408
+ versionMismatch = cleanReactVersion !== cleanRequiredVersion && !cleanReactVersion.startsWith(requiredReactVersion);
409
+ }
410
+
411
+ if (versionMismatch) {
412
+ issues.push({
413
+ type: 'incorrect-react-version',
414
+ file: 'package.json',
415
+ message: `React version ${reactVersion} does not match required version ${requiredReactVersion}`,
416
+ recommendation: `Install React ${requiredReactVersion}. Run: npm install react@${requiredReactVersion} react-dom@${requiredReactVersion}`
417
+ });
418
+ }
419
+ } else {
420
+ issues.push({
421
+ type: 'missing-react',
422
+ file: 'package.json',
423
+ message: 'React is not installed but is required',
424
+ recommendation: `Install React ${requiredReactVersion}. Run: npm install react@${requiredReactVersion} react-dom@${requiredReactVersion}`
425
+ });
426
+ }
427
+
428
+ // Check react-dom version matches
429
+ if (reactDomVersion) {
430
+ const normalizedReactDomVersion = normalizeVersion(reactDomVersion);
431
+ const normalizedRequiredVersion = normalizeVersion(requiredReactVersion);
432
+
433
+ // Check if versions match - always add issue if they don't match
434
+ let versionMismatch = false;
435
+
436
+ if (normalizedReactDomVersion && normalizedRequiredVersion) {
437
+ // Both normalized successfully - compare normalized versions
438
+ versionMismatch = normalizedReactDomVersion !== normalizedRequiredVersion;
439
+ } else {
440
+ // Normalization failed - do a simple string check
441
+ const cleanReactDomVersion = reactDomVersion.replace(/^[\^~>=<]/, '').trim();
442
+ const cleanRequiredVersion = requiredReactVersion.replace(/^[\^~>=<]/, '').trim();
443
+ versionMismatch = cleanReactDomVersion !== cleanRequiredVersion && !cleanReactDomVersion.startsWith(requiredReactVersion);
444
+ }
445
+
446
+ if (versionMismatch) {
447
+ issues.push({
448
+ type: 'incorrect-react-dom-version',
449
+ file: 'package.json',
450
+ message: `react-dom version ${reactDomVersion} does not match required version ${requiredReactVersion}`,
451
+ recommendation: `Install react-dom ${requiredReactVersion}. Run: npm install react-dom@${requiredReactVersion}`
452
+ });
453
+ }
454
+ } else if (reactVersion) {
455
+ // If react is installed but react-dom is not, that's also an issue
456
+ issues.push({
457
+ type: 'missing-react-dom',
458
+ file: 'package.json',
459
+ message: 'react-dom is not installed but is required',
460
+ recommendation: `Install react-dom ${requiredReactVersion}. Run: npm install react-dom@${requiredReactVersion}`
461
+ });
462
+ }
463
+ }
464
+
210
465
  // Check pace-core version (skip if this is the pace-core repository itself)
211
466
  if (!isPaceCoreRepository) {
212
- const paceCoreVersion = allDeps['@jmruthers/pace-core'];
213
467
  if (paceCoreVersion) {
214
468
  // Check if version is outdated (simplified - would need to check npm registry)
215
469
  if (paceCoreVersion.startsWith('^') || paceCoreVersion.startsWith('~')) {
@@ -251,10 +505,15 @@ const dependenciesCheck = {
251
505
  // Also scan config files
252
506
  const configFiles = findConfigFiles(projectRoot);
253
507
 
254
- // Build set of imported packages from source files
508
+ // Find test files separately for better analysis
509
+ const testFiles = !isPaceCoreRepository ? findTestFiles(projectRoot) : [];
510
+
511
+ // Build set of imported packages from source files (runtime)
255
512
  const importedPackages = new Set();
513
+ // Build set of packages used only in tests
514
+ const testOnlyPackages = new Set();
256
515
 
257
- // Scan source files
516
+ // Scan source files (runtime code)
258
517
  sourceFiles.forEach(filePath => {
259
518
  try {
260
519
  const content = fs.readFileSync(filePath, 'utf8');
@@ -277,6 +536,42 @@ const dependenciesCheck = {
277
536
  importedPackages.add(pkgName);
278
537
  }
279
538
  }
539
+ // Check for dynamic imports
540
+ const dynamicImportPattern = /import\(['"]([^'"]+)['"]\)/g;
541
+ while ((match = dynamicImportPattern.exec(content)) !== null) {
542
+ const modulePath = match[1];
543
+ const pkgName = extractPackageName(modulePath);
544
+ if (pkgName) {
545
+ importedPackages.add(pkgName);
546
+ }
547
+ }
548
+ } catch (error) {
549
+ // Skip files with errors
550
+ }
551
+ });
552
+
553
+ // Scan test files separately
554
+ testFiles.forEach(filePath => {
555
+ try {
556
+ const content = fs.readFileSync(filePath, 'utf8');
557
+ const importPattern = /from\s+['"]([^'"]+)['"]/g;
558
+ let match;
559
+ while ((match = importPattern.exec(content)) !== null) {
560
+ const modulePath = match[1];
561
+ const pkgName = extractPackageName(modulePath);
562
+ if (pkgName && !importedPackages.has(pkgName)) {
563
+ // Only add to test-only if not already used in runtime code
564
+ testOnlyPackages.add(pkgName);
565
+ }
566
+ }
567
+ const requirePattern = /require\(['"]([^'"]+)['"]\)/g;
568
+ while ((match = requirePattern.exec(content)) !== null) {
569
+ const modulePath = match[1];
570
+ const pkgName = extractPackageName(modulePath);
571
+ if (pkgName && !importedPackages.has(pkgName)) {
572
+ testOnlyPackages.add(pkgName);
573
+ }
574
+ }
280
575
  } catch (error) {
281
576
  // Skip files with errors
282
577
  }
@@ -352,9 +647,24 @@ const dependenciesCheck = {
352
647
  if (dep === '@jmruthers/pace-core') {
353
648
  return;
354
649
  }
650
+
651
+ // Skip if it's a dependency of pace-core (required by pace-core's bundled code)
652
+ if (!isPaceCoreRepository) {
653
+ const paceCoreDeps = getPaceCoreDeps(projectRoot);
654
+ if (paceCoreDeps[dep]) {
655
+ // This dependency is required by pace-core, so it's not unused
656
+ return;
657
+ }
658
+ }
355
659
 
356
- // Check if package is imported in source files
660
+ // Check if package is imported in source files (runtime)
357
661
  if (importedPackages.has(dep)) {
662
+ // Package is used in runtime code, so it's needed
663
+ return;
664
+ }
665
+
666
+ // Check if it's used in package.json scripts
667
+ if (checkPackageScripts(packageJson, dep)) {
358
668
  return;
359
669
  }
360
670
 
@@ -368,6 +678,10 @@ const dependenciesCheck = {
368
678
  return;
369
679
  }
370
680
 
681
+ // Check if it's used in test files
682
+ const isUsedInTests = testOnlyPackages.has(dep) || isOnlyUsedInTests(projectRoot, dep, testFiles);
683
+ const isOnlyInTests = isUsedInTests && !importedPackages.has(dep);
684
+
371
685
  // For dev dependencies, provide more context
372
686
  const isDevDep = dep in devDeps;
373
687
  const context = [];
@@ -381,26 +695,37 @@ const dependenciesCheck = {
381
695
  context.push('This may be required by @jmruthers/pace-core as a transitive dependency');
382
696
  }
383
697
 
698
+ // Check if it's in vite.config.ts optimizeDeps (common for pace-core peer deps)
699
+ const viteConfigPath = path.join(projectRoot, 'vite.config.ts');
700
+ const viteConfigJsPath = path.join(projectRoot, 'vite.config.js');
701
+ let isInViteConfig = false;
702
+
703
+ for (const configPath of [viteConfigPath, viteConfigJsPath]) {
704
+ if (fs.existsSync(configPath)) {
705
+ try {
706
+ const viteContent = fs.readFileSync(configPath, 'utf8');
707
+ if (viteContent.includes(dep)) {
708
+ isInViteConfig = true;
709
+ break;
710
+ }
711
+ } catch (error) {
712
+ // Skip if can't read
713
+ }
714
+ }
715
+ }
716
+
384
717
  // Only warn if we're reasonably sure it's unused
385
718
  // For runtime dependencies, be more strict
386
719
  if (!isDevDep) {
387
- // Check if it's in vite.config.ts optimizeDeps (common for pace-core peer deps)
388
- const viteConfigPath = path.join(projectRoot, 'vite.config.ts');
389
- const viteConfigJsPath = path.join(projectRoot, 'vite.config.js');
390
- let isInViteConfig = false;
391
-
392
- for (const configPath of [viteConfigPath, viteConfigJsPath]) {
393
- if (fs.existsSync(configPath)) {
394
- try {
395
- const viteContent = fs.readFileSync(configPath, 'utf8');
396
- if (viteContent.includes(dep)) {
397
- isInViteConfig = true;
398
- break;
399
- }
400
- } catch (error) {
401
- // Skip if can't read
402
- }
403
- }
720
+ // If it's only used in tests, suggest moving to devDependencies
721
+ if (isOnlyInTests) {
722
+ warnings.push({
723
+ type: 'test-only-runtime-dependency',
724
+ file: 'package.json',
725
+ message: `Runtime dependency '${dep}' is only used in test files`,
726
+ recommendation: `Move '${dep}' from dependencies to devDependencies since it's only used in tests. This will reduce your production bundle size.`
727
+ });
728
+ return;
404
729
  }
405
730
 
406
731
  const recommendation = `Verify if '${dep}' is actually used. ${context.length > 0 ? context.join(' ') : ''}${isInViteConfig ? 'This dependency is referenced in vite.config.ts, which suggests it may be required by pace-core components. ' : ''}If it's a peer dependency of @jmruthers/pace-core (check pace-core's package.json), it's required even if not directly imported. If confirmed unused, remove it to reduce bundle size.`;
@@ -417,7 +742,7 @@ const dependenciesCheck = {
417
742
  type: 'potentially-unused-dev-dependency',
418
743
  file: 'package.json',
419
744
  message: `Dev dependency '${dep}' may be unused`,
420
- recommendation: `Check if '${dep}' is used in build configs, test files, or by pace-core. ${context.join(' ')}If confirmed unused, you can remove it.`
745
+ recommendation: `Check if '${dep}' is used in build configs, test files, or by pace-core. ${context.join(' ')}${isInViteConfig ? 'This dependency is referenced in vite.config.ts. ' : ''}If confirmed unused, you can remove it.`
421
746
  });
422
747
  }
423
748
  });
@@ -435,18 +760,233 @@ const dependenciesCheck = {
435
760
  }
436
761
  });
437
762
 
438
- // Check for version mismatches (e.g., React versions)
439
- const reactVersion = allDeps.react;
440
- const reactDomVersion = allDeps['react-dom'];
441
- if (reactVersion && reactDomVersion && reactVersion !== reactDomVersion) {
763
+ // Check for version mismatches between react and react-dom
764
+ // (The required version check is done earlier in the function)
765
+ const reactVersionForMismatch = packageJson.dependencies?.react || packageJson.devDependencies?.react;
766
+ const reactDomVersionForMismatch = packageJson.dependencies?.['react-dom'] || packageJson.devDependencies?.['react-dom'];
767
+
768
+ if (reactVersionForMismatch && reactDomVersionForMismatch && reactVersionForMismatch !== reactDomVersionForMismatch) {
442
769
  issues.push({
443
770
  type: 'version-mismatch',
444
771
  file: 'package.json',
445
- message: `React version mismatch: react@${reactVersion} vs react-dom@${reactDomVersion}`,
772
+ message: `React version mismatch: react@${reactVersionForMismatch} vs react-dom@${reactDomVersionForMismatch}`,
446
773
  recommendation: 'React and react-dom versions must match exactly'
447
774
  });
448
775
  }
449
776
 
777
+ // ============================================
778
+ // NEW CHECKS: Future-proofing dependency management
779
+ // ============================================
780
+
781
+ // 1. Check for redundant dependencies in consuming apps
782
+ // (packages already provided by pace-core)
783
+ if (!isPaceCoreRepository) {
784
+ const paceCoreDeps = getPaceCoreDeps(projectRoot);
785
+ const paceCorePeerDeps = getPaceCorePeerDeps(projectRoot);
786
+
787
+ // Packages that pace-core provides as dependencies (should not be duplicated)
788
+ const redundantDeps = [];
789
+ Object.keys(packageJson.dependencies || {}).forEach(dep => {
790
+ // Skip pace-core itself
791
+ if (dep === '@jmruthers/pace-core') {
792
+ return;
793
+ }
794
+
795
+ // Check if it's provided by pace-core as a dependency
796
+ if (paceCoreDeps[dep]) {
797
+ redundantDeps.push({
798
+ dep,
799
+ reason: 'provided by pace-core as a dependency',
800
+ paceCoreVersion: paceCoreDeps[dep]
801
+ });
802
+ }
803
+ });
804
+
805
+ redundantDeps.forEach(({ dep, reason, paceCoreVersion }) => {
806
+ warnings.push({
807
+ type: 'redundant-dependency',
808
+ file: 'package.json',
809
+ message: `Dependency '${dep}' is redundant - ${reason}`,
810
+ recommendation: `Remove '${dep}' from dependencies. It's already provided by @jmruthers/pace-core@${paceCoreVersion}. Exception: If you need a different version or direct control, keep it but document why.`
811
+ });
812
+ });
813
+ }
814
+
815
+ // 2. Check for missing dependencies (used but not declared)
816
+ if (!isPaceCoreRepository) {
817
+ // Check for dynamic imports that might need to be declared
818
+ const optionalDeps = ['recharts', 'papaparse', 'lodash.debounce', 'lodash.throttle'];
819
+ const dynamicImportPattern = /import\(['"]([^'"]+)['"]\)/g;
820
+
821
+ sourceFiles.forEach(filePath => {
822
+ try {
823
+ const content = fs.readFileSync(filePath, 'utf8');
824
+ let match;
825
+ while ((match = dynamicImportPattern.exec(content)) !== null) {
826
+ const modulePath = match[1];
827
+ const pkgName = extractPackageName(modulePath);
828
+ if (pkgName && optionalDeps.includes(pkgName)) {
829
+ // Check if it's declared
830
+ if (!allDeps[pkgName]) {
831
+ suggestions.push({
832
+ type: 'missing-optional-dependency',
833
+ file: path.relative(projectRoot, filePath),
834
+ message: `Optional dependency '${pkgName}' is dynamically imported but not declared`,
835
+ recommendation: `Add '${pkgName}' to dependencies if you use this feature, or ensure it's marked as external in your build config`
836
+ });
837
+ }
838
+ }
839
+ }
840
+ } catch (error) {
841
+ // Skip files with errors
842
+ }
843
+ });
844
+ }
845
+
846
+ // 3. Check for misclassified dependencies
847
+ // Runtime deps in devDependencies, peer deps in dependencies, etc.
848
+ if (isPaceCoreRepository) {
849
+ // Check pace-core's own package.json
850
+ const packagesCorePath = path.join(projectRoot, 'packages', 'core', 'package.json');
851
+ if (fs.existsSync(packagesCorePath)) {
852
+ const corePkg = JSON.parse(fs.readFileSync(packagesCorePath, 'utf8'));
853
+ const coreDeps = corePkg.dependencies || {};
854
+ const coreDevDeps = corePkg.devDependencies || {};
855
+ const corePeerDeps = corePkg.peerDependencies || {};
856
+
857
+ // Check if peer dependencies are incorrectly in dependencies
858
+ Object.keys(coreDeps).forEach(dep => {
859
+ if (corePeerDeps[dep]) {
860
+ issues.push({
861
+ type: 'misclassified-dependency',
862
+ file: 'packages/core/package.json',
863
+ message: `'${dep}' is declared as both dependency and peerDependency`,
864
+ recommendation: `Remove '${dep}' from dependencies. Peer dependencies should only be in peerDependencies.`
865
+ });
866
+ }
867
+ });
868
+
869
+ // Check if runtime dependencies are in devDependencies
870
+ // This is harder to detect automatically, but we can check for common patterns
871
+ const runtimeDepsInDev = ['@hookform/resolvers', '@supabase/supabase-js', '@tanstack/react-query'];
872
+ runtimeDepsInDev.forEach(dep => {
873
+ if (coreDevDeps[dep] && !coreDeps[dep]) {
874
+ // Check if it's used in source code
875
+ const coreSrcPath = path.join(projectRoot, 'packages', 'core', 'src');
876
+ if (fs.existsSync(coreSrcPath)) {
877
+ const coreSourceFiles = findSourceFiles(coreSrcPath);
878
+ let isUsed = false;
879
+ coreSourceFiles.forEach(filePath => {
880
+ try {
881
+ const content = fs.readFileSync(filePath, 'utf8');
882
+ if (content.includes(dep) || content.includes(extractPackageName(dep))) {
883
+ isUsed = true;
884
+ }
885
+ } catch (error) {
886
+ // Skip
887
+ }
888
+ });
889
+
890
+ if (isUsed) {
891
+ warnings.push({
892
+ type: 'misclassified-dependency',
893
+ file: 'packages/core/package.json',
894
+ message: `'${dep}' is in devDependencies but appears to be used in source code`,
895
+ recommendation: `Move '${dep}' from devDependencies to dependencies if it's used at runtime`
896
+ });
897
+ }
898
+ }
899
+ }
900
+ });
901
+ }
902
+ }
903
+
904
+ // 4. Check for version mismatches across workspace (monorepo)
905
+ if (isPaceCoreRepository) {
906
+ const rootPkg = getWorkspaceRootPackageJson(projectRoot);
907
+ const corePkg = getPaceCorePackageJson(projectRoot);
908
+
909
+ if (rootPkg && corePkg) {
910
+ const rootDeps = { ...rootPkg.dependencies, ...rootPkg.devDependencies };
911
+ const coreDeps = { ...corePkg.dependencies, ...corePkg.devDependencies };
912
+
913
+ // Check for version mismatches in common dependencies
914
+ const commonDeps = Object.keys(rootDeps).filter(dep => coreDeps[dep]);
915
+ commonDeps.forEach(dep => {
916
+ const rootVersion = rootDeps[dep];
917
+ const coreVersion = coreDeps[dep];
918
+
919
+ // Normalize versions for comparison (remove ^, ~, etc.)
920
+ const normalizeVersion = (v) => v.replace(/^[\^~]/, '');
921
+ const rootNormalized = normalizeVersion(rootVersion);
922
+ const coreNormalized = normalizeVersion(coreVersion);
923
+
924
+ if (rootNormalized !== coreNormalized) {
925
+ warnings.push({
926
+ type: 'workspace-version-mismatch',
927
+ file: 'package.json',
928
+ message: `Version mismatch for '${dep}': root@${rootVersion} vs packages/core@${coreVersion}`,
929
+ recommendation: `Align versions across workspace. Consider using the same version in both root and packages/core package.json files.`
930
+ });
931
+ }
932
+ });
933
+ }
934
+ }
935
+
936
+ // 5. Check for missing peer dependencies in consuming apps
937
+ if (!isPaceCoreRepository) {
938
+ const paceCorePkg = getPaceCorePackageJson(projectRoot);
939
+ if (paceCorePkg) {
940
+ const peerDeps = paceCorePkg.peerDependencies || {};
941
+ const allDeps = { ...packageJson.dependencies, ...packageJson.devDependencies };
942
+
943
+ Object.keys(peerDeps).forEach(peerDep => {
944
+ if (!allDeps[peerDep]) {
945
+ issues.push({
946
+ type: 'missing-peer-dependency',
947
+ file: 'package.json',
948
+ message: `Missing peer dependency '${peerDep}' required by @jmruthers/pace-core`,
949
+ recommendation: `Install '${peerDep}@${peerDeps[peerDep]}' as a dependency. Run: npm install ${peerDep}@${peerDeps[peerDep]}`
950
+ });
951
+ }
952
+ });
953
+ }
954
+ }
955
+
956
+ // 6. Check for optional dependencies handling in vite.config
957
+ if (!isPaceCoreRepository) {
958
+ const viteConfigPath = path.join(projectRoot, 'vite.config.ts');
959
+ const viteConfigJsPath = path.join(projectRoot, 'vite.config.js');
960
+ const optionalDeps = ['recharts', 'papaparse'];
961
+
962
+ for (const configPath of [viteConfigPath, viteConfigJsPath]) {
963
+ if (fs.existsSync(configPath)) {
964
+ try {
965
+ const viteContent = fs.readFileSync(configPath, 'utf8');
966
+ optionalDeps.forEach(dep => {
967
+ // Check if it's marked as external
968
+ const isExternal = viteContent.includes(`external`) &&
969
+ (viteContent.includes(`'${dep}'`) || viteContent.includes(`"${dep}"`));
970
+
971
+ // Check if it's in dependencies
972
+ const isInDeps = allDeps[dep];
973
+
974
+ if (!isExternal && !isInDeps) {
975
+ suggestions.push({
976
+ type: 'optional-dependency-config',
977
+ file: path.relative(projectRoot, configPath),
978
+ message: `Optional dependency '${dep}' should be handled in build config`,
979
+ recommendation: `Add '${dep}' to build.rollupOptions.external if you want it resolved at runtime, or install it as a dependency if you want it bundled`
980
+ });
981
+ }
982
+ });
983
+ } catch (error) {
984
+ // Skip if can't read
985
+ }
986
+ }
987
+ }
988
+ }
989
+
450
990
  return { issues, warnings, suggestions };
451
991
  }
452
992
  };