@jmruthers/pace-core 0.6.4 → 0.6.6

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 (387) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/README.md +5 -403
  3. package/core-usage-manifest.json +93 -0
  4. package/cursor-rules/00-pace-core-compliance.mdc +128 -26
  5. package/cursor-rules/01-standards-compliance.mdc +49 -8
  6. package/cursor-rules/02-project-structure.mdc +6 -0
  7. package/cursor-rules/03-solid-principles.mdc +2 -0
  8. package/cursor-rules/04-testing-standards.mdc +2 -0
  9. package/cursor-rules/05-bug-reports-and-features.mdc +2 -0
  10. package/cursor-rules/06-code-quality.mdc +2 -0
  11. package/cursor-rules/07-tech-stack-compliance.mdc +2 -0
  12. package/cursor-rules/08-markup-quality.mdc +52 -27
  13. package/cursor-rules/09-rbac-compliance.mdc +462 -0
  14. package/cursor-rules/10-error-handling-patterns.mdc +179 -0
  15. package/cursor-rules/11-performance-optimization.mdc +169 -0
  16. package/cursor-rules/12-ci-cd-integration.mdc +150 -0
  17. package/dist/{AuthService-Cb34EQs3.d.ts → AuthService-DmfO5rGS.d.ts} +10 -0
  18. package/dist/{DataTable-BMRU8a1j.d.ts → DataTable-2N_tqbfq.d.ts} +1 -1
  19. package/dist/DataTable-LRJL4IRV.js +15 -0
  20. package/dist/{PublicPageProvider-DEMpysFR.d.ts → PublicPageProvider-BBH6Vqg7.d.ts} +72 -139
  21. package/dist/UnifiedAuthProvider-ZT6TIGM7.js +7 -0
  22. package/dist/api-Y4MQWOFW.js +4 -0
  23. package/dist/audit-MYQXYZFU.js +3 -0
  24. package/dist/{chunk-J36DSWQK.js → chunk-2HGJFNAH.js} +8 -28
  25. package/dist/{chunk-OEWDTMG7.js → chunk-3O3WHILE.js} +38 -121
  26. package/dist/{chunk-M43Y4SSO.js → chunk-3QC3KRHK.js} +1 -14
  27. package/dist/{chunk-DGUM43GV.js → chunk-3RG5ZIWI.js} +1 -4
  28. package/dist/{chunk-QXHPKYJV.js → chunk-4SXLQIZO.js} +1 -26
  29. package/dist/chunk-4T7OBVTU.js +62 -0
  30. package/dist/{chunk-E66EQZE6.js → chunk-6GLLNA6U.js} +3 -9
  31. package/dist/{chunk-ZSAAAMVR.js → chunk-6QYDGKQY.js} +1 -4
  32. package/dist/{chunk-NN6WWZ5U.js → chunk-7TYHROIV.js} +579 -563
  33. package/dist/{chunk-M7MPQISP.js → chunk-A55DK444.js} +9 -16
  34. package/dist/{chunk-63FOKYGO.js → chunk-AHU7G2R5.js} +2 -11
  35. package/dist/{chunk-L4OXEN46.js → chunk-BVP2BCJF.js} +2 -16
  36. package/dist/chunk-C7NSAPTL.js +1 -0
  37. package/dist/{chunk-YKRAFF5K.js → chunk-FENMYN2U.js} +73 -149
  38. package/dist/{chunk-AVMLPIM7.js → chunk-FTCRZOG2.js} +284 -432
  39. package/dist/{chunk-G37KK66H.js → chunk-FYHN4DD5.js} +60 -19
  40. package/dist/{chunk-VBXEHIUJ.js → chunk-HF6O3O37.js} +6 -88
  41. package/dist/{chunk-I6DAQMWX.js → chunk-LAZMKTTF.js} +930 -891
  42. package/dist/{chunk-5EC5MEWX.js → chunk-MAGBIDNS.js} +77 -222
  43. package/dist/chunk-MBADTM7L.js +64 -0
  44. package/dist/chunk-OHIK3MIO.js +994 -0
  45. package/dist/{chunk-6SOIHG6Z.js → chunk-S7DKJPLT.js} +115 -44
  46. package/dist/{chunk-FMUCXFII.js → chunk-SD6WQY43.js} +1 -5
  47. package/dist/{chunk-PWLANIRT.js → chunk-TTRFSOKR.js} +1 -7
  48. package/dist/{chunk-5DRSZLL2.js → chunk-UH3NTO3F.js} +1 -6
  49. package/dist/{chunk-FFQEQTNW.js → chunk-UIYSCEV7.js} +134 -45
  50. package/dist/{chunk-3LPHPB62.js → chunk-ZFYPMX46.js} +271 -87
  51. package/dist/{chunk-7JPAB3T5.js → chunk-ZS5VO5JB.js} +1989 -1283
  52. package/dist/components.d.ts +6 -6
  53. package/dist/components.js +57 -267
  54. package/dist/{database.generated-CzIvgcPu.d.ts → database.generated-CcnC_DRc.d.ts} +4795 -3691
  55. package/dist/eslint-rules/index.cjs +22 -0
  56. package/dist/eslint-rules/rules/compliance.cjs +348 -0
  57. package/dist/eslint-rules/rules/components.cjs +113 -0
  58. package/dist/eslint-rules/rules/imports.cjs +102 -0
  59. package/dist/eslint-rules/rules/rbac.cjs +790 -0
  60. package/dist/eslint-rules/utils/helpers.cjs +42 -0
  61. package/dist/eslint-rules/utils/manifest-loader.cjs +75 -0
  62. package/dist/hooks.d.ts +5 -5
  63. package/dist/hooks.js +62 -270
  64. package/dist/icons/index.d.ts +1 -0
  65. package/dist/icons/index.js +1 -0
  66. package/dist/index.d.ts +36 -26
  67. package/dist/index.js +87 -690
  68. package/dist/providers.d.ts +2 -2
  69. package/dist/providers.js +8 -35
  70. package/dist/rbac/eslint-rules.d.ts +46 -44
  71. package/dist/rbac/eslint-rules.js +7 -4
  72. package/dist/rbac/index.d.ts +124 -594
  73. package/dist/rbac/index.js +14 -207
  74. package/dist/styles/index.js +2 -12
  75. package/dist/theming/runtime.js +3 -19
  76. package/dist/{timezone-CHhWg6b4.d.ts → timezone-BZe_eUxx.d.ts} +175 -1
  77. package/dist/{types-CkbwOr4Y.d.ts → types-B-K_5VnO.d.ts} +4 -0
  78. package/dist/types-t9H8qKRw.d.ts +55 -0
  79. package/dist/types.d.ts +1 -1
  80. package/dist/types.js +7 -94
  81. package/dist/{usePublicRouteParams-i3qtoBgg.d.ts → usePublicRouteParams-COZ28Mvq.d.ts} +9 -9
  82. package/dist/utils.d.ts +24 -117
  83. package/dist/utils.js +54 -392
  84. package/docs/README.md +16 -6
  85. package/docs/api/README.md +4 -402
  86. package/docs/api/modules.md +454 -930
  87. package/docs/api-reference/components.md +3 -1
  88. package/docs/api-reference/deprecated.md +31 -6
  89. package/docs/api-reference/rpc-functions.md +78 -3
  90. package/docs/best-practices/accessibility.md +6 -3
  91. package/docs/getting-started/cursor-rules.md +3 -23
  92. package/docs/getting-started/dependencies.md +650 -0
  93. package/docs/getting-started/installation-guide.md +20 -7
  94. package/docs/getting-started/quick-start.md +23 -12
  95. package/docs/implementation-guides/permission-enforcement.md +4 -0
  96. package/docs/rbac/MIGRATION_GUIDE.md +819 -0
  97. package/docs/rbac/RBAC_CONTRACT.md +724 -0
  98. package/docs/rbac/README.md +12 -3
  99. package/docs/rbac/edge-functions-guide.md +376 -0
  100. package/docs/rbac/secure-client-protection.md +0 -34
  101. package/docs/standards/00-pace-core-compliance.md +967 -0
  102. package/docs/standards/01-standards-compliance.md +188 -0
  103. package/docs/standards/02-project-structure.md +985 -0
  104. package/docs/standards/03-solid-principles.md +39 -0
  105. package/docs/standards/04-testing-standards.md +36 -0
  106. package/docs/standards/05-bug-reports-and-features.md +27 -0
  107. package/docs/standards/{04-code-style-standard.md → 06-code-quality.md} +2 -0
  108. package/docs/standards/07-tech-stack-compliance.md +30 -0
  109. package/docs/standards/08-markup-quality.md +345 -0
  110. package/docs/standards/{07-rbac-and-rls-standard.md → 09-rbac-compliance.md} +149 -54
  111. package/docs/standards/10-error-handling-patterns.md +401 -0
  112. package/docs/standards/11-performance-optimization.md +348 -0
  113. package/docs/standards/12-ci-cd-integration.md +370 -0
  114. package/docs/standards/ALIGNMENT_REVIEW_SUMMARY.md +192 -0
  115. package/docs/standards/README.md +62 -33
  116. package/docs/troubleshooting/organisation-context-setup.md +42 -19
  117. package/eslint-config-pace-core.cjs +20 -4
  118. package/package.json +31 -21
  119. package/scripts/audit/audit-compliance.cjs +1295 -0
  120. package/scripts/audit/audit-components.cjs +260 -0
  121. package/scripts/audit/audit-dependencies.cjs +395 -0
  122. package/scripts/audit/audit-rbac.cjs +954 -0
  123. package/scripts/audit/audit-standards.cjs +1268 -0
  124. package/scripts/audit/index.cjs +1898 -194
  125. package/scripts/install-cursor-rules.cjs +259 -8
  126. package/scripts/validate-master.js +1 -1
  127. package/src/__tests__/fixtures/supabase.ts +1 -1
  128. package/src/__tests__/helpers/__tests__/component-test-utils.test.tsx +1 -1
  129. package/src/__tests__/helpers/__tests__/optimized-test-setup.test.ts +1 -1
  130. package/src/__tests__/helpers/__tests__/supabaseMock.test.ts +1 -1
  131. package/src/__tests__/helpers/__tests__/test-utils.test.tsx +3 -3
  132. package/src/__tests__/helpers/component-test-utils.tsx +1 -1
  133. package/src/__tests__/helpers/supabaseMock.ts +2 -2
  134. package/src/__tests__/public-recipe-view.test.ts +38 -9
  135. package/src/components/Button/Button.tsx +5 -1
  136. package/src/components/ContextSelector/ContextSelector.tsx +42 -39
  137. package/src/components/DataTable/__tests__/keyboard.test.tsx +15 -2
  138. package/src/components/DataTable/components/DataTableBody.tsx +55 -31
  139. package/src/components/DataTable/components/DataTableCore.tsx +186 -13
  140. package/src/components/DataTable/components/DataTableLayout.tsx +30 -5
  141. package/src/components/DataTable/components/EditFields.tsx +23 -3
  142. package/src/components/DataTable/components/EditableRow.tsx +7 -2
  143. package/src/components/DataTable/components/ImportModal.tsx +4 -6
  144. package/src/components/DataTable/components/RowComponent.tsx +12 -0
  145. package/src/components/DataTable/components/ViewRowModal.tsx +4 -4
  146. package/src/components/DataTable/components/__tests__/ImportModal.test.tsx +455 -96
  147. package/src/components/DataTable/components/__tests__/ViewRowModal.test.tsx +122 -58
  148. package/src/components/DataTable/components/hooks/usePermissionTracking.ts +0 -4
  149. package/src/components/DataTable/core/DataTableContext.tsx +1 -1
  150. package/src/components/DataTable/hooks/__tests__/useDataTableState.test.ts +51 -47
  151. package/src/components/DataTable/hooks/useDataTablePermissions.ts +24 -21
  152. package/src/components/DataTable/hooks/useDataTableState.ts +125 -9
  153. package/src/components/DataTable/hooks/useTableColumns.ts +40 -2
  154. package/src/components/DataTable/hooks/useTableHandlers.ts +11 -0
  155. package/src/components/DataTable/types.ts +5 -0
  156. package/src/components/DateTimeField/DateTimeField.tsx +20 -20
  157. package/src/components/DateTimeField/README.md +5 -2
  158. package/src/components/Dialog/Dialog.test.tsx +361 -318
  159. package/src/components/Dialog/Dialog.tsx +1154 -323
  160. package/src/components/Dialog/index.ts +3 -3
  161. package/src/components/FileDisplay/FileDisplay.test.tsx +45 -2
  162. package/src/components/FileDisplay/FileDisplay.tsx +28 -22
  163. package/src/components/Form/Form.test.tsx +9 -10
  164. package/src/components/Form/Form.tsx +369 -9
  165. package/src/components/InactivityWarningModal/InactivityWarningModal.test.tsx +28 -28
  166. package/src/components/InactivityWarningModal/InactivityWarningModal.tsx +40 -54
  167. package/src/components/LoginForm/LoginForm.tsx +2 -2
  168. package/src/components/NavigationMenu/NavigationMenu.test.tsx +14 -13
  169. package/src/components/NavigationMenu/NavigationMenu.tsx +2 -2
  170. package/src/components/NavigationMenu/useNavigationFiltering.ts +11 -21
  171. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +6 -4
  172. package/src/components/PaceAppLayout/PaceAppLayout.tsx +30 -41
  173. package/src/components/PaceAppLayout/README.md +10 -9
  174. package/src/components/PaceAppLayout/test-setup.tsx +40 -31
  175. package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +108 -61
  176. package/src/components/PaceLoginPage/PaceLoginPage.tsx +27 -3
  177. package/src/components/PasswordChange/PasswordChangeForm.test.tsx +61 -0
  178. package/src/components/PasswordChange/PasswordChangeForm.tsx +20 -13
  179. package/src/components/PublicLayout/PublicLayout.test.tsx +7 -3
  180. package/src/components/PublicLayout/PublicPageLayout.tsx +5 -8
  181. package/src/components/Select/Select.tsx +23 -21
  182. package/src/components/Select/types.ts +1 -1
  183. package/src/components/UserMenu/UserMenu.test.tsx +38 -6
  184. package/src/components/UserMenu/UserMenu.tsx +39 -34
  185. package/src/components/index.ts +3 -4
  186. package/src/eslint-rules/index.cjs +22 -0
  187. package/src/eslint-rules/rules/compliance.cjs +348 -0
  188. package/src/eslint-rules/rules/components.cjs +113 -0
  189. package/src/eslint-rules/rules/imports.cjs +102 -0
  190. package/src/eslint-rules/rules/rbac.cjs +790 -0
  191. package/src/eslint-rules/utils/helpers.cjs +42 -0
  192. package/src/eslint-rules/utils/manifest-loader.cjs +75 -0
  193. package/src/hooks/__tests__/hooks.integration.test.tsx +6 -8
  194. package/src/hooks/__tests__/useAppConfig.unit.test.ts +129 -67
  195. package/src/hooks/__tests__/usePublicEvent.simple.test.ts +149 -67
  196. package/src/hooks/__tests__/usePublicEvent.test.ts +149 -79
  197. package/src/hooks/__tests__/usePublicEvent.unit.test.ts +158 -109
  198. package/src/hooks/__tests__/useSessionDraft.test.ts +163 -0
  199. package/src/hooks/__tests__/useSessionRestoration.unit.test.tsx +10 -5
  200. package/src/hooks/public/usePublicEvent.ts +62 -190
  201. package/src/hooks/public/usePublicEventLogo.test.ts +70 -17
  202. package/src/hooks/public/usePublicEventLogo.ts +19 -9
  203. package/src/hooks/useAppConfig.ts +26 -24
  204. package/src/hooks/useEventTheme.test.ts +211 -233
  205. package/src/hooks/useEventTheme.ts +19 -28
  206. package/src/hooks/useEvents.ts +11 -7
  207. package/src/hooks/useKeyboardShortcuts.ts +1 -1
  208. package/src/hooks/useOrganisationPermissions.ts +9 -11
  209. package/src/hooks/useOrganisations.ts +13 -7
  210. package/src/hooks/useQueryCache.ts +0 -1
  211. package/src/hooks/useSessionDraft.ts +380 -0
  212. package/src/hooks/useSessionRestoration.ts +3 -1
  213. package/src/icons/index.ts +27 -0
  214. package/src/index.ts +16 -1
  215. package/src/providers/OrganisationProvider.tsx +23 -14
  216. package/src/providers/services/EventServiceProvider.tsx +1 -24
  217. package/src/providers/services/UnifiedAuthProvider.tsx +5 -48
  218. package/src/providers/services/__tests__/UnifiedAuthProvider.integration.test.tsx +3 -0
  219. package/src/rbac/README.md +20 -20
  220. package/src/rbac/__tests__/adapters.comprehensive.test.tsx +7 -457
  221. package/src/rbac/__tests__/auth-rbac.e2e.test.tsx +33 -7
  222. package/src/rbac/adapters.tsx +7 -295
  223. package/src/rbac/api.test.ts +44 -56
  224. package/src/rbac/api.ts +10 -17
  225. package/src/rbac/cache-invalidation.ts +0 -1
  226. package/src/rbac/compliance/index.ts +10 -0
  227. package/src/rbac/compliance/pattern-detector.ts +553 -0
  228. package/src/rbac/compliance/runtime-compliance.ts +22 -0
  229. package/src/rbac/components/AccessDenied.tsx +150 -0
  230. package/src/rbac/components/NavigationGuard.tsx +12 -20
  231. package/src/rbac/components/PagePermissionGuard.tsx +4 -24
  232. package/src/rbac/components/__tests__/NavigationGuard.test.tsx +21 -8
  233. package/src/rbac/components/index.ts +3 -41
  234. package/src/rbac/eslint-rules.js +1 -1
  235. package/src/rbac/hooks/index.ts +0 -3
  236. package/src/rbac/hooks/permissions/index.ts +0 -3
  237. package/src/rbac/hooks/permissions/useAccessLevel.ts +4 -8
  238. package/src/rbac/hooks/usePermissions.ts +0 -3
  239. package/src/rbac/hooks/useRBAC.test.ts +21 -3
  240. package/src/rbac/hooks/useRBAC.ts +4 -3
  241. package/src/rbac/hooks/useResolvedScope.test.ts +57 -47
  242. package/src/rbac/hooks/useResolvedScope.ts +58 -140
  243. package/src/rbac/hooks/useResourcePermissions.test.ts +241 -60
  244. package/src/rbac/hooks/useResourcePermissions.ts +182 -63
  245. package/src/rbac/hooks/useRoleManagement.test.ts +65 -22
  246. package/src/rbac/hooks/useRoleManagement.ts +147 -19
  247. package/src/rbac/hooks/useSecureSupabase.ts +4 -8
  248. package/src/rbac/index.ts +7 -9
  249. package/src/rbac/permissions.ts +17 -17
  250. package/src/rbac/utils/contextValidator.ts +45 -7
  251. package/src/services/AuthService.ts +132 -23
  252. package/src/services/EventService.ts +4 -97
  253. package/src/services/InactivityService.ts +155 -58
  254. package/src/services/OrganisationService.ts +7 -44
  255. package/src/services/__tests__/OrganisationService.test.ts +26 -8
  256. package/src/services/base/BaseService.ts +0 -3
  257. package/src/styles/core.css +4 -0
  258. package/src/types/database.generated.ts +4733 -3809
  259. package/src/utils/__tests__/organisationContext.unit.test.ts +9 -10
  260. package/src/utils/context/organisationContext.test.ts +13 -28
  261. package/src/utils/context/organisationContext.ts +21 -52
  262. package/src/utils/dynamic/dynamicUtils.ts +1 -1
  263. package/src/utils/file-reference/index.ts +39 -15
  264. package/src/utils/formatting/formatDateTime.test.ts +3 -2
  265. package/src/utils/formatting/formatTime.test.ts +3 -2
  266. package/src/utils/google-places/loadGoogleMapsScript.ts +29 -4
  267. package/src/utils/index.ts +4 -1
  268. package/src/utils/persistence/__tests__/keyDerivation.test.ts +135 -0
  269. package/src/utils/persistence/__tests__/sensitiveFieldDetection.test.ts +123 -0
  270. package/src/utils/persistence/keyDerivation.ts +304 -0
  271. package/src/utils/persistence/sensitiveFieldDetection.ts +212 -0
  272. package/src/utils/security/secureStorage.ts +5 -5
  273. package/src/utils/storage/helpers.ts +3 -3
  274. package/src/utils/supabase/createBaseClient.ts +147 -0
  275. package/src/utils/timezone/timezone.test.ts +1 -2
  276. package/src/utils/timezone/timezone.ts +1 -1
  277. package/src/utils/validation/csrf.ts +4 -4
  278. package/cursor-rules/CHANGELOG.md +0 -119
  279. package/cursor-rules/README.md +0 -192
  280. package/dist/DataTable-E7YQZD7D.js +0 -175
  281. package/dist/DataTable-E7YQZD7D.js.map +0 -1
  282. package/dist/UnifiedAuthProvider-QPXO24B4.js +0 -18
  283. package/dist/UnifiedAuthProvider-QPXO24B4.js.map +0 -1
  284. package/dist/api-6LVZTHDS.js +0 -52
  285. package/dist/api-6LVZTHDS.js.map +0 -1
  286. package/dist/audit-V53FV5AG.js +0 -17
  287. package/dist/audit-V53FV5AG.js.map +0 -1
  288. package/dist/chunk-36LVWXB2.js +0 -227
  289. package/dist/chunk-36LVWXB2.js.map +0 -1
  290. package/dist/chunk-3LPHPB62.js.map +0 -1
  291. package/dist/chunk-5DRSZLL2.js.map +0 -1
  292. package/dist/chunk-5EC5MEWX.js.map +0 -1
  293. package/dist/chunk-63FOKYGO.js.map +0 -1
  294. package/dist/chunk-6SOIHG6Z.js.map +0 -1
  295. package/dist/chunk-7JPAB3T5.js.map +0 -1
  296. package/dist/chunk-ATKZM7RX.js +0 -2053
  297. package/dist/chunk-ATKZM7RX.js.map +0 -1
  298. package/dist/chunk-AVMLPIM7.js.map +0 -1
  299. package/dist/chunk-DGUM43GV.js.map +0 -1
  300. package/dist/chunk-E66EQZE6.js.map +0 -1
  301. package/dist/chunk-FFQEQTNW.js.map +0 -1
  302. package/dist/chunk-FMUCXFII.js.map +0 -1
  303. package/dist/chunk-G37KK66H.js.map +0 -1
  304. package/dist/chunk-I6DAQMWX.js.map +0 -1
  305. package/dist/chunk-J36DSWQK.js.map +0 -1
  306. package/dist/chunk-KQCRWDSA.js +0 -1
  307. package/dist/chunk-KQCRWDSA.js.map +0 -1
  308. package/dist/chunk-L4OXEN46.js.map +0 -1
  309. package/dist/chunk-LMC26NLJ.js +0 -84
  310. package/dist/chunk-LMC26NLJ.js.map +0 -1
  311. package/dist/chunk-M43Y4SSO.js.map +0 -1
  312. package/dist/chunk-M7MPQISP.js.map +0 -1
  313. package/dist/chunk-NN6WWZ5U.js.map +0 -1
  314. package/dist/chunk-OEWDTMG7.js.map +0 -1
  315. package/dist/chunk-PWLANIRT.js.map +0 -1
  316. package/dist/chunk-QXHPKYJV.js.map +0 -1
  317. package/dist/chunk-VBXEHIUJ.js.map +0 -1
  318. package/dist/chunk-YKRAFF5K.js.map +0 -1
  319. package/dist/chunk-ZSAAAMVR.js.map +0 -1
  320. package/dist/components.js.map +0 -1
  321. package/dist/contextValidator-OOPCLPZW.js +0 -9
  322. package/dist/contextValidator-OOPCLPZW.js.map +0 -1
  323. package/dist/eslint-rules/pace-core-compliance.cjs +0 -510
  324. package/dist/hooks.js.map +0 -1
  325. package/dist/index.js.map +0 -1
  326. package/dist/providers.js.map +0 -1
  327. package/dist/rbac/eslint-rules.js.map +0 -1
  328. package/dist/rbac/index.js.map +0 -1
  329. package/dist/styles/index.js.map +0 -1
  330. package/dist/theming/runtime.js.map +0 -1
  331. package/dist/types.js.map +0 -1
  332. package/dist/utils.js.map +0 -1
  333. package/docs/standards/01-architecture-standard.md +0 -44
  334. package/docs/standards/02-api-and-rpc-standard.md +0 -39
  335. package/docs/standards/03-component-standard.md +0 -32
  336. package/docs/standards/05-security-standard.md +0 -44
  337. package/docs/standards/06-testing-and-docs-standard.md +0 -29
  338. package/docs/standards/pace-core-compliance.md +0 -432
  339. package/scripts/audit/core/checks/accessibility.cjs +0 -197
  340. package/scripts/audit/core/checks/api-usage.cjs +0 -191
  341. package/scripts/audit/core/checks/bundle.cjs +0 -142
  342. package/scripts/audit/core/checks/compliance.cjs +0 -2706
  343. package/scripts/audit/core/checks/config.cjs +0 -54
  344. package/scripts/audit/core/checks/coverage.cjs +0 -84
  345. package/scripts/audit/core/checks/dependencies.cjs +0 -994
  346. package/scripts/audit/core/checks/documentation.cjs +0 -268
  347. package/scripts/audit/core/checks/environment.cjs +0 -116
  348. package/scripts/audit/core/checks/error-handling.cjs +0 -340
  349. package/scripts/audit/core/checks/forms.cjs +0 -172
  350. package/scripts/audit/core/checks/heuristics.cjs +0 -68
  351. package/scripts/audit/core/checks/hooks.cjs +0 -334
  352. package/scripts/audit/core/checks/imports.cjs +0 -244
  353. package/scripts/audit/core/checks/performance.cjs +0 -325
  354. package/scripts/audit/core/checks/routes.cjs +0 -117
  355. package/scripts/audit/core/checks/state.cjs +0 -130
  356. package/scripts/audit/core/checks/structure.cjs +0 -65
  357. package/scripts/audit/core/checks/style.cjs +0 -584
  358. package/scripts/audit/core/checks/testing.cjs +0 -122
  359. package/scripts/audit/core/checks/typescript.cjs +0 -61
  360. package/scripts/audit/core/scanner.cjs +0 -199
  361. package/scripts/audit/core/utils.cjs +0 -137
  362. package/scripts/audit/reporters/console.cjs +0 -151
  363. package/scripts/audit/reporters/json.cjs +0 -54
  364. package/scripts/audit/reporters/markdown.cjs +0 -124
  365. package/scripts/audit-consuming-app.cjs +0 -86
  366. package/src/eslint-rules/pace-core-compliance.cjs +0 -510
  367. package/src/eslint-rules/pace-core-compliance.js +0 -638
  368. package/src/rbac/components/EnhancedNavigationMenu.test.tsx +0 -555
  369. package/src/rbac/components/EnhancedNavigationMenu.tsx +0 -293
  370. package/src/rbac/components/NavigationProvider.test.tsx +0 -481
  371. package/src/rbac/components/NavigationProvider.tsx +0 -345
  372. package/src/rbac/components/PagePermissionProvider.test.tsx +0 -476
  373. package/src/rbac/components/PagePermissionProvider.tsx +0 -279
  374. package/src/rbac/components/PermissionEnforcer.tsx +0 -312
  375. package/src/rbac/components/RoleBasedRouter.tsx +0 -440
  376. package/src/rbac/components/SecureDataProvider.test.tsx +0 -543
  377. package/src/rbac/components/SecureDataProvider.tsx +0 -339
  378. package/src/rbac/components/__tests__/EnhancedNavigationMenu.test.tsx +0 -620
  379. package/src/rbac/components/__tests__/NavigationProvider.test.tsx +0 -726
  380. package/src/rbac/components/__tests__/PagePermissionProvider.test.tsx +0 -661
  381. package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +0 -881
  382. package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +0 -783
  383. package/src/rbac/components/__tests__/SecureDataProvider.fixed.test.tsx +0 -645
  384. package/src/rbac/components/__tests__/SecureDataProvider.test.tsx +0 -659
  385. package/src/rbac/hooks/permissions/useCachedPermissions.ts +0 -79
  386. package/src/rbac/hooks/permissions/useHasAllPermissions.ts +0 -90
  387. package/src/rbac/hooks/permissions/useHasAnyPermission.ts +0 -90
