@jmruthers/pace-core 0.5.184 → 0.5.186

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 (319) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +60 -1
  3. package/core-usage-manifest.json +312 -0
  4. package/dist/{DataTable-QAB34V6K.js → DataTable-IX2NBUTP.js} +6 -6
  5. package/dist/{DataTable-Bz8ffqyA.d.ts → DataTable-Z9NLVJh0.d.ts} +1 -1
  6. package/dist/{index-Bl--n7-T.d.ts → PublicPageProvider-DIzEzwKl.d.ts} +23 -10
  7. package/dist/{UnifiedAuthProvider-7F6T4B6K.js → UnifiedAuthProvider-A4BCQRJY.js} +4 -2
  8. package/dist/{UnifiedAuthProvider-F86d7dSi.d.ts → UnifiedAuthProvider-BG0AL5eE.d.ts} +2 -1
  9. package/dist/{api-ROMBCNKU.js → api-BMFCXVQX.js} +2 -2
  10. package/dist/{chunk-RA3JUFMW.js → chunk-445GEP27.js} +154 -4
  11. package/dist/{chunk-RA3JUFMW.js.map → chunk-445GEP27.js.map} +1 -1
  12. package/dist/{chunk-W22JP75J.js → chunk-DAGICKHT.js} +9 -7
  13. package/dist/chunk-DAGICKHT.js.map +1 -0
  14. package/dist/{chunk-FUEYYMX5.js → chunk-FXFJRTKI.js} +24 -3
  15. package/dist/chunk-FXFJRTKI.js.map +1 -0
  16. package/dist/{chunk-CSOFYHAG.js → chunk-GRIQLQ52.js} +374 -60
  17. package/dist/chunk-GRIQLQ52.js.map +1 -0
  18. package/dist/{chunk-NQPMQGS2.js → chunk-HDCUMOOI.js} +497 -399
  19. package/dist/chunk-HDCUMOOI.js.map +1 -0
  20. package/dist/chunk-HESYZWZW.js +388 -0
  21. package/dist/chunk-HESYZWZW.js.map +1 -0
  22. package/dist/{chunk-QUVSNGIP.js → chunk-HGPQUCBC.js} +34 -9
  23. package/dist/{chunk-QUVSNGIP.js.map → chunk-HGPQUCBC.js.map} +1 -1
  24. package/dist/{chunk-PWAHJW4G.js → chunk-OALXJH4Y.js} +86 -33
  25. package/dist/chunk-OALXJH4Y.js.map +1 -0
  26. package/dist/{chunk-MI7HBHN3.js → chunk-TC7D3CR3.js} +89 -9
  27. package/dist/chunk-TC7D3CR3.js.map +1 -0
  28. package/dist/chunk-THRPYOFK.js +215 -0
  29. package/dist/chunk-THRPYOFK.js.map +1 -0
  30. package/dist/{chunk-M7W4CP3M.js → chunk-U6WNSFX5.js} +2 -1
  31. package/dist/chunk-U6WNSFX5.js.map +1 -0
  32. package/dist/{chunk-UHNYIBXL.js → chunk-UQWSHFVX.js} +1 -1
  33. package/dist/chunk-UQWSHFVX.js.map +1 -0
  34. package/dist/{chunk-QCDXODCA.js → chunk-XAUHJD3L.js} +2 -2
  35. package/dist/components.d.ts +182 -6
  36. package/dist/components.js +157 -11
  37. package/dist/components.js.map +1 -1
  38. package/dist/{database.generated-CBmg2950.d.ts → database.generated-DI89OQeI.d.ts} +63 -9
  39. package/dist/eslint-rules/pace-core-compliance.cjs +406 -0
  40. package/dist/{file-reference-D06mEEWW.d.ts → file-reference-PRTSLxKx.d.ts} +10 -1
  41. package/dist/hooks.d.ts +52 -15
  42. package/dist/hooks.js +12 -22
  43. package/dist/hooks.js.map +1 -1
  44. package/dist/index.d.ts +12 -12
  45. package/dist/index.js +82 -18
  46. package/dist/index.js.map +1 -1
  47. package/dist/providers.d.ts +1 -1
  48. package/dist/providers.js +3 -1
  49. package/dist/rbac/index.d.ts +206 -15
  50. package/dist/rbac/index.js +28 -6
  51. package/dist/timezone-_pgH8qrY.d.ts +530 -0
  52. package/dist/{types-_x1f4QBF.d.ts → types-DUyCRSTj.d.ts} +1 -1
  53. package/dist/types.d.ts +2 -2
  54. package/dist/types.js +1 -1
  55. package/dist/{usePublicRouteParams-JJczomYq.d.ts → usePublicRouteParams-D71QLlg4.d.ts} +114 -3
  56. package/dist/utils.d.ts +110 -152
  57. package/dist/utils.js +128 -138
  58. package/dist/utils.js.map +1 -1
  59. package/docs/api/README.md +60 -1
  60. package/docs/api/classes/ColumnFactory.md +1 -1
  61. package/docs/api/classes/ErrorBoundary.md +1 -1
  62. package/docs/api/classes/InvalidScopeError.md +1 -1
  63. package/docs/api/classes/Logger.md +178 -0
  64. package/docs/api/classes/MissingUserContextError.md +1 -1
  65. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  66. package/docs/api/classes/PermissionDeniedError.md +1 -1
  67. package/docs/api/classes/RBACAuditManager.md +2 -2
  68. package/docs/api/classes/RBACCache.md +1 -1
  69. package/docs/api/classes/RBACEngine.md +2 -2
  70. package/docs/api/classes/RBACError.md +1 -1
  71. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  72. package/docs/api/classes/SecureSupabaseClient.md +5 -5
  73. package/docs/api/classes/StorageUtils.md +1 -1
  74. package/docs/api/enums/FileCategory.md +1 -1
  75. package/docs/api/enums/LogLevel.md +54 -0
  76. package/docs/api/enums/RBACErrorCode.md +1 -1
  77. package/docs/api/enums/RPCFunction.md +1 -1
  78. package/docs/api/interfaces/AggregateConfig.md +1 -1
  79. package/docs/api/interfaces/BadgeProps.md +1 -1
  80. package/docs/api/interfaces/ButtonProps.md +1 -1
  81. package/docs/api/interfaces/CalendarProps.md +18 -2
  82. package/docs/api/interfaces/CardProps.md +1 -1
  83. package/docs/api/interfaces/ColorPalette.md +1 -1
  84. package/docs/api/interfaces/ColorShade.md +1 -1
  85. package/docs/api/interfaces/ComplianceResult.md +30 -0
  86. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  87. package/docs/api/interfaces/DataRecord.md +1 -1
  88. package/docs/api/interfaces/DataTableAction.md +1 -1
  89. package/docs/api/interfaces/DataTableColumn.md +1 -1
  90. package/docs/api/interfaces/DataTableProps.md +1 -1
  91. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  92. package/docs/api/interfaces/DatabaseComplianceResult.md +85 -0
  93. package/docs/api/interfaces/DatabaseIssue.md +41 -0
  94. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  95. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  96. package/docs/api/interfaces/EventAppRoleData.md +6 -6
  97. package/docs/api/interfaces/ExportColumn.md +1 -1
  98. package/docs/api/interfaces/ExportOptions.md +1 -1
  99. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  100. package/docs/api/interfaces/FileMetadata.md +1 -1
  101. package/docs/api/interfaces/FileReference.md +1 -1
  102. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  103. package/docs/api/interfaces/FileUploadOptions.md +48 -8
  104. package/docs/api/interfaces/FileUploadProps.md +46 -13
  105. package/docs/api/interfaces/FooterProps.md +1 -1
  106. package/docs/api/interfaces/FormFieldProps.md +1 -1
  107. package/docs/api/interfaces/FormProps.md +1 -1
  108. package/docs/api/interfaces/GrantEventAppRoleParams.md +9 -9
  109. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  110. package/docs/api/interfaces/InputProps.md +1 -1
  111. package/docs/api/interfaces/LabelProps.md +1 -1
  112. package/docs/api/interfaces/LoggerConfig.md +62 -0
  113. package/docs/api/interfaces/LoginFormProps.md +1 -1
  114. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  115. package/docs/api/interfaces/NavigationContextType.md +1 -1
  116. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  117. package/docs/api/interfaces/NavigationItem.md +1 -1
  118. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  119. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  120. package/docs/api/interfaces/Organisation.md +1 -1
  121. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  122. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  123. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  124. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  125. package/docs/api/interfaces/PaceAppLayoutProps.md +36 -23
  126. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  127. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  128. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  129. package/docs/api/interfaces/PagePermissionGuardProps.md +11 -11
  130. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  131. package/docs/api/interfaces/PaletteData.md +1 -1
  132. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  133. package/docs/api/interfaces/ProgressProps.md +1 -1
  134. package/docs/api/interfaces/ProtectedRouteProps.md +6 -6
  135. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  136. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  137. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  138. package/docs/api/interfaces/QuickFix.md +52 -0
  139. package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
  140. package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
  141. package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
  142. package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
  143. package/docs/api/interfaces/RBACConfig.md +4 -4
  144. package/docs/api/interfaces/RBACContext.md +1 -1
  145. package/docs/api/interfaces/RBACLogger.md +1 -1
  146. package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
  147. package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
  148. package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
  149. package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
  150. package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
  151. package/docs/api/interfaces/RBACResult.md +1 -1
  152. package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
  153. package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
  154. package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
  155. package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
  156. package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
  157. package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
  158. package/docs/api/interfaces/RBACRolesListParams.md +1 -1
  159. package/docs/api/interfaces/RBACRolesListResult.md +1 -1
  160. package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
  161. package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
  162. package/docs/api/interfaces/ResourcePermissions.md +1 -1
  163. package/docs/api/interfaces/RevokeEventAppRoleParams.md +7 -7
  164. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  165. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  166. package/docs/api/interfaces/RoleManagementResult.md +5 -5
  167. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  168. package/docs/api/interfaces/RouteConfig.md +1 -1
  169. package/docs/api/interfaces/RuntimeComplianceResult.md +55 -0
  170. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  171. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  172. package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
  173. package/docs/api/interfaces/SetupIssue.md +41 -0
  174. package/docs/api/interfaces/StorageConfig.md +1 -1
  175. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  176. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  177. package/docs/api/interfaces/StorageListOptions.md +1 -1
  178. package/docs/api/interfaces/StorageListResult.md +1 -1
  179. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  180. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  181. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  182. package/docs/api/interfaces/StyleImport.md +1 -1
  183. package/docs/api/interfaces/SwitchProps.md +1 -1
  184. package/docs/api/interfaces/TabsContentProps.md +1 -1
  185. package/docs/api/interfaces/TabsListProps.md +1 -1
  186. package/docs/api/interfaces/TabsProps.md +1 -1
  187. package/docs/api/interfaces/TabsTriggerProps.md +1 -1
  188. package/docs/api/interfaces/TextareaProps.md +1 -1
  189. package/docs/api/interfaces/ToastActionElement.md +1 -1
  190. package/docs/api/interfaces/ToastProps.md +1 -1
  191. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  192. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  193. package/docs/api/interfaces/UseFormDialogOptions.md +62 -0
  194. package/docs/api/interfaces/UseFormDialogReturn.md +117 -0
  195. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  196. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  197. package/docs/api/interfaces/UsePublicEventLogoOptions.md +2 -2
  198. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  199. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  200. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  201. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +2 -2
  202. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  203. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  204. package/docs/api/interfaces/UseResolvedScopeOptions.md +2 -2
  205. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  206. package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
  207. package/docs/api/interfaces/UserEventAccess.md +1 -1
  208. package/docs/api/interfaces/UserMenuProps.md +1 -1
  209. package/docs/api/interfaces/UserProfile.md +1 -1
  210. package/docs/api/modules.md +746 -50
  211. package/docs/api-reference/components.md +26 -12
  212. package/docs/api-reference/hooks.md +111 -0
  213. package/docs/api-reference/rpc-functions.md +1 -1
  214. package/docs/api-reference/utilities.md +184 -0
  215. package/docs/getting-started/installation-guide.md +75 -16
  216. package/docs/getting-started/quick-start.md +61 -11
  217. package/docs/implementation-guides/authentication.md +88 -12
  218. package/docs/implementation-guides/file-reference-system.md +26 -3
  219. package/docs/implementation-guides/file-upload-storage.md +30 -1
  220. package/docs/rbac/README.md +1 -0
  221. package/docs/rbac/compliance/compliance-guide.md +544 -0
  222. package/docs/rbac/getting-started.md +158 -33
  223. package/docs/standards/pace-core-compliance.md +432 -0
  224. package/eslint-config-pace-core.cjs +93 -0
  225. package/package.json +15 -3
  226. package/scripts/analyze-bundle.js +232 -0
  227. package/scripts/build-css.js +56 -0
  228. package/scripts/build-docs-incremental.js +1015 -0
  229. package/scripts/check-pace-core-compliance.cjs +2353 -0
  230. package/scripts/check-pace-core-compliance.js +512 -0
  231. package/scripts/generate-docs.js +157 -0
  232. package/scripts/setup-build-cache.js +73 -0
  233. package/scripts/utils/command-runner.js +131 -0
  234. package/scripts/utils/env.js +33 -0
  235. package/scripts/utils/index.js +10 -0
  236. package/scripts/utils/logger.js +88 -0
  237. package/scripts/utils/path-helpers.js +37 -0
  238. package/scripts/validate-formats.js +133 -0
  239. package/scripts/validate-master.js +155 -0
  240. package/scripts/validate-pre-publish.js +140 -0
  241. package/scripts/validate-theme.js +142 -0
  242. package/src/components/Calendar/Calendar.tsx +8 -1
  243. package/src/components/Card/Card.tsx +47 -8
  244. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +314 -0
  245. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.tsx +126 -0
  246. package/src/components/DatePickerWithTimezone/README.md +135 -0
  247. package/src/components/DatePickerWithTimezone/index.ts +10 -0
  248. package/src/components/DateTimeField/DateTimeField.test.tsx +358 -0
  249. package/src/components/DateTimeField/DateTimeField.tsx +232 -0
  250. package/src/components/DateTimeField/README.md +148 -0
  251. package/src/components/DateTimeField/index.ts +10 -0
  252. package/src/components/FileUpload/FileUpload.test.tsx +2 -0
  253. package/src/components/FileUpload/FileUpload.tsx +10 -1
  254. package/src/components/Header/Header.test.tsx +47 -18
  255. package/src/components/Header/Header.tsx +22 -7
  256. package/src/components/PaceAppLayout/PaceAppLayout.tsx +29 -20
  257. package/src/components/PaceAppLayout/README.md +9 -0
  258. package/src/components/ProtectedRoute/ProtectedRoute.test.tsx +37 -8
  259. package/src/components/ProtectedRoute/ProtectedRoute.tsx +146 -5
  260. package/src/components/index.ts +8 -0
  261. package/src/eslint-rules/pace-core-compliance.cjs +406 -0
  262. package/src/eslint-rules/pace-core-compliance.js +640 -0
  263. package/src/hooks/__tests__/useFormDialog.test.ts +478 -0
  264. package/src/hooks/index.ts +5 -0
  265. package/src/hooks/useFileReference.test.ts +2 -0
  266. package/src/hooks/useFormDialog.ts +147 -0
  267. package/src/hooks/usePreventTabReload.ts +106 -0
  268. package/src/hooks/useSecureDataAccess.ts +2 -2
  269. package/src/index.ts +27 -0
  270. package/src/providers/services/OrganisationServiceProvider.tsx +6 -5
  271. package/src/providers/services/UnifiedAuthProvider.tsx +24 -3
  272. package/src/rbac/__tests__/rbac-role-isolation.test.ts +456 -0
  273. package/src/rbac/__tests__/scenarios.user-role.test.tsx +3 -0
  274. package/src/rbac/compliance/database-validator.ts +165 -0
  275. package/src/rbac/compliance/index.ts +38 -0
  276. package/src/rbac/compliance/quick-fix-suggestions.ts +209 -0
  277. package/src/rbac/compliance/runtime-compliance.ts +77 -0
  278. package/src/rbac/compliance/setup-validator.ts +131 -0
  279. package/src/rbac/components/PagePermissionGuard.tsx +8 -64
  280. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +35 -21
  281. package/src/rbac/docs/event-based-apps.md +285 -0
  282. package/src/rbac/errors.ts +11 -0
  283. package/src/rbac/hooks/useRoleManagement.ts +292 -12
  284. package/src/rbac/index.ts +30 -0
  285. package/src/services/OrganisationService.ts +4 -0
  286. package/src/styles/core.css +5 -5
  287. package/src/types/database.generated.ts +63 -9
  288. package/src/types/file-reference.ts +9 -0
  289. package/src/utils/__tests__/timezone.test.ts +345 -0
  290. package/src/utils/file-reference/__tests__/file-reference.test.ts +60 -4
  291. package/src/utils/file-reference/index.ts +13 -2
  292. package/src/utils/formatting/formatDateTimeTimezone.test.ts +167 -0
  293. package/src/utils/formatting/formatting.ts +179 -0
  294. package/src/utils/index.ts +27 -1
  295. package/src/utils/location/index.ts +16 -0
  296. package/src/utils/location/location.test.ts +286 -0
  297. package/src/utils/location/location.ts +175 -0
  298. package/src/utils/security/secureDataAccess.ts +1 -1
  299. package/src/utils/storage/helpers.ts +68 -0
  300. package/src/utils/timezone/index.ts +17 -0
  301. package/src/utils/timezone/timezone.test.ts +349 -0
  302. package/src/utils/timezone/timezone.ts +281 -0
  303. package/dist/chunk-CSOFYHAG.js.map +0 -1
  304. package/dist/chunk-FUEYYMX5.js.map +0 -1
  305. package/dist/chunk-HKIT6O7W.js +0 -198
  306. package/dist/chunk-HKIT6O7W.js.map +0 -1
  307. package/dist/chunk-KUEN3HFB.js +0 -94
  308. package/dist/chunk-KUEN3HFB.js.map +0 -1
  309. package/dist/chunk-M7W4CP3M.js.map +0 -1
  310. package/dist/chunk-MI7HBHN3.js.map +0 -1
  311. package/dist/chunk-NQPMQGS2.js.map +0 -1
  312. package/dist/chunk-PWAHJW4G.js.map +0 -1
  313. package/dist/chunk-UHNYIBXL.js.map +0 -1
  314. package/dist/chunk-W22JP75J.js.map +0 -1
  315. package/dist/formatting-5wETwiGF.d.ts +0 -162
  316. /package/dist/{DataTable-QAB34V6K.js.map → DataTable-IX2NBUTP.js.map} +0 -0
  317. /package/dist/{UnifiedAuthProvider-7F6T4B6K.js.map → UnifiedAuthProvider-A4BCQRJY.js.map} +0 -0
  318. /package/dist/{api-ROMBCNKU.js.map → api-BMFCXVQX.js.map} +0 -0
  319. /package/dist/{chunk-QCDXODCA.js.map → chunk-XAUHJD3L.js.map} +0 -0