@@ -0,0 +1,1295 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Compliance Audit Script
5
+ *
6
+ * Audits consuming apps for compliance with pace-core usage patterns.
7
+ *
8
+ * NOTE: Many checks have been migrated to ESLint rules for real-time feedback:
9
+ * - Native HTML elements → ESLint: prefer-pace-core-components
10
+ * - Restricted imports → ESLint: no-restricted-imports
11
+ * - Custom hooks/utils → ESLint: prefer-pace-core-hooks/utils
12
+ * - Inline styles → ESLint: no-inline-styles
13
+ * - RBAC permission loading → ESLint: rbac-permission-loading
14
+ *
15
+ * This script now focuses on file-system and config-based checks:
16
+ * - Secure Supabase client usage (file location checks)
17
+ * - RBAC setup (main.tsx structure)
18
+ * - Provider usage (cross-file nesting)
19
+ * - Core styles import (app.css → core.css chain)
20
+ *
21
+ * Usage:
22
+ * const { runComplianceAudit } = require('./audit-compliance.cjs');
23
+ * const issues = runComplianceAudit(consumingAppPath);
24
+ */
25
+
26
+ const fs = require('fs');
27
+ const path = require('path');
28
+
29
+ /**
30
+ * Recursively find all TypeScript/JavaScript files in a directory
31
+ */
32
+ function findSourceFiles(dir, fileList = []) {
33
+ if (!fs.existsSync(dir)) {
34
+ return fileList;
35
+ }
36
+
37
+ const files = fs.readdirSync(dir);
38
+
39
+ files.forEach(file => {
40
+ const filePath = path.join(dir, file);
41
+ const stat = fs.statSync(filePath);
42
+
43
+ if (stat.isDirectory()) {
44
+ // Skip node_modules, dist, build, .git, etc.
45
+ if (!['node_modules', 'dist', 'build', '.git', '.next', '.vite', 'coverage', '.turbo'].includes(file)) {
46
+ findSourceFiles(filePath, fileList);
47
+ }
48
+ } else if (/\.(ts|tsx|js|jsx)$/.test(file)) {
49
+ fileList.push(filePath);
50
+ }
51
+ });
52
+
53
+ return fileList;
54
+ }
55
+
56
+ /**
57
+ * Get line number from index in content
58
+ */
59
+ function getLineNumber(content, index) {
60
+ return content.substring(0, index).split('\n').length;
61
+ }
62
+
63
+ /**
64
+ * Get code snippet around a match for context
65
+ */
66
+ function getCodeSnippet(content, index, before = 30, after = 50) {
67
+ const start = Math.max(0, index - before);
68
+ const end = Math.min(content.length, index + after);
69
+ return content.substring(start, end).trim();
70
+ }
71
+
72
+ /**
73
+ * Check if content is in a comment or string
74
+ */
75
+ function isInCommentOrString(content, index) {
76
+ const before = content.substring(0, index);
77
+
78
+ // Check for line comments
79
+ const lastLineComment = before.lastIndexOf('//');
80
+ const lastNewline = before.lastIndexOf('\n');
81
+ if (lastLineComment > lastNewline) {
82
+ return true;
83
+ }
84
+
85
+ // Check for block comments
86
+ const lastBlockCommentStart = before.lastIndexOf('/*');
87
+ const lastBlockCommentEnd = before.lastIndexOf('*/');
88
+ if (lastBlockCommentStart > lastBlockCommentEnd) {
89
+ return true;
90
+ }
91
+
92
+ // Check for string literals (simple check)
93
+ const singleQuoteMatches = [...before.matchAll(/'/g)];
94
+ const doubleQuoteMatches = [...before.matchAll(/"/g)];
95
+ const backtickMatches = [...before.matchAll(/`/g)];
96
+
97
+ // Simple heuristic: if odd number of quotes before, might be in string
98
+ // This is not perfect but good enough for audit purposes
99
+ const inSingleQuote = singleQuoteMatches.length % 2 === 1;
100
+ const inDoubleQuote = doubleQuoteMatches.length % 2 === 1;
101
+ const inBacktick = backtickMatches.length % 2 === 1;
102
+
103
+ return inSingleQuote || inDoubleQuote || inBacktick;
104
+ }
105
+
106
+ /**
107
+ * Load core-usage-manifest.json
108
+ */
109
+ function loadManifest(consumingAppPath) {
110
+ // Try to find manifest in pace-core package
111
+ const possiblePaths = [
112
+ path.resolve(__dirname, '../../core-usage-manifest.json'),
113
+ path.resolve(__dirname, '../../../core-usage-manifest.json'),
114
+ path.join(consumingAppPath, 'node_modules', '@jmruthers', 'pace-core', 'core-usage-manifest.json'),
115
+ ];
116
+
117
+ for (const manifestPath of possiblePaths) {
118
+ if (fs.existsSync(manifestPath)) {
119
+ try {
120
+ return JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
121
+ } catch (e) {
122
+ // Continue to next path
123
+ }
124
+ }
125
+ }
126
+
127
+ return null;
128
+ }
129
+
130
+ /**
131
+ * Check if file imports a specific component/hook/util from pace-core
132
+ */
133
+ function importsFromPaceCore(content, name) {
134
+ // Check for imports like:
135
+ // import { Name } from '@jmruthers/pace-core'
136
+ // import { Name } from '@jmruthers/pace-core/components'
137
+ // import Name from '@jmruthers/pace-core'
138
+ const patterns = [
139
+ new RegExp(`import\\s+.*\\b${name}\\b.*from\\s+['"]@jmruthers/pace-core`),
140
+ new RegExp(`import\\s+.*\\b${name}\\b.*from\\s+['"]@jmruthers/pace-core/components`),
141
+ new RegExp(`import\\s+.*\\b${name}\\b.*from\\s+['"]@jmruthers/pace-core/hooks`),
142
+ new RegExp(`import\\s+.*\\b${name}\\b.*from\\s+['"]@jmruthers/pace-core/utils`),
143
+ ];
144
+
145
+ return patterns.some(pattern => pattern.test(content));
146
+ }
147
+
148
+ /**
149
+ * Check for native HTML elements when pace-core components exist
150
+ *
151
+ * MIGRATED TO ESLINT: This check is now handled by 'prefer-pace-core-components' ESLint rule.
152
+ * Kept for reference only.
153
+ */
154
+ function checkNativeElements_MIGRATED_TO_ESLINT(content, filePath, consumingAppPath, manifest) {
155
+ const issues = [];
156
+
157
+ // Map of native elements to pace-core components
158
+ const elementMap = {
159
+ 'button': 'Button',
160
+ 'input': 'Input',
161
+ 'label': 'Label',
162
+ 'textarea': 'Textarea',
163
+ };
164
+
165
+ Object.entries(elementMap).forEach(([nativeElement, paceCoreComponent]) => {
166
+ // Pattern to match <element> tags (lowercase only - not <Element> component)
167
+ // Case-sensitive regex (no 'i' flag) ensures we only match native HTML, not pace-core components
168
+ const pattern = new RegExp(`<${nativeElement}(\\s|>|/|\\n)`, 'g');
169
+ let match;
170
+
171
+ while ((match = pattern.exec(content)) !== null) {
172
+ if (isInCommentOrString(content, match.index)) {
173
+ continue;
174
+ }
175
+
176
+ // Check if this file imports the pace-core component
177
+ if (importsFromPaceCore(content, paceCoreComponent)) {
178
+ // They have the import but still using native element - might be intentional
179
+ // Flag it anyway as it should use the component
180
+ const relativePath = path.relative(consumingAppPath || process.cwd(), filePath);
181
+ issues.push({
182
+ type: 'nativeElement',
183
+ file: relativePath,
184
+ line: getLineNumber(content, match.index),
185
+ message: `Native <${nativeElement}> element detected. Use pace-core ${paceCoreComponent} component instead.`,
186
+ code: getCodeSnippet(content, match.index),
187
+ severity: 'error',
188
+ fix: `Replace <${nativeElement}> with <${paceCoreComponent}> from '@jmruthers/pace-core'`,
189
+ });
190
+ } else {
191
+ // No import, definitely should use pace-core component
192
+ const relativePath = path.relative(consumingAppPath || process.cwd(), filePath);
193
+ issues.push({
194
+ type: 'nativeElement',
195
+ file: relativePath,
196
+ line: getLineNumber(content, match.index),
197
+ message: `Native <${nativeElement}> element detected. Use pace-core ${paceCoreComponent} component instead.`,
198
+ code: getCodeSnippet(content, match.index),
199
+ severity: 'error',
200
+ fix: `Import and use ${paceCoreComponent} from '@jmruthers/pace-core'`,
201
+ });
202
+ }
203
+ }
204
+ });
205
+
206
+ return issues;
207
+ }
208
+
209
+ /**
210
+ * Check for restricted library imports
211
+ *
212
+ * MIGRATED TO ESLINT: This check is now handled by 'no-restricted-imports' ESLint rule.
213
+ * Kept for reference only.
214
+ */
215
+ function checkRestrictedImports_MIGRATED_TO_ESLINT(content, filePath, consumingAppPath, manifest) {
216
+ const issues = [];
217
+
218
+ if (!manifest || !manifest.restrictedImports) {
219
+ return issues;
220
+ }
221
+
222
+ manifest.restrictedImports.forEach(restriction => {
223
+ const moduleName = restriction.module;
224
+
225
+ // Skip react-hook-form - it's handled by audit-components.cjs with more nuanced logic
226
+ // (allows useFormContext when Form is imported from pace-core)
227
+ if (moduleName === 'react-hook-form') {
228
+ return;
229
+ }
230
+
231
+ // Pattern to match imports from restricted module
232
+ const importPattern = new RegExp(`from\\s+['"]${moduleName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}['"]`, 'g');
233
+ let match;
234
+
235
+ while ((match = importPattern.exec(content)) !== null) {
236
+ if (isInCommentOrString(content, match.index)) {
237
+ continue;
238
+ }
239
+
240
+ // Get the import line
241
+ const lineStart = content.lastIndexOf('\n', match.index) + 1;
242
+ const lineEnd = content.indexOf('\n', match.index);
243
+ const importLine = content.substring(lineStart, lineEnd === -1 ? content.length : lineEnd);
244
+
245
+ const relativePath = path.relative(consumingAppPath || process.cwd(), filePath);
246
+
247
+ let fix = restriction.reason;
248
+ if (restriction.importExample) {
249
+ fix += `\n Example: ${restriction.importExample}`;
250
+ }
251
+ if (restriction.usageExample) {
252
+ fix += `\n Usage: ${restriction.usageExample}`;
253
+ }
254
+
255
+ issues.push({
256
+ type: 'restrictedImport',
257
+ file: relativePath,
258
+ line: getLineNumber(content, match.index),
259
+ message: `Direct import from '${moduleName}' detected. ${restriction.reason}`,
260
+ code: importLine.trim(),
261
+ severity: 'error',
262
+ fix: fix,
263
+ module: moduleName,
264
+ });
265
+ }
266
+ });
267
+
268
+ return issues;
269
+ }
270
+
271
+ /**
272
+ * Check for custom hooks when pace-core provides them
273
+ *
274
+ * MIGRATED TO ESLINT: This check is now handled by 'prefer-pace-core-hooks' ESLint rule.
275
+ * Kept for reference only.
276
+ */
277
+ function checkCustomHooks_MIGRATED_TO_ESLINT(content, filePath, consumingAppPath, manifest) {
278
+ const issues = [];
279
+
280
+ if (!manifest || !manifest.hooks) {
281
+ return issues;
282
+ }
283
+
284
+ // Common custom hook patterns that duplicate pace-core functionality
285
+ const problematicHooks = {
286
+ 'useAuth': 'useUnifiedAuth',
287
+ 'useToast': 'useToast',
288
+ 'useDebounce': 'useDebounce',
289
+ 'useForm': 'useZodForm',
290
+ };
291
+
292
+ Object.entries(problematicHooks).forEach(([customHook, paceCoreHook]) => {
293
+ // Pattern to match hook definitions
294
+ const patterns = [
295
+ new RegExp(`(function|const)\\s+${customHook}\\s*[=(]`, 'g'),
296
+ new RegExp(`export\\s+(function|const)\\s+${customHook}\\s*[=(]`, 'g'),
297
+ ];
298
+
299
+ patterns.forEach(pattern => {
300
+ let match;
301
+ while ((match = pattern.exec(content)) !== null) {
302
+ if (isInCommentOrString(content, match.index)) {
303
+ continue;
304
+ }
305
+
306
+ // Check if pace-core hook is imported
307
+ if (importsFromPaceCore(content, paceCoreHook)) {
308
+ // They have the import but also defined custom hook - likely duplicate
309
+ const relativePath = path.relative(consumingAppPath || process.cwd(), filePath);
310
+ issues.push({
311
+ type: 'customHook',
312
+ file: relativePath,
313
+ line: getLineNumber(content, match.index),
314
+ message: `Custom ${customHook} hook detected. Use pace-core ${paceCoreHook} hook instead.`,
315
+ code: getCodeSnippet(content, match.index),
316
+ severity: 'warning',
317
+ fix: `Remove custom ${customHook} and use ${paceCoreHook} from '@jmruthers/pace-core'`,
318
+ });
319
+ } else {
320
+ // No import, might be custom implementation
321
+ const relativePath = path.relative(consumingAppPath || process.cwd(), filePath);
322
+ issues.push({
323
+ type: 'customHook',
324
+ file: relativePath,
325
+ line: getLineNumber(content, match.index),
326
+ message: `Custom ${customHook} hook detected. Consider using pace-core ${paceCoreHook} hook instead.`,
327
+ code: getCodeSnippet(content, match.index),
328
+ severity: 'warning',
329
+ fix: `Use ${paceCoreHook} from '@jmruthers/pace-core' instead of custom implementation`,
330
+ });
331
+ }
332
+ }
333
+ });
334
+ });
335
+
336
+ return issues;
337
+ }
338
+
339
+ /**
340
+ * Check for custom utilities when pace-core provides them
341
+ *
342
+ * MIGRATED TO ESLINT: This check is now handled by 'prefer-pace-core-utils' ESLint rule.
343
+ * Kept for reference only.
344
+ */
345
+ function checkCustomUtils_MIGRATED_TO_ESLINT(content, filePath, consumingAppPath, manifest) {
346
+ const issues = [];
347
+
348
+ if (!manifest || !manifest.utils) {
349
+ return issues;
350
+ }
351
+
352
+ // Common utility patterns that duplicate pace-core functionality
353
+ const problematicUtils = {
354
+ 'formatDate': 'formatDate',
355
+ 'formatTime': 'formatTime',
356
+ 'formatDateTime': 'formatDateTime',
357
+ 'formatCurrency': 'formatCurrency',
358
+ 'formatNumber': 'formatNumber',
359
+ 'cn': 'cn',
360
+ };
361
+
362
+ Object.entries(problematicUtils).forEach(([customUtil, paceCoreUtil]) => {
363
+ // Pattern to match function definitions
364
+ const patterns = [
365
+ new RegExp(`(function|const|export\\s+(function|const))\\s+${customUtil}\\s*[=(]`, 'g'),
366
+ ];
367
+
368
+ patterns.forEach(pattern => {
369
+ let match;
370
+ while ((match = pattern.exec(content)) !== null) {
371
+ if (isInCommentOrString(content, match.index)) {
372
+ continue;
373
+ }
374
+
375
+ // Check if pace-core util is imported
376
+ if (importsFromPaceCore(content, paceCoreUtil)) {
377
+ // They have the import but also defined custom util - likely duplicate
378
+ const relativePath = path.relative(consumingAppPath || process.cwd(), filePath);
379
+ issues.push({
380
+ type: 'customUtil',
381
+ file: relativePath,
382
+ line: getLineNumber(content, match.index),
383
+ message: `Custom ${customUtil} utility detected. Use pace-core ${paceCoreUtil} utility instead.`,
384
+ code: getCodeSnippet(content, match.index),
385
+ severity: 'warning',
386
+ fix: `Remove custom ${customUtil} and use ${paceCoreUtil} from '@jmruthers/pace-core'`,
387
+ });
388
+ } else {
389
+ // No import, might be custom implementation
390
+ const relativePath = path.relative(consumingAppPath || process.cwd(), filePath);
391
+ issues.push({
392
+ type: 'customUtil',
393
+ file: relativePath,
394
+ line: getLineNumber(content, match.index),
395
+ message: `Custom ${customUtil} utility detected. Consider using pace-core ${paceCoreUtil} utility instead.`,
396
+ code: getCodeSnippet(content, match.index),
397
+ severity: 'warning',
398
+ fix: `Use ${paceCoreUtil} from '@jmruthers/pace-core' instead of custom implementation`,
399
+ });
400
+ }
401
+ }
402
+ });
403
+ });
404
+
405
+ return issues;
406
+ }
407
+
408
+ /**
409
+ * Check for secure Supabase client usage
410
+ */
411
+ function checkSupabaseUsage(content, filePath, consumingAppPath) {
412
+ const issues = [];
413
+ const relativePath = path.relative(consumingAppPath || process.cwd(), filePath);
414
+
415
+ // Check for createClient() calls
416
+ const createClientPattern = /createClient\s*\(/g;
417
+ let match;
418
+
419
+ while ((match = createClientPattern.exec(content)) !== null) {
420
+ if (isInCommentOrString(content, match.index)) {
421
+ continue;
422
+ }
423
+
424
+ // Check if this is from @supabase/supabase-js
425
+ const beforeMatch = content.substring(Math.max(0, match.index - 200), match.index);
426
+ const hasSupabaseImport = /from\s+['"]@supabase\/supabase-js['"]/.test(beforeMatch);
427
+
428
+ if (hasSupabaseImport) {
429
+ // Check if file is in allowed location
430
+ const allowedPatterns = [
431
+ /[\/\\]main\.(tsx?|jsx?)$/i,
432
+ /[\/\\]App\.(tsx?|jsx?)$/i,
433
+ /[\/\\]lib[\/\\]supabase\.(tsx?|jsx?)$/i,
434
+ /[\/\\]src[\/\\]supabase\.(tsx?|jsx?)$/i,
435
+ ];
436
+
437
+ const isAllowed = allowedPatterns.some(pattern => pattern.test(filePath));
438
+
439
+ if (!isAllowed) {
440
+ issues.push({
441
+ type: 'supabaseClient',
442
+ file: relativePath,
443
+ line: getLineNumber(content, match.index),
444
+ message: 'createClient() call detected in unauthorized location. Should only be in main.tsx, App.tsx, or lib/supabase.ts',
445
+ code: getCodeSnippet(content, match.index),
446
+ severity: 'error',
447
+ fix: 'Move createClient() call to main.tsx, App.tsx, or lib/supabase.ts. Use useSecureSupabase() hook for queries.',
448
+ });
449
+ }
450
+ }
451
+ }
452
+
453
+ // Check for .from(), .rpc(), .auth. calls on potentially insecure clients
454
+ // This is a heuristic - we look for these patterns and check if they're on a variable
455
+ // that might be an insecure client
456
+ const queryPatterns = [
457
+ /\.from\s*\(/g,
458
+ /\.rpc\s*\(/g,
459
+ /\.auth\./g,
460
+ ];
461
+
462
+ queryPatterns.forEach(pattern => {
463
+ let match;
464
+ while ((match = pattern.exec(content)) !== null) {
465
+ if (isInCommentOrString(content, match.index)) {
466
+ continue;
467
+ }
468
+
469
+ // Check if this is on useSecureSupabase() result
470
+ const beforeMatch = content.substring(Math.max(0, match.index - 100), match.index);
471
+ const hasSecureSupabase = /useSecureSupabase\s*\(/.test(beforeMatch) ||
472
+ /secureSupabase/.test(beforeMatch) ||
473
+ /supabase\s*=\s*useSecureSupabase/.test(beforeMatch);
474
+
475
+ if (!hasSecureSupabase) {
476
+ // Might be insecure - check context
477
+ const contextBefore = content.substring(Math.max(0, match.index - 200), match.index);
478
+ const hasCreateClient = /createClient\s*\(/.test(contextBefore);
479
+
480
+ if (hasCreateClient) {
481
+ issues.push({
482
+ type: 'supabaseClient',
483
+ file: relativePath,
484
+ line: getLineNumber(content, match.index),
485
+ message: 'Database query detected on potentially insecure client. Use useSecureSupabase() hook instead.',
486
+ code: getCodeSnippet(content, match.index),
487
+ severity: 'error',
488
+ fix: 'Use useSecureSupabase() hook from @jmruthers/pace-core/rbac for all database queries',
489
+ });
490
+ }
491
+ }
492
+ }
493
+ });
494
+
495
+ // Check for useSecureSupabase() called with parameters (should be called without)
496
+ const useSecureSupabasePattern = /useSecureSupabase\s*\(\s*[^)]/g;
497
+ let secureMatch;
498
+ while ((secureMatch = useSecureSupabasePattern.exec(content)) !== null) {
499
+ if (isInCommentOrString(content, secureMatch.index)) {
500
+ continue;
501
+ }
502
+
503
+ // Check if parameter is a base client (acceptable exception)
504
+ const afterMatch = content.substring(secureMatch.index, secureMatch.index + 100);
505
+ const hasBaseClientParam = /useSecureSupabase\s*\(\s*(supabase|baseClient|client)/.test(afterMatch);
506
+
507
+ if (!hasBaseClientParam) {
508
+ // Might be an issue, but base client as parameter is acceptable
509
+ // This is a warning, not an error
510
+ issues.push({
511
+ type: 'supabaseClient',
512
+ file: relativePath,
513
+ line: getLineNumber(content, secureMatch.index),
514
+ message: 'useSecureSupabase() called with parameters. Should be called without parameters (gets client from provider automatically).',
515
+ code: getCodeSnippet(content, secureMatch.index),
516
+ severity: 'warning',
517
+ fix: 'Call useSecureSupabase() without parameters. The hook automatically gets the client from UnifiedAuthProvider.',
518
+ });
519
+ }
520
+ }
521
+
522
+ return issues;
523
+ }
524
+
525
+ /**
526
+ * Check for RBAC permission usage patterns
527
+ * Detects cases where permission checks are called without waiting for loading state
528
+ *
529
+ * MIGRATED TO ESLINT: This check is now handled by 'rbac-permission-loading' ESLint rule.
530
+ * Kept for reference only.
531
+ */
532
+ function checkRBACPermissionUsage_MIGRATED_TO_ESLINT(content, filePath, consumingAppPath) {
533
+ const issues = [];
534
+ const relativePath = path.relative(consumingAppPath || process.cwd(), filePath);
535
+
536
+ // Check if file uses useResourcePermissions
537
+ const usesResourcePermissions = /useResourcePermissions\s*\(/i.test(content);
538
+ if (!usesResourcePermissions) {
539
+ return issues;
540
+ }
541
+
542
+ // Find useResourcePermissions calls and check if isLoading is extracted
543
+ // Pattern to match destructuring assignments with useResourcePermissions
544
+ const resourcePermissionsPattern = /(const|let|var)\s*\{[^}]*useResourcePermissions\s*\([^)]*\)[^}]*\}/g;
545
+ let match;
546
+
547
+ while ((match = resourcePermissionsPattern.exec(content)) !== null) {
548
+ if (isInCommentOrString(content, match.index)) {
549
+ continue;
550
+ }
551
+
552
+ const destructuringBlock = match[0];
553
+ const matchIndex = match.index;
554
+
555
+ // Check if isLoading is extracted from useResourcePermissions
556
+ // Match: isLoading, isLoading: permissionsLoading, isLoading: loading, etc.
557
+ const hasIsLoading = /\bisLoading\s*(?::\s*\w+)?\s*[,}]/i.test(destructuringBlock);
558
+
559
+ // Find permission function calls (canCreate, canUpdate, canDelete) in the file
560
+ // Look for calls that might not check loading state first
561
+ const permissionCallPattern = /\b(canCreate|canUpdate|canDelete)\s*\(/g;
562
+ let permMatch;
563
+
564
+ while ((permMatch = permissionCallPattern.exec(content)) !== null) {
565
+ if (isInCommentOrString(content, permMatch.index)) {
566
+ continue;
567
+ }
568
+
569
+ // Find the function/block that contains this permission call
570
+ // Look backwards to find the start of the function/block
571
+ const beforeCall = content.substring(0, permMatch.index);
572
+ const functionStart = Math.max(
573
+ beforeCall.lastIndexOf('function'),
574
+ beforeCall.lastIndexOf('const'),
575
+ beforeCall.lastIndexOf('=>'),
576
+ beforeCall.lastIndexOf('async')
577
+ );
578
+
579
+ if (functionStart === -1) continue;
580
+
581
+ // Get the context around the permission call
582
+ const contextStart = Math.max(0, functionStart);
583
+ const contextEnd = Math.min(content.length, permMatch.index + 200);
584
+ const context = content.substring(contextStart, contextEnd);
585
+
586
+ // Check if isLoading is checked before this permission call in the same context
587
+ const isLoadingCheckedBefore = new RegExp(
588
+ `(if\\s*\\(\\s*!?\\s*isLoading|if\\s*\\(\\s*isLoading\\s*===\\s*false|if\\s*\\(\\s*isLoading\\s*!==\\s*true|await|permissionsLoading)`,
589
+ 'i'
590
+ ).test(context.substring(0, permMatch.index - contextStart));
591
+
592
+ // Check if this is inside a mutation function (where we need to check loading)
593
+ const isInMutation = /mutationFn\s*[:=]\s*async|mutateAsync|mutation\s*[:=]/i.test(context);
594
+
595
+ // If isLoading is not extracted, flag it
596
+ if (!hasIsLoading && isInMutation) {
597
+ issues.push({
598
+ type: 'rbacPermission',
599
+ file: relativePath,
600
+ line: getLineNumber(content, matchIndex),
601
+ message: `useResourcePermissions is used but 'isLoading' is not extracted. Permission checks may fail if scope resolution is still in progress.`,
602
+ code: getCodeSnippet(content, matchIndex, 0, 100),
603
+ severity: 'error',
604
+ fix: `Extract 'isLoading' from useResourcePermissions: const { canCreate, canUpdate, canDelete, isLoading: permissionsLoading } = useResourcePermissions(...);`,
605
+ });
606
+ }
607
+
608
+ // If isLoading is extracted but not checked before permission call in mutation
609
+ if (hasIsLoading && isInMutation && !isLoadingCheckedBefore) {
610
+ const permLine = getLineNumber(content, permMatch.index);
611
+ issues.push({
612
+ type: 'rbacPermission',
613
+ file: relativePath,
614
+ line: permLine,
615
+ message: `Permission check '${permMatch[1]}()' is called without checking isLoading first. This can cause false negatives when scope resolution is still in progress.`,
616
+ code: getCodeSnippet(content, permMatch.index, 20, 50),
617
+ severity: 'error',
618
+ fix: `Check isLoading before calling permission function: if (permissionsLoading) { throw new Error("Permission check in progress. Please wait..."); } if (!canCreate(resource)) { ... }`,
619
+ });
620
+ }
621
+ }
622
+ }
623
+
624
+ return issues;
625
+ }
626
+
627
+ /**
628
+ * Check for setupRBAC() call in main.tsx
629
+ */
630
+ function checkRBACSetup(consumingAppPath) {
631
+ const issues = [];
632
+
633
+ // Check main.tsx or main.jsx
634
+ const mainFiles = [
635
+ path.join(consumingAppPath, 'src', 'main.tsx'),
636
+ path.join(consumingAppPath, 'src', 'main.ts'),
637
+ path.join(consumingAppPath, 'src', 'main.jsx'),
638
+ path.join(consumingAppPath, 'src', 'main.js'),
639
+ path.join(consumingAppPath, 'main.tsx'),
640
+ path.join(consumingAppPath, 'main.ts'),
641
+ path.join(consumingAppPath, 'main.jsx'),
642
+ path.join(consumingAppPath, 'main.js'),
643
+ ];
644
+
645
+ let mainFile = null;
646
+ for (const file of mainFiles) {
647
+ if (fs.existsSync(file)) {
648
+ mainFile = file;
649
+ break;
650
+ }
651
+ }
652
+
653
+ if (!mainFile) {
654
+ issues.push({
655
+ type: 'rbacSetup',
656
+ file: 'src/main.tsx (not found)',
657
+ line: 0,
658
+ message: 'main.tsx file not found. setupRBAC() must be called in main.tsx before app rendering.',
659
+ code: '',
660
+ severity: 'error',
661
+ fix: 'Create src/main.tsx and call setupRBAC(supabase) before rendering the app.',
662
+ });
663
+ return issues;
664
+ }
665
+
666
+ const content = fs.readFileSync(mainFile, 'utf8');
667
+ const relativePath = path.relative(consumingAppPath || process.cwd(), mainFile);
668
+
669
+ // Check for setupRBAC call
670
+ const setupRBACPattern = /setupRBAC\s*\(/;
671
+ if (!setupRBACPattern.test(content)) {
672
+ issues.push({
673
+ type: 'rbacSetup',
674
+ file: relativePath,
675
+ line: 1,
676
+ message: 'setupRBAC() call not found. Must be called in main.tsx before app rendering.',
677
+ code: '',
678
+ severity: 'error',
679
+ fix: 'Add: import { setupRBAC } from \'@jmruthers/pace-core/rbac\'; setupRBAC(supabase);',
680
+ });
681
+ } else {
682
+ // Check if it's called before React rendering
683
+ const setupRBACIndex = content.indexOf('setupRBAC');
684
+ const renderIndex = content.search(/(createRoot|render)\s*\(/);
685
+
686
+ if (renderIndex !== -1 && setupRBACIndex > renderIndex) {
687
+ issues.push({
688
+ type: 'rbacSetup',
689
+ file: relativePath,
690
+ line: getLineNumber(content, setupRBACIndex),
691
+ message: 'setupRBAC() called after React rendering. Must be called before rendering.',
692
+ code: getCodeSnippet(content, setupRBACIndex),
693
+ severity: 'error',
694
+ fix: 'Move setupRBAC() call before createRoot() or render() call.',
695
+ });
696
+ }
697
+ }
698
+
699
+ return issues;
700
+ }
701
+
702
+ /**
703
+ * Check for required providers
704
+ * Providers can be in:
705
+ * - main.tsx or App.tsx (direct)
706
+ * - A provider file (e.g., AppProviders, providers.tsx)
707
+ * - A component imported in App.tsx that wraps the app
708
+ */
709
+ function checkProviders(consumingAppPath) {
710
+ const issues = [];
711
+
712
+ // First, check main.tsx or App.tsx
713
+ const checkFiles = [
714
+ path.join(consumingAppPath, 'src', 'main.tsx'),
715
+ path.join(consumingAppPath, 'src', 'main.ts'),
716
+ path.join(consumingAppPath, 'src', 'main.jsx'),
717
+ path.join(consumingAppPath, 'src', 'main.js'),
718
+ path.join(consumingAppPath, 'src', 'App.tsx'),
719
+ path.join(consumingAppPath, 'src', 'App.ts'),
720
+ path.join(consumingAppPath, 'src', 'App.jsx'),
721
+ path.join(consumingAppPath, 'src', 'App.js'),
722
+ ];
723
+
724
+ let foundFile = null;
725
+ let mainContent = '';
726
+ let appContent = '';
727
+
728
+ // Read main.tsx if it exists
729
+ const mainFile = checkFiles.find(f => fs.existsSync(f));
730
+ if (mainFile) {
731
+ foundFile = mainFile;
732
+ mainContent = fs.readFileSync(mainFile, 'utf8');
733
+ }
734
+
735
+ // Read App.tsx if it exists
736
+ const appFile = checkFiles.slice(4).find(f => fs.existsSync(f));
737
+ if (appFile) {
738
+ appContent = fs.readFileSync(appFile, 'utf8');
739
+ }
740
+
741
+ if (!foundFile && !appFile) {
742
+ issues.push({
743
+ type: 'providers',
744
+ file: 'src/main.tsx or src/App.tsx (not found)',
745
+ line: 0,
746
+ message: 'main.tsx or App.tsx not found. Required providers must wrap the app.',
747
+ code: '',
748
+ severity: 'error',
749
+ fix: 'Create src/main.tsx and wrap app with UnifiedAuthProvider and OrganisationProvider.',
750
+ });
751
+ return issues;
752
+ }
753
+
754
+ // Combine content from both files for checking
755
+ const combinedContent = mainContent + '\n' + appContent;
756
+
757
+ // Check for UnifiedAuthProvider in main.tsx or App.tsx
758
+ let hasUnifiedAuthProvider = /<UnifiedAuthProvider/.test(combinedContent) ||
759
+ /UnifiedAuthProvider\s*</.test(combinedContent);
760
+
761
+ // Check for OrganisationProvider in main.tsx or App.tsx
762
+ let hasOrganisationProvider = /<OrganisationProvider/.test(combinedContent) ||
763
+ /OrganisationProvider\s*</.test(combinedContent);
764
+
765
+ // If providers not found in main.tsx/App.tsx, check for provider files
766
+ if (!hasUnifiedAuthProvider || !hasOrganisationProvider) {
767
+ // Look for provider files that might contain the providers
768
+ const providerFilePatterns = [
769
+ '**/providers/**/*.{ts,tsx,js,jsx}',
770
+ '**/app/providers/**/*.{ts,tsx,js,jsx}',
771
+ '**/AppProviders.{ts,tsx,js,jsx}',
772
+ '**/app-providers.{ts,tsx,js,jsx}',
773
+ ];
774
+
775
+ // Check common provider file locations
776
+ const providerFiles = [
777
+ path.join(consumingAppPath, 'src', 'app', 'providers', 'app-providers.tsx'),
778
+ path.join(consumingAppPath, 'src', 'app', 'providers', 'app-providers.ts'),
779
+ path.join(consumingAppPath, 'src', 'app', 'providers', 'AppProviders.tsx'),
780
+ path.join(consumingAppPath, 'src', 'app', 'providers', 'AppProviders.ts'),
781
+ path.join(consumingAppPath, 'src', 'providers', 'AppProviders.tsx'),
782
+ path.join(consumingAppPath, 'src', 'providers', 'AppProviders.ts'),
783
+ path.join(consumingAppPath, 'src', 'providers', 'app-providers.tsx'),
784
+ path.join(consumingAppPath, 'src', 'providers', 'app-providers.ts'),
785
+ ];
786
+
787
+ for (const providerFile of providerFiles) {
788
+ if (fs.existsSync(providerFile)) {
789
+ try {
790
+ const providerContent = fs.readFileSync(providerFile, 'utf8');
791
+
792
+ // Check for providers in this file
793
+ if (!hasUnifiedAuthProvider && /<UnifiedAuthProvider/.test(providerContent)) {
794
+ hasUnifiedAuthProvider = true;
795
+ }
796
+
797
+ if (!hasOrganisationProvider && /<OrganisationProvider/.test(providerContent)) {
798
+ hasOrganisationProvider = true;
799
+ }
800
+
801
+ // If we found both, we can stop looking
802
+ if (hasUnifiedAuthProvider && hasOrganisationProvider) {
803
+ break;
804
+ }
805
+ } catch (e) {
806
+ // Skip files that can't be read
807
+ }
808
+ }
809
+ }
810
+
811
+ // Also check if App.tsx imports a provider component
812
+ if (appContent) {
813
+ // Look for imports like: import { AppProviders } from '@/app/providers/app-providers';
814
+ const providerImportPattern = /import\s+.*\b(AppProviders|app-providers|Providers)\b.*from\s+['"]([^'"]+)['"]/;
815
+ const providerImportMatch = appContent.match(providerImportPattern);
816
+
817
+ if (providerImportMatch) {
818
+ // Try to find the imported file
819
+ const importPath = providerImportMatch[2];
820
+ // Handle path aliases (@/app/providers/app-providers)
821
+ const resolvedPath = importPath.startsWith('@/')
822
+ ? path.join(consumingAppPath, 'src', importPath.slice(2))
823
+ : path.join(path.dirname(appFile), importPath);
824
+
825
+ // Try with different extensions
826
+ const possiblePaths = [
827
+ resolvedPath,
828
+ resolvedPath + '.tsx',
829
+ resolvedPath + '.ts',
830
+ resolvedPath + '.jsx',
831
+ resolvedPath + '.js',
832
+ ];
833
+
834
+ for (const possiblePath of possiblePaths) {
835
+ if (fs.existsSync(possiblePath)) {
836
+ try {
837
+ const providerContent = fs.readFileSync(possiblePath, 'utf8');
838
+
839
+ if (!hasUnifiedAuthProvider && /<UnifiedAuthProvider/.test(providerContent)) {
840
+ hasUnifiedAuthProvider = true;
841
+ }
842
+
843
+ if (!hasOrganisationProvider && /<OrganisationProvider/.test(providerContent)) {
844
+ hasOrganisationProvider = true;
845
+ }
846
+
847
+ break;
848
+ } catch (e) {
849
+ // Skip files that can't be read
850
+ }
851
+ }
852
+ }
853
+ }
854
+ }
855
+ }
856
+
857
+ const relativePath = foundFile ? path.relative(consumingAppPath || process.cwd(), foundFile) : 'src/App.tsx';
858
+
859
+ // Check for UnifiedAuthProvider
860
+ if (!hasUnifiedAuthProvider) {
861
+ issues.push({
862
+ type: 'providers',
863
+ file: relativePath,
864
+ line: 1,
865
+ message: 'UnifiedAuthProvider not found. Must wrap the app (can be in main.tsx, App.tsx, or a provider file).',
866
+ code: '',
867
+ severity: 'error',
868
+ fix: 'Import and wrap app with UnifiedAuthProvider from @jmruthers/pace-core',
869
+ });
870
+ }
871
+
872
+ // Check for OrganisationProvider
873
+ if (!hasOrganisationProvider) {
874
+ issues.push({
875
+ type: 'providers',
876
+ file: relativePath,
877
+ line: 1,
878
+ message: 'OrganisationProvider not found. Must wrap the app (inside UnifiedAuthProvider, can be in main.tsx, App.tsx, or a provider file).',
879
+ code: '',
880
+ severity: 'error',
881
+ fix: 'Import and wrap app with OrganisationProvider from @jmruthers/pace-core (inside UnifiedAuthProvider)',
882
+ });
883
+ }
884
+
885
+ // Check nesting order
886
+ // Find JSX opening tags (not in import/export statements)
887
+ if (hasUnifiedAuthProvider && hasOrganisationProvider) {
888
+ // Find the file that contains the providers for nesting check
889
+ let nestingCheckContent = combinedContent;
890
+ let nestingCheckFile = relativePath;
891
+
892
+ // If providers weren't found in main.tsx/App.tsx, find the provider file
893
+ if (!/<UnifiedAuthProvider/.test(combinedContent) || !/<OrganisationProvider/.test(combinedContent)) {
894
+ // Look for provider files
895
+ const providerFiles = [
896
+ path.join(consumingAppPath, 'src', 'app', 'providers', 'app-providers.tsx'),
897
+ path.join(consumingAppPath, 'src', 'app', 'providers', 'app-providers.ts'),
898
+ path.join(consumingAppPath, 'src', 'app', 'providers', 'AppProviders.tsx'),
899
+ path.join(consumingAppPath, 'src', 'app', 'providers', 'AppProviders.ts'),
900
+ path.join(consumingAppPath, 'src', 'providers', 'AppProviders.tsx'),
901
+ path.join(consumingAppPath, 'src', 'providers', 'AppProviders.ts'),
902
+ path.join(consumingAppPath, 'src', 'providers', 'app-providers.tsx'),
903
+ path.join(consumingAppPath, 'src', 'providers', 'app-providers.ts'),
904
+ ];
905
+
906
+ for (const providerFile of providerFiles) {
907
+ if (fs.existsSync(providerFile)) {
908
+ try {
909
+ const providerContent = fs.readFileSync(providerFile, 'utf8');
910
+ if (/<UnifiedAuthProvider/.test(providerContent) && /<OrganisationProvider/.test(providerContent)) {
911
+ nestingCheckContent = providerContent;
912
+ nestingCheckFile = path.relative(consumingAppPath || process.cwd(), providerFile);
913
+ break;
914
+ }
915
+ } catch (e) {
916
+ // Skip files that can't be read
917
+ }
918
+ }
919
+ }
920
+ }
921
+
922
+ // Find all occurrences of opening tags
923
+ const unifiedAuthMatches = [];
924
+ const organisationMatches = [];
925
+
926
+ // Find <UnifiedAuthProvider opening tags (not in import statements)
927
+ const unifiedAuthPattern = /<UnifiedAuthProvider\s*(?:[^>]*>|>)/g;
928
+ let match;
929
+ while ((match = unifiedAuthPattern.exec(nestingCheckContent)) !== null) {
930
+ // Check if this is in an import/export statement
931
+ const beforeMatch = nestingCheckContent.substring(Math.max(0, match.index - 100), match.index);
932
+ const isInImport = /import\s+.*from\s+['"]/.test(beforeMatch) ||
933
+ /export\s+.*from\s+['"]/.test(beforeMatch);
934
+ if (!isInImport) {
935
+ unifiedAuthMatches.push(match.index);
936
+ }
937
+ }
938
+
939
+ // Find <OrganisationProvider opening tags (not in import statements)
940
+ const organisationPattern = /<OrganisationProvider\s*(?:[^>]*>|>)/g;
941
+ while ((match = organisationPattern.exec(nestingCheckContent)) !== null) {
942
+ // Check if this is in an import/export statement
943
+ const beforeMatch = nestingCheckContent.substring(Math.max(0, match.index - 100), match.index);
944
+ const isInImport = /import\s+.*from\s+['"]/.test(beforeMatch) ||
945
+ /export\s+.*from\s+['"]/.test(beforeMatch);
946
+ if (!isInImport) {
947
+ organisationMatches.push(match.index);
948
+ }
949
+ }
950
+
951
+ // Check nesting order - UnifiedAuthProvider should come before OrganisationProvider
952
+ if (unifiedAuthMatches.length > 0 && organisationMatches.length > 0) {
953
+ const firstUnifiedAuth = unifiedAuthMatches[0];
954
+ const firstOrganisation = organisationMatches[0];
955
+
956
+ // Check if OrganisationProvider appears before UnifiedAuthProvider (wrong order)
957
+ if (firstOrganisation < firstUnifiedAuth) {
958
+ issues.push({
959
+ type: 'providers',
960
+ file: nestingCheckFile,
961
+ line: getLineNumber(nestingCheckContent, firstOrganisation),
962
+ message: 'OrganisationProvider must be nested inside UnifiedAuthProvider, not the other way around.',
963
+ code: getCodeSnippet(nestingCheckContent, firstOrganisation),
964
+ severity: 'error',
965
+ fix: 'Nest OrganisationProvider inside UnifiedAuthProvider: <UnifiedAuthProvider><OrganisationProvider>...</OrganisationProvider></UnifiedAuthProvider>',
966
+ });
967
+ } else {
968
+ // UnifiedAuthProvider comes first - verify proper nesting
969
+ // Find closing tags to check if OrganisationProvider is actually inside UnifiedAuthProvider
970
+ const unifiedAuthClosePattern = /<\/UnifiedAuthProvider>/g;
971
+ const organisationClosePattern = /<\/OrganisationProvider>/g;
972
+
973
+ const unifiedAuthCloses = [];
974
+ const organisationCloses = [];
975
+
976
+ while ((match = unifiedAuthClosePattern.exec(nestingCheckContent)) !== null) {
977
+ unifiedAuthCloses.push(match.index);
978
+ }
979
+
980
+ while ((match = organisationClosePattern.exec(nestingCheckContent)) !== null) {
981
+ organisationCloses.push(match.index);
982
+ }
983
+
984
+ // Check if OrganisationProvider closing tag comes before UnifiedAuthProvider closing tag
985
+ // This ensures proper nesting: <UnifiedAuthProvider>...<OrganisationProvider>...</OrganisationProvider>...</UnifiedAuthProvider>
986
+ if (unifiedAuthCloses.length > 0 && organisationCloses.length > 0) {
987
+ const firstUnifiedAuthClose = unifiedAuthCloses[0];
988
+ const firstOrganisationClose = organisationCloses[0];
989
+
990
+ // OrganisationProvider should close before UnifiedAuthProvider closes
991
+ if (firstOrganisationClose > firstUnifiedAuthClose) {
992
+ issues.push({
993
+ type: 'providers',
994
+ file: nestingCheckFile,
995
+ line: getLineNumber(nestingCheckContent, firstOrganisation),
996
+ message: 'OrganisationProvider closing tag must come before UnifiedAuthProvider closing tag. Check nesting order.',
997
+ code: getCodeSnippet(nestingCheckContent, firstOrganisation),
998
+ severity: 'error',
999
+ fix: 'Nest OrganisationProvider inside UnifiedAuthProvider: <UnifiedAuthProvider><OrganisationProvider>...</OrganisationProvider></UnifiedAuthProvider>',
1000
+ });
1001
+ }
1002
+ }
1003
+ }
1004
+ }
1005
+ }
1006
+
1007
+ return issues;
1008
+ }
1009
+
1010
+ /**
1011
+ * Check for core.css import (via app.css)
1012
+ * Correct pattern:
1013
+ * - main.tsx imports ./app.css (NOT core.css directly)
1014
+ * - app.css contains @import "@jmruthers/pace-core/styles/core.css";
1015
+ */
1016
+ function checkCoreStyles(consumingAppPath) {
1017
+ const issues = [];
1018
+
1019
+ // Check main.tsx or App.tsx
1020
+ const checkFiles = [
1021
+ path.join(consumingAppPath, 'src', 'main.tsx'),
1022
+ path.join(consumingAppPath, 'src', 'main.ts'),
1023
+ path.join(consumingAppPath, 'src', 'main.jsx'),
1024
+ path.join(consumingAppPath, 'src', 'main.js'),
1025
+ path.join(consumingAppPath, 'src', 'App.tsx'),
1026
+ path.join(consumingAppPath, 'src', 'App.ts'),
1027
+ path.join(consumingAppPath, 'src', 'App.jsx'),
1028
+ path.join(consumingAppPath, 'src', 'App.js'),
1029
+ ];
1030
+
1031
+ let foundFile = null;
1032
+ let content = '';
1033
+
1034
+ for (const file of checkFiles) {
1035
+ if (fs.existsSync(file)) {
1036
+ foundFile = file;
1037
+ content = fs.readFileSync(file, 'utf8');
1038
+ break;
1039
+ }
1040
+ }
1041
+
1042
+ if (!foundFile) {
1043
+ issues.push({
1044
+ type: 'coreStyles',
1045
+ file: 'src/main.tsx or src/App.tsx (not found)',
1046
+ line: 0,
1047
+ message: 'main.tsx or App.tsx not found. app.css must be imported.',
1048
+ code: '',
1049
+ severity: 'error',
1050
+ fix: 'Create src/main.tsx and import: import \'./app.css\';',
1051
+ });
1052
+ return issues;
1053
+ }
1054
+
1055
+ const relativePath = path.relative(consumingAppPath || process.cwd(), foundFile);
1056
+
1057
+ // Check for app.css import (correct pattern)
1058
+ // Matches: import './app.css', import "./app.css", import '@/app.css', import '../app.css', etc.
1059
+ const hasAppCssImport = /import\s+['"](\.\/|@\/|\.\.\/)?app\.css['"]/.test(content);
1060
+
1061
+ // Check for direct core.css import (incorrect pattern - should be in app.css)
1062
+ const coreCssImportMatch = content.match(/@jmruthers\/pace-core\/styles\/core\.css/);
1063
+ const hasDirectCoreCssImport = coreCssImportMatch !== null;
1064
+
1065
+ if (hasDirectCoreCssImport) {
1066
+ const coreCssIndex = coreCssImportMatch.index;
1067
+ issues.push({
1068
+ type: 'coreStyles',
1069
+ file: relativePath,
1070
+ line: getLineNumber(content, coreCssIndex),
1071
+ message: 'Direct core.css import detected. Should import app.css instead, which imports core.css.',
1072
+ code: getCodeSnippet(content, coreCssIndex),
1073
+ severity: 'error',
1074
+ fix: 'Remove direct core.css import and import app.css instead: import \'./app.css\';',
1075
+ });
1076
+ }
1077
+
1078
+ if (!hasAppCssImport) {
1079
+ issues.push({
1080
+ type: 'coreStyles',
1081
+ file: relativePath,
1082
+ line: 1,
1083
+ message: 'app.css import not found. Must import app.css (which imports core.css).',
1084
+ code: '',
1085
+ severity: 'error',
1086
+ fix: 'Add: import \'./app.css\';',
1087
+ });
1088
+ }
1089
+
1090
+ // Check that app.css exists and contains core.css import
1091
+ const appCssFiles = [
1092
+ path.join(consumingAppPath, 'src', 'app.css'),
1093
+ path.join(consumingAppPath, 'app.css'),
1094
+ ];
1095
+
1096
+ let appCssFile = null;
1097
+ let appCssContent = '';
1098
+
1099
+ for (const file of appCssFiles) {
1100
+ if (fs.existsSync(file)) {
1101
+ appCssFile = file;
1102
+ appCssContent = fs.readFileSync(file, 'utf8');
1103
+ break;
1104
+ }
1105
+ }
1106
+
1107
+ if (!appCssFile) {
1108
+ issues.push({
1109
+ type: 'coreStyles',
1110
+ file: 'src/app.css (not found)',
1111
+ line: 0,
1112
+ message: 'app.css file not found. Must create app.css with @import "@jmruthers/pace-core/styles/core.css";',
1113
+ code: '',
1114
+ severity: 'error',
1115
+ fix: 'Create src/app.css with: @import "tailwindcss"; @import "@jmruthers/pace-core/styles/core.css";',
1116
+ });
1117
+ } else {
1118
+ // Check that app.css contains the core.css import
1119
+ const hasCoreCssInAppCss = /@import\s+['"]@jmruthers\/pace-core\/styles\/core\.css['"]/.test(appCssContent);
1120
+
1121
+ if (!hasCoreCssInAppCss) {
1122
+ const appCssRelativePath = path.relative(consumingAppPath || process.cwd(), appCssFile);
1123
+ issues.push({
1124
+ type: 'coreStyles',
1125
+ file: appCssRelativePath,
1126
+ line: 1,
1127
+ message: 'core.css import not found in app.css. Must add @import "@jmruthers/pace-core/styles/core.css";',
1128
+ code: '',
1129
+ severity: 'error',
1130
+ fix: 'Add to app.css: @import "@jmruthers/pace-core/styles/core.css";',
1131
+ });
1132
+ }
1133
+ }
1134
+
1135
+ return issues;
1136
+ }
1137
+
1138
+ /**
1139
+ * Check for inline styles
1140
+ *
1141
+ * MIGRATED TO ESLINT: This check is now handled by 'no-inline-styles' ESLint rule.
1142
+ * Kept for reference only.
1143
+ */
1144
+ function checkInlineStyles_MIGRATED_TO_ESLINT(content, filePath, consumingAppPath) {
1145
+ const issues = [];
1146
+
1147
+ // Pattern to match style={{...}} or style={...}
1148
+ const stylePattern = /style\s*=\s*\{/g;
1149
+ let match;
1150
+
1151
+ while ((match = stylePattern.exec(content)) !== null) {
1152
+ if (isInCommentOrString(content, match.index)) {
1153
+ continue;
1154
+ }
1155
+
1156
+ const relativePath = path.relative(consumingAppPath || process.cwd(), filePath);
1157
+ issues.push({
1158
+ type: 'inlineStyles',
1159
+ file: relativePath,
1160
+ line: getLineNumber(content, match.index),
1161
+ message: 'Inline style detected. Use pace-core components or Tailwind classes instead.',
1162
+ code: getCodeSnippet(content, match.index),
1163
+ severity: 'error',
1164
+ fix: 'Remove inline style and use pace-core component styling or Tailwind utility classes',
1165
+ });
1166
+ }
1167
+
1168
+ return issues;
1169
+ }
1170
+
1171
+ /**
1172
+ * Main audit function
1173
+ */
1174
+ function runComplianceAudit(consumingAppPath = process.cwd()) {
1175
+ const srcPath = path.join(consumingAppPath, 'src');
1176
+ const searchPath = fs.existsSync(srcPath) ? srcPath : consumingAppPath;
1177
+
1178
+ if (!fs.existsSync(searchPath)) {
1179
+ return {
1180
+ error: `Source directory not found at ${searchPath}`,
1181
+ issues: {
1182
+ supabaseClient: [],
1183
+ rbacSetup: [],
1184
+ providers: [],
1185
+ coreStyles: [],
1186
+ rbacPermission: [],
1187
+ },
1188
+ };
1189
+ }
1190
+
1191
+ // Load manifest
1192
+ const manifest = loadManifest(consumingAppPath);
1193
+
1194
+ // Find all source files
1195
+ const sourceFiles = findSourceFiles(searchPath);
1196
+
1197
+ if (sourceFiles.length === 0) {
1198
+ return {
1199
+ error: `No source files found in ${searchPath}`,
1200
+ issues: {
1201
+ supabaseClient: [],
1202
+ rbacSetup: [],
1203
+ providers: [],
1204
+ coreStyles: [],
1205
+ rbacPermission: [],
1206
+ },
1207
+ };
1208
+ }
1209
+
1210
+ const issues = {
1211
+ // NOTE: nativeElements, restrictedImports, customHooks, customUtils, inlineStyles
1212
+ // have been migrated to ESLint rules. Only file-system/config checks remain.
1213
+ supabaseClient: [],
1214
+ rbacSetup: [],
1215
+ providers: [],
1216
+ coreStyles: [],
1217
+ // Keep rbacPermission for now (partial migration - complex checks may remain)
1218
+ rbacPermission: [],
1219
+ };
1220
+
1221
+ // Check each file
1222
+ // NOTE: Native elements, restricted imports, custom hooks/utils, inline styles,
1223
+ // and RBAC permission loading checks have been migrated to ESLint rules.
1224
+ // Only file-system and config-based checks remain here.
1225
+ sourceFiles.forEach(filePath => {
1226
+ try {
1227
+ const content = fs.readFileSync(filePath, 'utf8');
1228
+
1229
+ // Check for Supabase usage (file-system based checks only)
1230
+ // Direct client creation checks moved to ESLint
1231
+ const supabaseIssues = checkSupabaseUsage(content, filePath, consumingAppPath);
1232
+ issues.supabaseClient.push(...supabaseIssues);
1233
+
1234
+ } catch (error) {
1235
+ // Skip files that can't be read
1236
+ console.warn(`Warning: Could not read ${filePath}: ${error.message}`);
1237
+ }
1238
+ });
1239
+
1240
+ // Check setup-specific files
1241
+ const rbacIssues = checkRBACSetup(consumingAppPath);
1242
+ issues.rbacSetup.push(...rbacIssues);
1243
+
1244
+ const providerIssues = checkProviders(consumingAppPath);
1245
+ issues.providers.push(...providerIssues);
1246
+
1247
+ const coreStyleIssues = checkCoreStyles(consumingAppPath);
1248
+ issues.coreStyles.push(...coreStyleIssues);
1249
+
1250
+ return {
1251
+ issues,
1252
+ };
1253
+ }
1254
+
1255
+ // Export for use by other scripts
1256
+ if (typeof module !== 'undefined' && module.exports) {
1257
+ module.exports = { runComplianceAudit };
1258
+ }
1259
+
1260
+ // If run directly, output results
1261
+ if (require.main === module) {
1262
+ const consumingAppPath = process.argv[2] || process.cwd();
1263
+ const result = runComplianceAudit(consumingAppPath);
1264
+
1265
+ if (result.error) {
1266
+ console.error(`Error: ${result.error}`);
1267
+ process.exit(1);
1268
+ }
1269
+
1270
+ const { issues } = result;
1271
+
1272
+ // Count total issues
1273
+ const totalIssues = Object.values(issues).reduce((sum, arr) => sum + arr.length, 0);
1274
+
1275
+ if (totalIssues === 0) {
1276
+ console.log('✅ No compliance issues found!');
1277
+ process.exit(0);
1278
+ }
1279
+
1280
+ console.log(`\n❌ Found ${totalIssues} compliance issue(s):\n`);
1281
+
1282
+ // Group by type
1283
+ Object.entries(issues).forEach(([type, typeIssues]) => {
1284
+ if (typeIssues.length > 0) {
1285
+ console.log(`\n${type}: ${typeIssues.length} issue(s)`);
1286
+ typeIssues.forEach(issue => {
1287
+ console.log(` ${issue.file}:${issue.line}`);
1288
+ console.log(` ${issue.message}`);
1289
+ });
1290
+ }
1291
+ });
1292
+
1293
+ process.exit(1);
1294
+ }
1295
+