@@ -0,0 +1,1015 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Incremental TypeDoc Build Script
5
+ *
6
+ * This script only regenerates documentation when source files have changed,
7
+ * significantly speeding up the build process.
8
+ */
9
+
10
+ import { execSync } from 'child_process';
11
+ import { existsSync, statSync, readFileSync, writeFileSync, readdirSync, mkdirSync, rmSync, cpSync, utimesSync } from 'fs';
12
+ import { join, dirname, relative } from 'path';
13
+ import { fileURLToPath } from 'url';
14
+ import { createHash } from 'crypto';
15
+
16
+ const __filename = fileURLToPath(import.meta.url);
17
+ const __dirname = dirname(__filename);
18
+
19
+ const PACKAGE_ROOT = join(__dirname, '..');
20
+ const REPO_ROOT = join(PACKAGE_ROOT, '..', '..');
21
+ const SRC_DIR = join(PACKAGE_ROOT, 'src');
22
+ const DOCS_DIR = join(PACKAGE_ROOT, 'docs', 'api');
23
+ const CACHE_FILE = join(PACKAGE_ROOT, '.docs-cache.json');
24
+ const DOCS_RELEVANT_PATHS = [
25
+ 'packages/core/src',
26
+ 'packages/core/examples',
27
+ 'packages/core/typedoc.json'
28
+ ];
29
+
30
+ /**
31
+ * Recursively scan directory for files matching pattern
32
+ */
33
+ function scanDirectory(dir, extensions, ignorePatterns = []) {
34
+ const files = [];
35
+
36
+ function scan(currentDir) {
37
+ if (!existsSync(currentDir)) {
38
+ return;
39
+ }
40
+
41
+ try {
42
+ const items = readdirSync(currentDir, { withFileTypes: true });
43
+
44
+ for (const item of items) {
45
+ const fullPath = join(currentDir, item.name);
46
+
47
+ // Skip ignored patterns
48
+ const shouldIgnore = ignorePatterns.some(pattern => {
49
+ const relativePath = fullPath.replace(SRC_DIR + '/', '');
50
+ return pattern.test(relativePath);
51
+ });
52
+
53
+ if (shouldIgnore) {
54
+ continue;
55
+ }
56
+
57
+ if (item.isDirectory() && !item.name.startsWith('.') && item.name !== 'node_modules') {
58
+ scan(fullPath);
59
+ } else if (item.isFile()) {
60
+ const ext = item.name.substring(item.name.lastIndexOf('.'));
61
+ if (extensions.includes(ext)) {
62
+ files.push(fullPath);
63
+ }
64
+ }
65
+ }
66
+ } catch (err) {
67
+ // Skip directories we can't read
68
+ }
69
+ }
70
+
71
+ scan(dir);
72
+ return files;
73
+ }
74
+
75
+ /**
76
+ * Determine if we're inside a git repository
77
+ */
78
+ function isGitRepository() {
79
+ try {
80
+ execSync('git rev-parse --is-inside-work-tree', {
81
+ cwd: REPO_ROOT,
82
+ stdio: 'pipe'
83
+ });
84
+ return true;
85
+ } catch (err) {
86
+ return false;
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Check if git reports any relevant source changes
92
+ */
93
+ function hasGitTrackedSourceChanges() {
94
+ if (!isGitRepository()) {
95
+ return true; // Without git context, err on the side of rebuilding
96
+ }
97
+
98
+ try {
99
+ const status = execSync(`git status --porcelain -- ${DOCS_RELEVANT_PATHS.join(' ')}`.trim(), {
100
+ cwd: REPO_ROOT,
101
+ encoding: 'utf-8',
102
+ stdio: 'pipe'
103
+ }).trim();
104
+
105
+ return status.length > 0;
106
+ } catch (err) {
107
+ console.log('⚠️ Unable to determine git status for docs generation, running TypeDoc to be safe.');
108
+ return true;
109
+ }
110
+ }
111
+
112
+ /**
113
+ * If we don't have a cache yet but the repo is clean and docs exist, we can skip
114
+ */
115
+ function shouldSkipDueToCleanGitState() {
116
+ if (!existsSync(DOCS_DIR)) {
117
+ return false;
118
+ }
119
+
120
+ const existingDocs = scanDirectory(DOCS_DIR, ['.md']);
121
+ if (existingDocs.length === 0) {
122
+ return false;
123
+ }
124
+
125
+ if (hasGitTrackedSourceChanges()) {
126
+ return false;
127
+ }
128
+
129
+ console.log('✅ Git working tree is clean for documentation-related sources.');
130
+ console.log(' Assuming checked-in docs are up to date, skipping TypeDoc run.');
131
+ return true;
132
+ }
133
+
134
+ /**
135
+ * Prime the docs cache using the currently checked-in documentation.
136
+ * This lets future runs rely on hash comparisons even if git status is dirty.
137
+ */
138
+ function primeCacheFromExistingDocs() {
139
+ const currentDocHashes = getExistingDocHashes();
140
+ const currentSourceHashes = getSourceFileHashes();
141
+
142
+ const docHashesObj = {};
143
+ for (const [relativePath, hash] of currentDocHashes.entries()) {
144
+ docHashesObj[relativePath] = hash;
145
+ }
146
+
147
+ const sourceHashesObj = {};
148
+ for (const [relativePath, hash] of currentSourceHashes.entries()) {
149
+ sourceHashesObj[relativePath] = hash;
150
+ }
151
+
152
+ const cache = {
153
+ lastBuildTime: Date.now(),
154
+ sourceModTime: getLatestSourceModTime(),
155
+ docModTime: getLatestDocModTime(),
156
+ fileHashes: docHashesObj,
157
+ sourceFileHashes: sourceHashesObj
158
+ };
159
+
160
+ saveCache(cache);
161
+ }
162
+
163
+ /**
164
+ * Get the most recent modification time of all source files
165
+ */
166
+ function getLatestSourceModTime() {
167
+ const ignorePatterns = [
168
+ /\.test\.(ts|tsx)$/,
169
+ /\.spec\.(ts|tsx)$/,
170
+ /__tests__/,
171
+ /\/test\//,
172
+ /\/tests\//
173
+ ];
174
+
175
+ const sourceFiles = scanDirectory(SRC_DIR, ['.ts', '.tsx'], ignorePatterns);
176
+
177
+ let latestTime = 0;
178
+ for (const filePath of sourceFiles) {
179
+ try {
180
+ const stats = statSync(filePath);
181
+ if (stats.mtimeMs > latestTime) {
182
+ latestTime = stats.mtimeMs;
183
+ }
184
+ } catch (err) {
185
+ // File might have been deleted, skip it
186
+ }
187
+ }
188
+
189
+ return latestTime;
190
+ }
191
+
192
+ /**
193
+ * Get the most recent modification time of generated docs
194
+ */
195
+ function getLatestDocModTime() {
196
+ if (!existsSync(DOCS_DIR)) {
197
+ return 0;
198
+ }
199
+
200
+ const docFiles = scanDirectory(DOCS_DIR, ['.md']);
201
+
202
+ if (docFiles.length === 0) {
203
+ return 0;
204
+ }
205
+
206
+ let latestTime = 0;
207
+ for (const filePath of docFiles) {
208
+ try {
209
+ const stats = statSync(filePath);
210
+ if (stats.mtimeMs > latestTime) {
211
+ latestTime = stats.mtimeMs;
212
+ }
213
+ } catch (err) {
214
+ // File might have been deleted, skip it
215
+ }
216
+ }
217
+
218
+ return latestTime;
219
+ }
220
+
221
+ /**
222
+ * Get hashes of all source files
223
+ */
224
+ function getSourceFileHashes() {
225
+ const ignorePatterns = [
226
+ /\.test\.(ts|tsx)$/,
227
+ /\.spec\.(ts|tsx)$/,
228
+ /__tests__/,
229
+ /\/test\//,
230
+ /\/tests\//
231
+ ];
232
+
233
+ const sourceFiles = scanDirectory(SRC_DIR, ['.ts', '.tsx'], ignorePatterns);
234
+ const hashes = new Map();
235
+
236
+ for (const filePath of sourceFiles) {
237
+ try {
238
+ const content = readFileSync(filePath, 'utf-8');
239
+ const hash = createHash('md5').update(content).digest('hex');
240
+ const relativePath = relative(SRC_DIR, filePath);
241
+ hashes.set(relativePath, hash);
242
+ } catch (err) {
243
+ // Skip files we can't read
244
+ }
245
+ }
246
+
247
+ return hashes;
248
+ }
249
+
250
+ /**
251
+ * Check if source files have changed by comparing hashes
252
+ */
253
+ function hasSourceChanged(currentHashes, cachedHashes) {
254
+ if (!cachedHashes || Object.keys(cachedHashes).length === 0) {
255
+ return true; // No cache, assume changed
256
+ }
257
+
258
+ // Check if any file changed
259
+ for (const [relativePath, currentHash] of currentHashes.entries()) {
260
+ const cachedHash = cachedHashes[relativePath];
261
+ if (cachedHash !== currentHash) {
262
+ return true;
263
+ }
264
+ }
265
+
266
+ // Check if any files were added or removed
267
+ if (currentHashes.size !== Object.keys(cachedHashes).length) {
268
+ return true;
269
+ }
270
+
271
+ return false;
272
+ }
273
+
274
+ /**
275
+ * Check if doc file hashes match cached hashes
276
+ */
277
+ function hashesMatch(currentHashes, cachedHashes) {
278
+ if (!cachedHashes || Object.keys(cachedHashes).length === 0) {
279
+ return false; // No cache, assume mismatch
280
+ }
281
+
282
+ if (currentHashes.size !== Object.keys(cachedHashes).length) {
283
+ return false; // Different number of files
284
+ }
285
+
286
+ for (const [relativePath, currentHash] of currentHashes.entries()) {
287
+ const cachedHash = cachedHashes[relativePath];
288
+ if (cachedHash !== currentHash) {
289
+ return false;
290
+ }
291
+ }
292
+
293
+ return true;
294
+ }
295
+
296
+ /**
297
+ * Load the cache file
298
+ */
299
+ function loadCache() {
300
+ if (!existsSync(CACHE_FILE)) {
301
+ return {
302
+ lastBuildTime: 0,
303
+ sourceModTime: 0,
304
+ docModTime: 0,
305
+ fileHashes: {},
306
+ sourceFileHashes: {}
307
+ };
308
+ }
309
+
310
+ try {
311
+ const content = readFileSync(CACHE_FILE, 'utf-8');
312
+ const cache = JSON.parse(content);
313
+ // Ensure fileHashes exists for backward compatibility
314
+ if (!cache.fileHashes) {
315
+ cache.fileHashes = {};
316
+ }
317
+ if (!cache.sourceFileHashes) {
318
+ cache.sourceFileHashes = {};
319
+ }
320
+ return cache;
321
+ } catch (err) {
322
+ return {
323
+ lastBuildTime: 0,
324
+ sourceModTime: 0,
325
+ docModTime: 0,
326
+ fileHashes: {},
327
+ sourceFileHashes: {}
328
+ };
329
+ }
330
+ }
331
+
332
+ /**
333
+ * Save the cache file
334
+ */
335
+ function saveCache(cache) {
336
+ // Ensure fileHashes is always present
337
+ if (!cache.fileHashes) {
338
+ cache.fileHashes = {};
339
+ }
340
+ if (!cache.sourceFileHashes) {
341
+ cache.sourceFileHashes = {};
342
+ }
343
+ writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2), 'utf-8');
344
+ }
345
+
346
+ /**
347
+ * Aggressive pre-check: Should we run TypeDoc at all?
348
+ * This checks source file hashes BEFORE running TypeDoc to avoid unnecessary runs
349
+ */
350
+ function shouldRunTypeDoc() {
351
+ const cache = loadCache();
352
+
353
+ // If no cache, must run
354
+ if (cache.lastBuildTime === 0) {
355
+ if (shouldSkipDueToCleanGitState()) {
356
+ console.log('⏭️ Skipping TypeDoc - no cache yet but git shows no relevant source changes.');
357
+ console.log(' Reason: Using checked-in docs since source tree matches HEAD');
358
+ console.log(' Action: Priming docs cache from checked-in files for future runs');
359
+ primeCacheFromExistingDocs();
360
+ return false;
361
+ }
362
+ console.log('📝 No cache found, running TypeDoc...');
363
+ console.log(' Reason: First run - no cache exists');
364
+ return true;
365
+ }
366
+
367
+ // If docs don't exist, must run
368
+ if (!existsSync(DOCS_DIR)) {
369
+ console.log('📝 Documentation directory not found, running TypeDoc...');
370
+ console.log(' Reason: Documentation directory missing');
371
+ return true;
372
+ }
373
+
374
+ // Check source file hashes - most reliable check
375
+ console.log('🔍 Checking source file hashes...');
376
+ const currentSourceHashes = getSourceFileHashes();
377
+ const cachedSourceHashes = cache.sourceFileHashes || {};
378
+
379
+ console.log(` Current source files: ${currentSourceHashes.size}`);
380
+ console.log(` Cached source files: ${Object.keys(cachedSourceHashes).length}`);
381
+
382
+ if (hasSourceChanged(currentSourceHashes, cachedSourceHashes)) {
383
+ // Find which files changed
384
+ const changedFiles = [];
385
+ for (const [relativePath, currentHash] of currentSourceHashes.entries()) {
386
+ const cachedHash = cachedSourceHashes[relativePath];
387
+ if (cachedHash !== currentHash) {
388
+ changedFiles.push(relativePath);
389
+ }
390
+ }
391
+ console.log('📝 Source files have changed (hash comparison), running TypeDoc...');
392
+ if (changedFiles.length > 0 && changedFiles.length <= 10) {
393
+ console.log(` Changed files: ${changedFiles.slice(0, 5).join(', ')}${changedFiles.length > 5 ? ` ... and ${changedFiles.length - 5} more` : ''}`);
394
+ } else if (changedFiles.length > 10) {
395
+ console.log(` Changed files: ${changedFiles.length} files modified`);
396
+ }
397
+ return true;
398
+ }
399
+
400
+ // Source hasn't changed - check if docs match cache
401
+ console.log('🔍 Checking documentation file hashes...');
402
+ const currentDocHashes = getExistingDocHashes();
403
+ const cachedDocHashes = cache.fileHashes || {};
404
+
405
+ console.log(` Current doc files: ${currentDocHashes.size}`);
406
+ console.log(` Cached doc files: ${Object.keys(cachedDocHashes).length}`);
407
+
408
+ if (hashesMatch(currentDocHashes, cachedDocHashes)) {
409
+ console.log('✅ Source unchanged and docs match cache - skipping TypeDoc entirely');
410
+ console.log(' Reason: No source changes detected and documentation is up to date');
411
+ return false; // Skip TypeDoc!
412
+ }
413
+
414
+ // Docs don't match cache but source unchanged - refresh cache snapshot instead of regenerating
415
+ const mismatchedFiles = [];
416
+ for (const [relativePath, currentHash] of currentDocHashes.entries()) {
417
+ const cachedHash = cachedDocHashes[relativePath];
418
+ if (cachedHash !== currentHash) {
419
+ mismatchedFiles.push(relativePath);
420
+ }
421
+ }
422
+ console.log('⚠️ Source unchanged but docs don\'t match cache - refreshing cache from disk');
423
+ console.log(` Mismatched files: ${mismatchedFiles.length} files don't match cache`);
424
+ if (mismatchedFiles.length <= 5) {
425
+ console.log(` Files: ${mismatchedFiles.join(', ')}`);
426
+ }
427
+ primeCacheFromExistingDocs();
428
+ return false;
429
+ }
430
+
431
+ /**
432
+ * Check if any source files have changed since last build
433
+ */
434
+ function needsRebuild() {
435
+ const cache = loadCache();
436
+ const currentSourceModTime = getLatestSourceModTime();
437
+ const currentDocModTime = getLatestDocModTime();
438
+
439
+ // If docs don't exist, we need to build
440
+ if (!existsSync(DOCS_DIR) || currentDocModTime === 0) {
441
+ console.log('📝 Documentation directory not found or empty, building...');
442
+ return true;
443
+ }
444
+
445
+ // Check if typedoc.json has changed
446
+ const typedocConfigPath = join(PACKAGE_ROOT, 'typedoc.json');
447
+ if (existsSync(typedocConfigPath)) {
448
+ const configStats = statSync(typedocConfigPath);
449
+ if (cache.lastBuildTime > 0 && configStats.mtimeMs > cache.lastBuildTime) {
450
+ console.log('📝 TypeDoc configuration has changed, rebuilding...');
451
+ return true;
452
+ }
453
+ if (currentDocModTime > 0 && configStats.mtimeMs > currentDocModTime) {
454
+ console.log('📝 TypeDoc configuration is newer than docs, rebuilding...');
455
+ return true;
456
+ }
457
+ }
458
+
459
+ // Check entry point file (what TypeDoc actually uses)
460
+ const entryPointPath = join(PACKAGE_ROOT, 'src', 'index.ts');
461
+ if (existsSync(entryPointPath)) {
462
+ const entryStats = statSync(entryPointPath);
463
+ if (cache.lastBuildTime > 0 && entryStats.mtimeMs > cache.lastBuildTime) {
464
+ console.log('📝 Entry point file has changed, rebuilding...');
465
+ return true;
466
+ }
467
+ if (currentDocModTime > 0 && entryStats.mtimeMs > currentDocModTime) {
468
+ console.log('📝 Entry point file is newer than docs, rebuilding...');
469
+ return true;
470
+ }
471
+ }
472
+
473
+ // If source files are newer than docs, we need to rebuild
474
+ if (currentSourceModTime > currentDocModTime) {
475
+ console.log('📝 Source files are newer than documentation, rebuilding...');
476
+ return true;
477
+ }
478
+
479
+ // If cache indicates a rebuild is needed (e.g., after a clean)
480
+ // Only check cache if we have a valid cache entry
481
+ if (cache.lastBuildTime > 0) {
482
+ // Check if source files have changed since last build
483
+ const sourceChanged = currentSourceModTime > cache.sourceModTime;
484
+
485
+ // Check if docs are newer than the last build time (meaning they were just built)
486
+ const docsJustBuilt = currentDocModTime > cache.lastBuildTime - 1000; // 1 second tolerance
487
+
488
+ if (sourceChanged) {
489
+ console.log('📝 Source files have changed since last build, rebuilding...');
490
+ return true;
491
+ }
492
+
493
+ // If docs were just built (within last second), skip rebuild to avoid double-building
494
+ if (docsJustBuilt) {
495
+ console.log('✅ Documentation was just built, skipping rebuild to avoid double-build');
496
+ return false;
497
+ }
498
+
499
+ // Check file-level hashes if available
500
+ if (cache.fileHashes && Object.keys(cache.fileHashes).length > 0) {
501
+ const currentHashes = getExistingDocHashes();
502
+ let filesChanged = false;
503
+
504
+ // Check if any file hashes changed
505
+ for (const [relativePath, cachedHash] of Object.entries(cache.fileHashes)) {
506
+ const currentHash = currentHashes.get(relativePath);
507
+ if (currentHash !== cachedHash) {
508
+ filesChanged = true;
509
+ break;
510
+ }
511
+ }
512
+
513
+ // Check if any new files were added
514
+ if (!filesChanged && currentHashes.size !== Object.keys(cache.fileHashes).length) {
515
+ filesChanged = true;
516
+ }
517
+
518
+ if (!filesChanged && !sourceChanged) {
519
+ console.log('✅ Documentation is up to date (file hashes match cache), skipping rebuild');
520
+ return false;
521
+ }
522
+ }
523
+
524
+ // If cache exists and source hasn't changed, we're good
525
+ console.log('✅ Documentation is up to date (cache valid), skipping rebuild');
526
+ return false;
527
+ }
528
+
529
+ // No cache exists - compare source vs docs directly
530
+ if (currentSourceModTime <= currentDocModTime) {
531
+ console.log('✅ Documentation is up to date, skipping rebuild');
532
+ return false;
533
+ }
534
+
535
+ // Fallback: rebuild if we can't determine
536
+ console.log('📝 Unable to determine if rebuild needed, rebuilding to be safe...');
537
+ return true;
538
+ }
539
+
540
+ /**
541
+ * Get current package version from package.json
542
+ */
543
+ function getPackageVersion() {
544
+ try {
545
+ const packageJsonPath = join(PACKAGE_ROOT, 'package.json');
546
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
547
+ return packageJson.version || '';
548
+ } catch (err) {
549
+ return '';
550
+ }
551
+ }
552
+
553
+ /**
554
+ * Sort markdown list items for deterministic comparison
555
+ */
556
+ function sortMarkdownListItems(text) {
557
+ // Match markdown list sections (lines starting with - or *)
558
+ const lines = text.split('\n');
559
+ const sortedLines = [];
560
+ let currentList = [];
561
+ let inList = false;
562
+
563
+ for (const line of lines) {
564
+ const isListItem = /^\s*[-*]\s+/.test(line);
565
+
566
+ if (isListItem) {
567
+ if (!inList && currentList.length > 0) {
568
+ // Flush previous non-list content
569
+ sortedLines.push(...currentList);
570
+ currentList = [];
571
+ }
572
+ inList = true;
573
+ currentList.push(line);
574
+ } else {
575
+ if (inList && currentList.length > 0) {
576
+ // Sort and flush list
577
+ currentList.sort();
578
+ sortedLines.push(...currentList);
579
+ currentList = [];
580
+ }
581
+ inList = false;
582
+ currentList.push(line);
583
+ }
584
+ }
585
+
586
+ // Flush remaining content
587
+ if (currentList.length > 0) {
588
+ if (inList) {
589
+ currentList.sort();
590
+ }
591
+ sortedLines.push(...currentList);
592
+ }
593
+
594
+ return sortedLines.join('\n');
595
+ }
596
+
597
+ /**
598
+ * Normalize markdown link references for deterministic comparison
599
+ */
600
+ function normalizeMarkdownLinks(text) {
601
+ // Extract and sort link references [id]: url "title"
602
+ const linkRefPattern = /^\[([^\]]+)\]:\s*(.+)$/gm;
603
+ const links = [];
604
+ let match;
605
+
606
+ while ((match = linkRefPattern.exec(text)) !== null) {
607
+ links.push(match[0]);
608
+ }
609
+
610
+ if (links.length > 0) {
611
+ // Sort links by ID
612
+ links.sort();
613
+ // Replace all link references with sorted version
614
+ let normalized = text.replace(linkRefPattern, 'LINKREF');
615
+ // Append sorted links at the end
616
+ normalized = normalized.replace(/LINKREF/g, () => links.shift() || 'LINKREF');
617
+ return normalized;
618
+ }
619
+
620
+ return text;
621
+ }
622
+
623
+ /**
624
+ * Normalize "Defined in" links emitted by TypeDoc so that branch/remote differences
625
+ * don't cause hash mismatches between environments.
626
+ */
627
+ function normalizeDefinedInLinks(text) {
628
+ const definedInPattern = /\[(packages\/core\/[^\]]+?:\d+)\]\((https?:\/\/[^(]+?#L\d+)\)/g;
629
+ return text.replace(definedInPattern, (_match, displayText) => displayText);
630
+ }
631
+
632
+ /**
633
+ * Normalize content for comparison (remove trailing whitespace, normalize line endings, version numbers, timestamps)
634
+ */
635
+ function normalizeContent(content) {
636
+ let normalized = content
637
+ .replace(/\r\n/g, '\n') // Normalize line endings
638
+ .replace(/\r/g, '\n') // Handle old Mac line endings
639
+ .replace(/[ \t]+$/gm, '') // Remove trailing spaces/tabs
640
+ .replace(/\n{3,}/g, '\n\n'); // Normalize multiple blank lines
641
+
642
+ // Remove version numbers from TypeDoc-generated content
643
+ // TypeDoc includes version like "@jmruthers/pace-core@0.5.158"
644
+ // We normalize this to "@jmruthers/pace-core@VERSION" so version changes don't trigger updates
645
+ // This allows docs to remain unchanged when only the version number changes
646
+ normalized = normalized.replace(/@jmruthers\/pace-core@[\d.]+/g, '@jmruthers/pace-core@VERSION');
647
+ normalized = normalized.replace(/version\s+[\d.]+/gi, 'version VERSION');
648
+ // Also handle any other version patterns that might appear
649
+ normalized = normalized.replace(/\b[\d]+\.[\d]+\.[\d]+/g, 'VERSION');
650
+
651
+ // Remove timestamps and dates that might be generated dynamically
652
+ normalized = normalized.replace(/\d{4}-\d{2}-\d{2}/g, 'DATE'); // YYYY-MM-DD
653
+ normalized = normalized.replace(/\d{2}\/\d{2}\/\d{4}/g, 'DATE'); // MM/DD/YYYY
654
+ normalized = normalized.replace(/\d{2}:\d{2}:\d{2}/g, 'TIME'); // HH:MM:SS
655
+ normalized = normalized.replace(/Generated\s+on[^\n]*/gi, 'Generated on DATE');
656
+ normalized = normalized.replace(/Last\s+updated[^\n]*/gi, 'Last updated DATE');
657
+ normalized = normalized.replace(/Updated\s+[^\n]*/gi, 'Updated DATE');
658
+
659
+ // Remove any git commit hashes or SHAs that might appear
660
+ normalized = normalized.replace(/\b[0-9a-f]{7,40}\b/gi, 'HASH');
661
+
662
+ // Remove any ISO timestamps
663
+ normalized = normalized.replace(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[.\d]*Z?/g, 'TIMESTAMP');
664
+
665
+ // Normalize whitespace more aggressively
666
+ normalized = normalized.replace(/[ \t]+/g, ' '); // Multiple spaces/tabs to single space
667
+ normalized = normalized.replace(/ \n/g, '\n'); // Remove trailing spaces before newlines
668
+
669
+ // Sort markdown lists for deterministic comparison
670
+ normalized = sortMarkdownListItems(normalized);
671
+
672
+ // Normalize markdown link references
673
+ normalized = normalizeMarkdownLinks(normalized);
674
+
675
+ // Normalize "Defined in" GitHub links
676
+ normalized = normalizeDefinedInLinks(normalized);
677
+
678
+ return normalized.trim();
679
+ }
680
+
681
+ /**
682
+ * Get file content hash for comparison
683
+ */
684
+ function getFileHash(filePath) {
685
+ if (!existsSync(filePath)) {
686
+ return null;
687
+ }
688
+ try {
689
+ const content = readFileSync(filePath, 'utf-8');
690
+ const normalized = normalizeContent(content);
691
+ return createHash('md5').update(normalized).digest('hex');
692
+ } catch (err) {
693
+ return null;
694
+ }
695
+ }
696
+
697
+ /**
698
+ * Get hashes of all existing doc files
699
+ */
700
+ function getExistingDocHashes() {
701
+ const hashes = new Map();
702
+ if (!existsSync(DOCS_DIR)) {
703
+ return hashes;
704
+ }
705
+
706
+ const docFiles = scanDirectory(DOCS_DIR, ['.md']);
707
+ for (const filePath of docFiles) {
708
+ const relativePath = relative(DOCS_DIR, filePath);
709
+ const hash = getFileHash(filePath);
710
+ if (hash) {
711
+ hashes.set(relativePath, hash);
712
+ }
713
+ }
714
+
715
+ return hashes;
716
+ }
717
+
718
+ /**
719
+ * Get git status of docs directory
720
+ */
721
+ function getGitStatus(directory) {
722
+ try {
723
+ const result = execSync(`git status --porcelain ${directory}`, {
724
+ cwd: PACKAGE_ROOT,
725
+ encoding: 'utf-8',
726
+ stdio: 'pipe'
727
+ });
728
+ return result.trim().split('\n').filter(line => line.trim());
729
+ } catch (err) {
730
+ // Not a git repo or git not available
731
+ return [];
732
+ }
733
+ }
734
+
735
+ /**
736
+ * Run TypeDoc to generate documentation, but only update files that actually changed
737
+ */
738
+ function runTypeDoc(dryRun = false) {
739
+ if (dryRun) {
740
+ console.log('🔍 DRY-RUN MODE: Showing what would be updated without actually updating');
741
+ }
742
+ console.log('🔨 Running TypeDoc to generate documentation...');
743
+ console.log(`📝 Incremental build script v2.0 - Only updating files with actual content changes`);
744
+
745
+ // Get hashes of existing files before generation
746
+ // Version numbers are normalized out during hash calculation
747
+ const existingHashes = getExistingDocHashes();
748
+ const totalExistingFiles = existingHashes.size;
749
+ console.log(`📋 Found ${totalExistingFiles} existing documentation files to compare against`);
750
+
751
+ // Create temp directory for new docs
752
+ const tempDocsDir = join(PACKAGE_ROOT, '.docs-temp');
753
+
754
+ try {
755
+ // Backup current docs if they exist
756
+ const backupDir = join(PACKAGE_ROOT, '.docs-backup');
757
+ if (existsSync(DOCS_DIR)) {
758
+ if (existsSync(backupDir)) {
759
+ rmSync(backupDir, { recursive: true, force: true });
760
+ }
761
+ cpSync(DOCS_DIR, backupDir, { recursive: true });
762
+ }
763
+
764
+ // Run TypeDoc to temp directory first
765
+ const tempTypedocConfig = {
766
+ ...JSON.parse(readFileSync(join(PACKAGE_ROOT, 'typedoc.json'), 'utf-8')),
767
+ out: '.docs-temp'
768
+ };
769
+ const tempConfigPath = join(PACKAGE_ROOT, '.typedoc-temp.json');
770
+ writeFileSync(tempConfigPath, JSON.stringify(tempTypedocConfig, null, 2));
771
+
772
+ execSync(`npx typedoc --options ${tempConfigPath}`, {
773
+ cwd: PACKAGE_ROOT,
774
+ stdio: 'inherit'
775
+ });
776
+
777
+ // Compare and only copy changed files
778
+ let updatedCount = 0;
779
+ let unchangedCount = 0;
780
+
781
+ if (existsSync(tempDocsDir)) {
782
+ const newDocFiles = scanDirectory(tempDocsDir, ['.md']);
783
+
784
+ // Ensure docs directory exists
785
+ if (!existsSync(DOCS_DIR)) {
786
+ mkdirSync(DOCS_DIR, { recursive: true });
787
+ }
788
+
789
+ const updatedFiles = [];
790
+
791
+ for (const newFilePath of newDocFiles) {
792
+ const relativePath = relative(tempDocsDir, newFilePath);
793
+ const targetPath = join(DOCS_DIR, relativePath);
794
+ const newHash = getFileHash(newFilePath);
795
+ const oldHash = existingHashes.get(relativePath);
796
+
797
+ // CRITICAL: Only copy if normalized content hash changed
798
+ // If hash matches, do NOT write the file at all to prevent git from seeing it as modified
799
+ if (newHash !== oldHash) {
800
+ // Double-check: read existing file and compare normalized content
801
+ if (existsSync(targetPath)) {
802
+ const existingContent = readFileSync(targetPath, 'utf-8');
803
+ const existingNormalized = normalizeContent(existingContent);
804
+ const newContent = readFileSync(newFilePath, 'utf-8');
805
+ const newNormalized = normalizeContent(newContent);
806
+
807
+ // If normalized content is identical, skip write even if hash differs
808
+ // (this handles edge cases where hash calculation might differ)
809
+ if (existingNormalized === newNormalized) {
810
+ unchangedCount++;
811
+ continue;
812
+ }
813
+ }
814
+
815
+ // Ensure directory exists
816
+ const targetDir = dirname(targetPath);
817
+ if (!existsSync(targetDir)) {
818
+ mkdirSync(targetDir, { recursive: true });
819
+ }
820
+
821
+ // Copy file only if content actually changed
822
+ if (!dryRun) {
823
+ const content = readFileSync(newFilePath, 'utf-8');
824
+ writeFileSync(targetPath, content, 'utf-8');
825
+ }
826
+
827
+ updatedCount++;
828
+ updatedFiles.push(relativePath);
829
+ } else {
830
+ // Content is identical - DO NOT TOUCH THE FILE AT ALL
831
+ // This prevents git from seeing it as modified
832
+ // We don't restore timestamps or do anything - just leave it alone
833
+ unchangedCount++;
834
+ }
835
+ }
836
+
837
+ // Log which files were updated if any (only if debug mode or significant changes)
838
+ if (updatedCount > 0) {
839
+ if (process.env.DEBUG_DOCS || updatedCount <= 10) {
840
+ console.log(`\n📝 Files updated (${updatedCount}):`);
841
+ updatedFiles.slice(0, 10).forEach(file => console.log(` - ${file}`));
842
+ if (updatedFiles.length > 10) {
843
+ console.log(` ... and ${updatedFiles.length - 10} more`);
844
+ }
845
+ } else {
846
+ console.log(`\n📝 ${updatedCount} files were updated (use DEBUG_DOCS=1 to see list)`);
847
+ }
848
+ }
849
+
850
+ // Handle deleted files - files that existed before but don't exist in new generation
851
+ // (TypeDoc might remove some files if exports changed)
852
+ for (const [relativePath, oldHash] of existingHashes.entries()) {
853
+ const targetPath = join(DOCS_DIR, relativePath);
854
+ const newFilePath = join(tempDocsDir, relativePath);
855
+ if (!existsSync(newFilePath) && existsSync(targetPath)) {
856
+ // File was deleted by TypeDoc - remove it
857
+ rmSync(targetPath, { force: true });
858
+ console.log(` Deleted: ${relativePath}`);
859
+ }
860
+ }
861
+
862
+ // Clean up temp files
863
+ rmSync(tempDocsDir, { recursive: true, force: true });
864
+ if (existsSync(backupDir)) {
865
+ rmSync(backupDir, { recursive: true, force: true });
866
+ }
867
+ if (existsSync(tempConfigPath)) {
868
+ rmSync(tempConfigPath, { force: true });
869
+ }
870
+
871
+ console.log(`📊 Documentation update summary:`);
872
+ console.log(` Updated: ${updatedCount} files`);
873
+ console.log(` Unchanged: ${unchangedCount} files`);
874
+
875
+ // Verification step: Check if suspicious number of files changed
876
+ if (updatedCount > 50 && updatedCount === newDocFiles.length) {
877
+ console.log(`\n⚠️ WARNING: All ${updatedCount} files appear to have changed!`);
878
+ console.log(` This might indicate TypeDoc is generating non-deterministic content.`);
879
+ console.log(` Check if source files actually changed, or if TypeDoc config changed.`);
880
+
881
+ // Additional verification: Check if source files actually changed
882
+ const cache = loadCache();
883
+ const currentSourceModTime = getLatestSourceModTime();
884
+ if (cache.lastBuildTime > 0 && currentSourceModTime <= cache.sourceModTime) {
885
+ console.log(`\n🔍 VERIFICATION: Source files have NOT changed since last build.`);
886
+ console.log(` This suggests TypeDoc may be generating non-deterministic output.`);
887
+ console.log(` Files were updated but source is unchanged - this is unexpected.`);
888
+ }
889
+ }
890
+
891
+ // Verification: If many files changed but normalized content matches, warn
892
+ if (updatedCount > 0 && unchangedCount > 0) {
893
+ const changeRatio = updatedCount / (updatedCount + unchangedCount);
894
+ if (changeRatio > 0.8) {
895
+ console.log(`\n⚠️ WARNING: ${Math.round(changeRatio * 100)}% of files were updated.`);
896
+ console.log(` This is unusually high. Verify source files actually changed.`);
897
+ }
898
+ }
899
+
900
+ // Get final file hashes after update
901
+ const finalHashes = getExistingDocHashes();
902
+ const fileHashesObj = {};
903
+ for (const [relativePath, hash] of finalHashes.entries()) {
904
+ fileHashesObj[relativePath] = hash;
905
+ }
906
+
907
+ return { success: true, fileHashes: fileHashesObj };
908
+ }
909
+
910
+ return { success: true, fileHashes: {} };
911
+ } catch (error) {
912
+ // Clean up on error
913
+ if (existsSync(tempDocsDir)) {
914
+ rmSync(tempDocsDir, { recursive: true, force: true });
915
+ }
916
+ const tempConfigPath = join(PACKAGE_ROOT, '.typedoc-temp.json');
917
+ if (existsSync(tempConfigPath)) {
918
+ rmSync(tempConfigPath, { force: true });
919
+ }
920
+ console.error('❌ TypeDoc failed:', error.message);
921
+ return { success: false, fileHashes: {} };
922
+ }
923
+ }
924
+
925
+ /**
926
+ * Main function
927
+ */
928
+ function main() {
929
+ const forceRebuild = process.argv.includes('--force');
930
+ const dryRun = process.argv.includes('--dry-run');
931
+ const verifyGit = process.argv.includes('--verify-git');
932
+
933
+ if (forceRebuild) {
934
+ console.log('🔄 Force rebuild requested, regenerating all documentation...');
935
+ const result = runTypeDoc(dryRun);
936
+ if (result.success) {
937
+ const currentSourceHashes = getSourceFileHashes();
938
+ const sourceHashesObj = {};
939
+ for (const [relativePath, hash] of currentSourceHashes.entries()) {
940
+ sourceHashesObj[relativePath] = hash;
941
+ }
942
+
943
+ const cache = {
944
+ lastBuildTime: Date.now(),
945
+ sourceModTime: getLatestSourceModTime(),
946
+ docModTime: getLatestDocModTime(),
947
+ fileHashes: result.fileHashes || {},
948
+ sourceFileHashes: sourceHashesObj
949
+ };
950
+ if (!dryRun) {
951
+ saveCache(cache);
952
+ }
953
+ console.log('✅ Documentation generation completed');
954
+ }
955
+ process.exit(result.success ? 0 : 1);
956
+ return;
957
+ }
958
+
959
+ // Aggressive pre-check: Skip TypeDoc entirely if source unchanged
960
+ if (!shouldRunTypeDoc()) {
961
+ console.log('⏭️ Skipping TypeDoc - source files unchanged and docs are up to date');
962
+ process.exit(0);
963
+ return;
964
+ }
965
+
966
+ // Get git status before if verifying
967
+ let gitStatusBefore = [];
968
+ if (verifyGit) {
969
+ gitStatusBefore = getGitStatus(DOCS_DIR);
970
+ console.log(`📋 Git status before: ${gitStatusBefore.length} modified files`);
971
+ }
972
+
973
+ if (needsRebuild()) {
974
+ const result = runTypeDoc(dryRun);
975
+ if (result.success) {
976
+ const currentSourceHashes = getSourceFileHashes();
977
+ const sourceHashesObj = {};
978
+ for (const [relativePath, hash] of currentSourceHashes.entries()) {
979
+ sourceHashesObj[relativePath] = hash;
980
+ }
981
+
982
+ const cache = {
983
+ lastBuildTime: Date.now(),
984
+ sourceModTime: getLatestSourceModTime(),
985
+ docModTime: getLatestDocModTime(),
986
+ fileHashes: result.fileHashes || {},
987
+ sourceFileHashes: sourceHashesObj
988
+ };
989
+ if (!dryRun) {
990
+ saveCache(cache);
991
+ }
992
+
993
+ // Verify git status after if requested
994
+ if (verifyGit && !dryRun) {
995
+ const gitStatusAfter = getGitStatus(DOCS_DIR);
996
+ const newChanges = gitStatusAfter.filter(file => !gitStatusBefore.includes(file));
997
+ if (newChanges.length > 0 && result.updatedCount === 0) {
998
+ console.log(`\n⚠️ WARNING: Git shows ${newChanges.length} files changed but script reported 0 updates`);
999
+ console.log(` This suggests files were modified unnecessarily`);
1000
+ } else if (newChanges.length !== result.updatedCount) {
1001
+ console.log(`\n📊 Git verification: ${newChanges.length} files changed in git, ${result.updatedCount} reported by script`);
1002
+ }
1003
+ }
1004
+
1005
+ console.log('✅ Documentation generation completed');
1006
+ }
1007
+ process.exit(result.success ? 0 : 1);
1008
+ } else {
1009
+ console.log('⏭️ Skipping documentation generation (no changes detected)');
1010
+ process.exit(0);
1011
+ }
1012
+ }
1013
+
1014
+ main();
1015
+