@taskforcehq/taskforce 0.3.305 → 0.3.306

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 (337) hide show
  1. package/dist/Taskforce.module.css +295 -89
  2. package/dist/TaskforceCore.js +215 -43
  3. package/dist/TaskforceCore.test.js +433 -28
  4. package/dist/assets/fonts/CourierPrime-Bold.woff2 +0 -0
  5. package/dist/assets/fonts/CourierPrime-BoldItalic.woff2 +0 -0
  6. package/dist/assets/fonts/CourierPrime-Italic.woff2 +0 -0
  7. package/dist/assets/fonts/CourierPrime-Regular.woff2 +0 -0
  8. package/dist/assets/fonts/Noir_medium.woff2 +0 -0
  9. package/dist/assets/fonts/Noir_regular.woff2 +0 -0
  10. package/dist/assets/fonts/RussoOne-Regular.woff2 +0 -0
  11. package/dist/assets/fonts/SpecialElite-Regular.woff2 +0 -0
  12. package/dist/compat/workspaceSyncCompat.js +46 -2
  13. package/dist/compat/workspaceSyncCompat.test.js +38 -1
  14. package/dist/components/context/ContextAttachmentManager.d.ts +14 -0
  15. package/dist/components/context/ContextAttachmentManager.js +549 -0
  16. package/dist/components/features/AgentsModule.d.ts +10 -1
  17. package/dist/components/features/AgentsModule.js +260 -15
  18. package/dist/components/features/AgentsModule.test.js +255 -5
  19. package/dist/components/features/DocumentViewer.d.ts +5 -1
  20. package/dist/components/features/DocumentViewer.js +60 -3
  21. package/dist/components/features/DocumentWorkspace.js +1 -1
  22. package/dist/components/features/DocumentWorkspace.test.js +33 -0
  23. package/dist/components/features/documentWorkspaceModel.d.ts +1 -1
  24. package/dist/components/task/TaskCard.js +19 -28
  25. package/dist/components/task/TaskCard.test.js +38 -3
  26. package/dist/components/task/TaskContextUpload.js +4 -526
  27. package/dist/components/task/TaskForm.js +85 -90
  28. package/dist/components/task/TaskForm.test.js +110 -8
  29. package/dist/components/task/TaskKanban.d.ts +2 -1
  30. package/dist/components/task/TaskKanban.js +94 -13
  31. package/dist/components/task/TaskKanban.test.js +36 -0
  32. package/dist/components/ui/EditableAvatarButton.d.ts +18 -0
  33. package/dist/components/ui/EditableAvatarButton.js +18 -0
  34. package/dist/components/views/PlanComparisonPage.d.ts +9 -1
  35. package/dist/components/views/PlanComparisonPage.js +70 -14
  36. package/dist/components/views/PlanComparisonPage.test.js +238 -0
  37. package/dist/components/views/PlansPage.js +127 -36
  38. package/dist/components/views/PlansPage.test.js +294 -12
  39. package/dist/components/views/StandaloneLayout.js +283 -533
  40. package/dist/components/views/panels/PlanningDrawer.d.ts +8 -1
  41. package/dist/components/views/panels/PlanningDrawer.js +18 -11
  42. package/dist/components/views/panels/PlanningDrawer.test.d.ts +1 -0
  43. package/dist/components/views/panels/PlanningDrawer.test.js +141 -0
  44. package/dist/components/views/planningScope.d.ts +20 -0
  45. package/dist/components/views/planningScope.js +25 -0
  46. package/dist/components/views/planningScope.test.d.ts +1 -0
  47. package/dist/components/views/planningScope.test.js +60 -0
  48. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +26 -1
  49. package/dist/components/views/standalone/modals/AccountHubModal.js +76 -2
  50. package/dist/components/views/standalone/modals/AccountHubModal.test.js +20 -3
  51. package/dist/components/views/standalone/modals/AvatarImageManagerModal.d.ts +20 -0
  52. package/dist/components/views/standalone/modals/AvatarImageManagerModal.js +127 -0
  53. package/dist/components/views/standalone/modals/EditProfileModal.d.ts +2 -9
  54. package/dist/components/views/standalone/modals/EditProfileModal.js +3 -2
  55. package/dist/components/views/standalone/modals/SyncEnableWarningModal.d.ts +9 -0
  56. package/dist/components/views/standalone/modals/SyncEnableWarningModal.js +6 -0
  57. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.d.ts +1 -0
  58. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.js +24 -0
  59. package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +2 -1
  60. package/dist/components/views/standalone/modals/SyncStatusModal.js +2 -2
  61. package/dist/core/AiProfileService.d.ts +39 -2
  62. package/dist/core/AiProfileService.js +639 -73
  63. package/dist/core/AiProfileService.test.js +19 -1
  64. package/dist/core/AiProfiles.test.js +566 -7
  65. package/dist/core/AttachmentLinkService.js +57 -15
  66. package/dist/core/AuthIdentityService.d.ts +107 -0
  67. package/dist/core/AuthIdentityService.js +284 -0
  68. package/dist/core/AuthTokenService.d.ts +4 -0
  69. package/dist/core/AuthTokenService.js +34 -6
  70. package/dist/core/AuthTokenService.test.js +3 -3
  71. package/dist/core/EntitlementsPolicy.test.js +2 -2
  72. package/dist/core/GlobalSettingsService.js +115 -6
  73. package/dist/core/PlanEntitlementService.d.ts +23 -2
  74. package/dist/core/PlanEntitlementService.js +343 -57
  75. package/dist/core/PlanFeatureCatalog.test.d.ts +1 -0
  76. package/dist/core/PlanFeatureCatalog.test.js +84 -0
  77. package/dist/core/PlanVersionPolicy.test.js +35 -2
  78. package/dist/core/PlanningEntities.test.js +52 -0
  79. package/dist/core/SignupMonetizationSettings.test.js +199 -27
  80. package/dist/core/SyncReconciliationService.js +2 -65
  81. package/dist/core/SystemAdmin.test.js +42 -42
  82. package/dist/core/TaskAttachmentsCanonical.test.js +51 -1
  83. package/dist/core/Taskforce.d.ts +225 -14
  84. package/dist/core/Taskforce.js +765 -192
  85. package/dist/core/Taskforce.listTasksSlim.test.d.ts +1 -0
  86. package/dist/core/Taskforce.listTasksSlim.test.js +91 -0
  87. package/dist/core/UserProfileAvatarDrafts.test.js +30 -3
  88. package/dist/core/WorkspaceLifecycleService.js +0 -5
  89. package/dist/core/WorkspacePermissions.test.js +25 -1
  90. package/dist/core/WorkspacePlanMode.test.js +117 -16
  91. package/dist/core/shared.d.ts +1 -1
  92. package/dist/core/shared.js +5 -1
  93. package/dist/core/types.d.ts +68 -2
  94. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +26 -1
  95. package/dist/hooks/sync/bootstrap.js +2 -0
  96. package/dist/hooks/sync/controlPlane.d.ts +37 -0
  97. package/dist/hooks/sync/controlPlane.js +78 -0
  98. package/dist/hooks/sync/controlPlane.test.d.ts +1 -0
  99. package/dist/hooks/sync/controlPlane.test.js +121 -0
  100. package/dist/hooks/sync/lifecyclePolicy.d.ts +45 -0
  101. package/dist/hooks/sync/lifecyclePolicy.js +93 -0
  102. package/dist/hooks/sync/lifecyclePolicy.test.d.ts +1 -0
  103. package/dist/hooks/sync/lifecyclePolicy.test.js +124 -0
  104. package/dist/hooks/sync/orchestratorShared.d.ts +10 -1
  105. package/dist/hooks/sync/orchestratorShared.js +68 -47
  106. package/dist/hooks/sync/orchestratorShared.test.js +50 -1
  107. package/dist/hooks/sync/pullLifecycle.d.ts +31 -0
  108. package/dist/hooks/sync/pullLifecycle.js +24 -0
  109. package/dist/hooks/sync/pullLifecycle.test.d.ts +1 -0
  110. package/dist/hooks/sync/pullLifecycle.test.js +80 -0
  111. package/dist/hooks/sync/recovery.d.ts +16 -2
  112. package/dist/hooks/sync/recovery.js +171 -53
  113. package/dist/hooks/sync/recovery.test.d.ts +1 -0
  114. package/dist/hooks/sync/recovery.test.js +292 -0
  115. package/dist/hooks/sync/transfers.d.ts +15 -3
  116. package/dist/hooks/sync/transfers.js +179 -62
  117. package/dist/hooks/sync/transfers.test.d.ts +1 -0
  118. package/dist/hooks/sync/transfers.test.js +396 -0
  119. package/dist/hooks/sync/useRecentSyncEvents.d.ts +21 -0
  120. package/dist/hooks/sync/useRecentSyncEvents.js +92 -0
  121. package/dist/hooks/sync/useRecentSyncEvents.test.d.ts +1 -0
  122. package/dist/hooks/sync/useRecentSyncEvents.test.js +124 -0
  123. package/dist/hooks/sync/useSyncStatusActions.d.ts +30 -0
  124. package/dist/hooks/sync/useSyncStatusActions.js +88 -0
  125. package/dist/hooks/sync/useSyncStatusActions.test.d.ts +1 -0
  126. package/dist/hooks/sync/useSyncStatusActions.test.js +133 -0
  127. package/dist/hooks/sync/useSyncStatusControls.d.ts +25 -0
  128. package/dist/hooks/sync/useSyncStatusControls.js +60 -0
  129. package/dist/hooks/sync/useSyncStatusControls.test.d.ts +1 -0
  130. package/dist/hooks/sync/useSyncStatusControls.test.js +88 -0
  131. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +12 -0
  132. package/dist/hooks/useSyncOrchestrator.d.ts +9 -2
  133. package/dist/hooks/useSyncOrchestrator.js +359 -178
  134. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +407 -0
  135. package/dist/hooks/useTaskData.js +8 -3
  136. package/dist/hooks/useTaskData.test.js +45 -0
  137. package/dist/hooks/useTaskforce.d.ts +60 -2
  138. package/dist/hooks/useTaskforce.js +549 -240
  139. package/dist/hooks/useTaskforce.runtime-routing.test.d.ts +1 -0
  140. package/dist/hooks/useTaskforce.runtime-routing.test.js +152 -0
  141. package/dist/hooks/useTaskforce.sync-behavior.test.js +2380 -212
  142. package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
  143. package/dist/hooks/useWorkspaceSyncController.js +6 -1
  144. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +16 -0
  145. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +316 -35
  146. package/dist/localization/locales/en-US.d.ts +0 -1
  147. package/dist/localization/locales/en-US.js +0 -1
  148. package/dist/localization/locales/es-419.js +0 -1
  149. package/dist/localization/locales/pt-BR.js +0 -1
  150. package/dist/mcp/documentAssetRegistrar.js +3 -3
  151. package/dist/mcp/runtime.d.ts +2 -0
  152. package/dist/mcp/runtime.js +131 -64
  153. package/dist/mcp/runtime.test.js +291 -12
  154. package/dist/migrations/taskSchemaMigrations.d.ts +1 -1
  155. package/dist/migrations/taskSchemaMigrations.js +171 -61
  156. package/dist/migrations/taskSchemaMigrations.test.js +15 -0
  157. package/dist/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
  158. package/dist/resources/templates/workflows/collaborate.yaml +1 -1
  159. package/dist/resources/templates/workflows/evaluate.yaml +2 -2
  160. package/dist/resources/templates/workflows/plan.yaml +1 -1
  161. package/dist/resources/templates/workflows/review.yaml +2 -2
  162. package/dist/server/annotatedAttachmentsRoutes.test.js +3 -3
  163. package/dist/server/auth/providers/apple.d.ts +33 -0
  164. package/dist/server/auth/providers/apple.js +82 -0
  165. package/dist/server/auth/providers/appleClientSecret.d.ts +37 -0
  166. package/dist/server/auth/providers/appleClientSecret.js +65 -0
  167. package/dist/server/auth/providers/github.d.ts +26 -0
  168. package/dist/server/auth/providers/github.js +86 -0
  169. package/dist/server/auth/providers/google.d.ts +21 -0
  170. package/dist/server/auth/providers/google.js +56 -0
  171. package/dist/server/auth/providers/types.d.ts +21 -0
  172. package/dist/server/auth/providers/types.js +1 -0
  173. package/dist/server/auth.d.ts +2 -0
  174. package/dist/server/auth.js +6 -3
  175. package/dist/server/documentReviewRoutes.test.js +36 -3
  176. package/dist/server/index.cookieProxy.test.js +1 -0
  177. package/dist/server/index.d.ts +12 -0
  178. package/dist/server/index.js +120 -9
  179. package/dist/server/index.rateLimit.test.js +3 -0
  180. package/dist/server/index.test.js +106 -3
  181. package/dist/server/routes/admin.d.ts +3 -0
  182. package/dist/server/routes/admin.js +272 -19
  183. package/dist/server/routes/auth.d.ts +15 -2
  184. package/dist/server/routes/auth.js +994 -57
  185. package/dist/server/routes/authSupport.d.ts +1 -1
  186. package/dist/server/routes/authSupport.js +1 -1
  187. package/dist/server/routes/billing.js +361 -35
  188. package/dist/server/routes/billing.test.js +592 -12
  189. package/dist/server/routes/documents.d.ts +1 -0
  190. package/dist/server/routes/documents.js +172 -70
  191. package/dist/server/routes/shared.d.ts +28 -2
  192. package/dist/server/routes/shared.js +69 -4
  193. package/dist/server/routes/sync.integration.test.js +437 -39
  194. package/dist/server/routes/sync.js +61 -605
  195. package/dist/server/routes/syncAuxRoutes.d.ts +15 -0
  196. package/dist/server/routes/syncAuxRoutes.js +91 -0
  197. package/dist/server/routes/syncAuxRoutes.test.d.ts +1 -0
  198. package/dist/server/routes/syncAuxRoutes.test.js +158 -0
  199. package/dist/server/routes/syncPullApplyRoutes.d.ts +36 -0
  200. package/dist/server/routes/syncPullApplyRoutes.js +204 -0
  201. package/dist/server/routes/syncPushRoutes.d.ts +24 -0
  202. package/dist/server/routes/syncPushRoutes.js +212 -0
  203. package/dist/server/routes/syncRouteGuards.d.ts +36 -0
  204. package/dist/server/routes/syncRouteGuards.js +67 -0
  205. package/dist/server/routes/syncRouteGuards.test.d.ts +1 -0
  206. package/dist/server/routes/syncRouteGuards.test.js +94 -0
  207. package/dist/server/routes/syncRouteTypes.d.ts +13 -0
  208. package/dist/server/routes/syncRouteTypes.js +1 -0
  209. package/dist/server/routes/syncSnapshotRoutes.d.ts +66 -0
  210. package/dist/server/routes/syncSnapshotRoutes.js +180 -0
  211. package/dist/server/routes/tasks.js +43 -1
  212. package/dist/server/routes/workspaces.js +36 -5
  213. package/dist/server/routes.js +60 -3
  214. package/dist/server/routes.test.js +1824 -171
  215. package/dist/services/workflowExportSnapshots.test.js +2 -0
  216. package/dist/shared/aiProfileSeatScope.d.ts +5 -0
  217. package/dist/shared/aiProfileSeatScope.js +21 -0
  218. package/dist/shared/passwordPolicy.d.ts +13 -0
  219. package/dist/shared/passwordPolicy.js +84 -0
  220. package/dist/shared/passwordPolicy.test.d.ts +1 -0
  221. package/dist/shared/passwordPolicy.test.js +34 -0
  222. package/dist/storage/workspaceAssetStore.d.ts +15 -3
  223. package/dist/storage/workspaceAssetStore.js +132 -44
  224. package/dist/storage/workspaceAssetStore.test.js +93 -6
  225. package/dist/styles/fonts.css +70 -0
  226. package/dist/sync/collaborationSyncPayload.d.ts +6 -1
  227. package/dist/sync/collaborationSyncPayload.js +17 -11
  228. package/dist/sync/collaborationSyncPayload.test.js +13 -1
  229. package/dist/sync/contentSyncState.d.ts +4 -8
  230. package/dist/sync/contentSyncState.js +17 -70
  231. package/dist/sync/syncApplyHandlers.d.ts +7 -0
  232. package/dist/sync/syncApplyHandlers.js +28 -13
  233. package/dist/sync/syncApplyHandlers.test.js +70 -3
  234. package/dist/sync/syncFieldSemantics.d.ts +12 -0
  235. package/dist/sync/syncFieldSemantics.js +55 -0
  236. package/dist/sync/syncResourceAdapters.d.ts +44 -0
  237. package/dist/sync/syncResourceAdapters.js +77 -0
  238. package/dist/sync/syncService.d.ts +5 -0
  239. package/dist/sync/syncService.js +52 -0
  240. package/dist/sync/syncService.test.js +57 -3
  241. package/dist/sync/taskSyncChangeKey.d.ts +2 -0
  242. package/dist/sync/taskSyncChangeKey.js +143 -0
  243. package/dist/sync/taskSyncPayload.js +3 -8
  244. package/dist/sync/workspacePullFeed.js +4 -3
  245. package/dist/sync/workspacePullFeed.test.js +43 -0
  246. package/dist/sync/workspaceRepair.js +5 -2
  247. package/dist/sync/workspaceSyncModel.d.ts +45 -0
  248. package/dist/sync/workspaceSyncModel.js +183 -28
  249. package/dist/sync/workspaceSyncModel.test.js +291 -12
  250. package/dist/sync/workspaceSyncState.d.ts +17 -0
  251. package/dist/sync/workspaceSyncState.js +2 -1
  252. package/dist/sync/workspaceSyncSurface.js +3 -8
  253. package/dist/types.d.ts +17 -0
  254. package/dist/ui/assets/AgentsModule-DrLawwNl.js +1 -0
  255. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-BG6P_GVW.js → AnnotatedAttachmentWorkspace-Z9_whf7F.js} +1 -1
  256. package/dist/ui/assets/CourierPrime-Bold-BuLj_dpw.woff2 +0 -0
  257. package/dist/ui/assets/CourierPrime-BoldItalic-BrK2pUkZ.woff2 +0 -0
  258. package/dist/ui/assets/CourierPrime-Italic-DFUZK5Ey.woff2 +0 -0
  259. package/dist/ui/assets/CourierPrime-Regular-BsKphzVf.woff2 +0 -0
  260. package/dist/ui/assets/DocumentWorkspace-B03MaSkw.js +252 -0
  261. package/dist/ui/assets/DocumentWorkspace-Dhdso07p.css +1 -0
  262. package/dist/ui/assets/{InitiativesModule-Dhm7M8e7.js → InitiativesModule-9E6ParrY.js} +1 -1
  263. package/dist/ui/assets/Noir_medium-bxQwKGzB.woff2 +0 -0
  264. package/dist/ui/assets/Noir_regular-ojf2kxlG.woff2 +0 -0
  265. package/dist/ui/assets/PlansPage-CvfnMiWq.css +1 -0
  266. package/dist/ui/assets/PlansPage-DRXscYxH.js +1 -0
  267. package/dist/ui/assets/RussoOne-Regular-Cu_qq_qC.woff2 +0 -0
  268. package/dist/ui/assets/SpecialElite-Regular-Di6gSvAS.woff2 +0 -0
  269. package/dist/ui/assets/{TaskSettings-ln0aF7xc.js → TaskSettings-DZX4jk7e.js} +1 -1
  270. package/dist/ui/assets/{WorkflowsModule-BcS4afRn.js → WorkflowsModule-BA7jcEpH.js} +1 -1
  271. package/dist/ui/assets/index-9eT8e0Lu.css +1 -0
  272. package/dist/ui/assets/index-C6k75jr8.js +6 -0
  273. package/dist/ui/assets/{vendor-icons-BSUaRwF8.js → vendor-icons-DuEd_65c.js} +1 -1
  274. package/dist/ui/fonts/CourierPrime-Bold.ttf +0 -0
  275. package/dist/ui/fonts/CourierPrime-Bold.woff2 +0 -0
  276. package/dist/ui/fonts/CourierPrime-BoldItalic.ttf +0 -0
  277. package/dist/ui/fonts/CourierPrime-BoldItalic.woff2 +0 -0
  278. package/dist/ui/fonts/CourierPrime-Italic.ttf +0 -0
  279. package/dist/ui/fonts/CourierPrime-Italic.woff2 +0 -0
  280. package/dist/ui/fonts/CourierPrime-Regular.ttf +0 -0
  281. package/dist/ui/fonts/CourierPrime-Regular.woff2 +0 -0
  282. package/dist/ui/fonts/Noir_medium.otf +0 -0
  283. package/dist/ui/fonts/Noir_medium.woff2 +0 -0
  284. package/dist/ui/fonts/Noir_regular.otf +0 -0
  285. package/dist/ui/fonts/Noir_regular.woff2 +0 -0
  286. package/dist/ui/fonts/RussoOne-Regular.woff2 +0 -0
  287. package/dist/ui/fonts/SpecialElite-Regular.ttf +0 -0
  288. package/dist/ui/fonts/SpecialElite-Regular.woff2 +0 -0
  289. package/dist/ui/index.html +3 -3
  290. package/dist/utils/accountProfileSummaryCache.d.ts +5 -0
  291. package/dist/utils/assignees.d.ts +2 -0
  292. package/dist/utils/assignees.js +2 -0
  293. package/dist/utils/avatarUpload.d.ts +27 -0
  294. package/dist/utils/avatarUpload.js +132 -0
  295. package/dist/utils/avatarUpload.test.d.ts +1 -0
  296. package/dist/utils/avatarUpload.test.js +40 -0
  297. package/dist/utils/bootstrapTrace.d.ts +7 -0
  298. package/dist/utils/bootstrapTrace.js +29 -0
  299. package/dist/utils/commercialLifecyclePresentation.js +3 -0
  300. package/dist/utils/contextAssetEvents.d.ts +2 -0
  301. package/dist/utils/syncEventDetails.d.ts +3 -0
  302. package/dist/utils/syncEventDetails.js +64 -0
  303. package/dist/utils/syncEventDetails.test.d.ts +1 -0
  304. package/dist/utils/syncEventDetails.test.js +23 -0
  305. package/dist/utils/syncEventPresentation.d.ts +15 -0
  306. package/dist/utils/syncEventPresentation.js +129 -0
  307. package/dist/utils/syncEventPresentation.test.d.ts +1 -0
  308. package/dist/utils/syncEventPresentation.test.js +64 -0
  309. package/dist/utils/syncStatusPresentation.d.ts +72 -0
  310. package/dist/utils/syncStatusPresentation.js +217 -0
  311. package/dist/utils/syncStatusPresentation.test.d.ts +1 -0
  312. package/dist/utils/syncStatusPresentation.test.js +164 -0
  313. package/dist/utils/taskActivity.d.ts +2 -0
  314. package/dist/utils/taskActivity.js +228 -22
  315. package/dist/utils/taskActivity.test.js +119 -8
  316. package/dist/utils/workspaceSyncPresentation.d.ts +4 -0
  317. package/dist/utils/workspaceSyncPresentation.js +68 -2
  318. package/dist/utils/workspaceSyncPresentation.test.js +36 -1
  319. package/package.json +3 -2
  320. package/scripts/export-sqlite-to-postgres.mjs +7 -9
  321. package/scripts/run-e2e-realtime.sh +17 -3
  322. package/scripts/run-smoke.sh +17 -5
  323. package/scripts/run-soak.sh +17 -5
  324. package/src/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
  325. package/src/resources/templates/workflows/collaborate.yaml +1 -1
  326. package/src/resources/templates/workflows/evaluate.yaml +2 -2
  327. package/src/resources/templates/workflows/plan.yaml +1 -1
  328. package/src/resources/templates/workflows/review.yaml +2 -2
  329. package/dist/ui/assets/AgentsModule-N2MnQBZk.js +0 -1
  330. package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +0 -1
  331. package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +0 -252
  332. package/dist/ui/assets/PlansPage-C2dd2n1X.css +0 -1
  333. package/dist/ui/assets/PlansPage-Cq0zXj3I.js +0 -1
  334. package/dist/ui/assets/RussoOne-Regular-C3BxZIj7.ttf +0 -0
  335. package/dist/ui/assets/index-C2-OXZV7.css +0 -1
  336. package/dist/ui/assets/index-DcSI5F86.js +0 -6
  337. package/scripts/migrate-planning-model.ts +0 -233
@@ -0,0 +1,84 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import fs from 'fs';
3
+ import os from 'os';
4
+ import path from 'path';
5
+ import { TaskforceCore } from './Taskforce';
6
+ function makeProjectRoot() {
7
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'taskforce-plan-feature-catalog-'));
8
+ }
9
+ describe('TaskforceCore plan feature catalog marketing copy', () => {
10
+ it('persists public marketing copy overrides and falls back after clearing them', () => {
11
+ const projectRoot = makeProjectRoot();
12
+ const core = new TaskforceCore({
13
+ projectRoot,
14
+ storagePath: path.join(projectRoot, '.taskforce')
15
+ });
16
+ const initialFeature = core.listPlanFeatureCatalog().find((feature) => feature.featureKey === 'collaboration.ai_profiles');
17
+ expect(initialFeature?.label).toBe('AI Profiles');
18
+ expect(initialFeature?.publicLabel ?? null).toBeNull();
19
+ expect(initialFeature?.publicDescription ?? null).toBeNull();
20
+ expect(initialFeature?.publicDisplayOrder).toBe(20);
21
+ const updated = core.updatePlanFeatureCatalogMarketingCopy({
22
+ featureKey: 'collaboration.ai_profiles',
23
+ publicLabel: 'AI teammates',
24
+ publicDescription: 'Add reusable AI teammates to your workspace.',
25
+ publicDisplayOrder: 24
26
+ });
27
+ expect(updated.publicLabel).toBe('AI teammates');
28
+ expect(updated.publicDescription).toBe('Add reusable AI teammates to your workspace.');
29
+ expect(updated.publicDisplayOrder).toBe(24);
30
+ const persisted = core.listPlanFeatureCatalog().find((feature) => feature.featureKey === 'collaboration.ai_profiles');
31
+ expect(persisted?.publicLabel).toBe('AI teammates');
32
+ expect(persisted?.publicDescription).toBe('Add reusable AI teammates to your workspace.');
33
+ expect(persisted?.publicDisplayOrder).toBe(24);
34
+ const cleared = core.updatePlanFeatureCatalogMarketingCopy({
35
+ featureKey: 'collaboration.ai_profiles',
36
+ publicLabel: ' ',
37
+ publicDescription: '',
38
+ publicDisplayOrder: null
39
+ });
40
+ expect(cleared.publicLabel ?? null).toBeNull();
41
+ expect(cleared.publicDescription ?? null).toBeNull();
42
+ expect(cleared.publicDisplayOrder).toBe(20);
43
+ const fallback = core.listPlanFeatureCatalog().find((feature) => feature.featureKey === 'collaboration.ai_profiles');
44
+ expect(fallback?.label).toBe('AI Profiles');
45
+ expect(fallback?.publicLabel ?? null).toBeNull();
46
+ expect(fallback?.publicDescription ?? null).toBeNull();
47
+ expect(fallback?.publicDisplayOrder).toBe(20);
48
+ core.close();
49
+ fs.rmSync(projectRoot, { recursive: true, force: true });
50
+ });
51
+ it('migrates legacy workspace feature catalog overrides to the canonical key', () => {
52
+ const projectRoot = makeProjectRoot();
53
+ const core = new TaskforceCore({
54
+ projectRoot,
55
+ storagePath: path.join(projectRoot, '.taskforce')
56
+ });
57
+ const db = core.db;
58
+ db.prepare(`
59
+ INSERT INTO plan_feature_catalog_overrides (
60
+ tenant_id,
61
+ feature_key,
62
+ public_label,
63
+ public_description,
64
+ public_display_order,
65
+ created_at,
66
+ updated_at
67
+ ) VALUES (?, 'workspace.switching', ?, ?, ?, ?, ?)
68
+ `).run('default', 'Workspace access', 'Let a user create and switch between multiple workspaces.', 14, '2026-01-01T00:00:00.000Z', '2026-01-01T00:00:00.000Z');
69
+ core.planEntitlementService.normalizeLegacyPlanDataIfNeeded();
70
+ const feature = core.listPlanFeatureCatalog().find((item) => item.featureKey === 'workspace.workspaces');
71
+ expect(feature?.publicLabel).toBe('Workspace access');
72
+ expect(feature?.publicDescription).toBe('Let a user create and switch between multiple workspaces.');
73
+ expect(feature?.publicDisplayOrder).toBe(14);
74
+ const rows = db.prepare(`
75
+ SELECT feature_key
76
+ FROM plan_feature_catalog_overrides
77
+ WHERE tenant_id = ?
78
+ `).all('default');
79
+ expect(rows.some((row) => row.feature_key === 'workspace.switching')).toBe(false);
80
+ expect(rows.some((row) => row.feature_key === 'workspace.workspaces')).toBe(true);
81
+ core.close();
82
+ fs.rmSync(projectRoot, { recursive: true, force: true });
83
+ });
84
+ });
@@ -141,11 +141,11 @@ describe('TaskforceCore plan version policy', () => {
141
141
  const version = core.updatePlanVersionFeatures({
142
142
  planVersionId: 'free-v1',
143
143
  features: [
144
- { featureKey: 'workspace.switching', access: 'limited', config: { maxWorkspaces: 0 } },
144
+ { featureKey: 'workspace.workspaces', access: 'limited', config: { maxWorkspaces: 0 } },
145
145
  { featureKey: 'context.uploads', access: 'limited', config: { storageLimitMb: -50 } }
146
146
  ]
147
147
  });
148
- expect(version.features['workspace.switching']?.config).toEqual({ maxWorkspaces: 1 });
148
+ expect(version.features['workspace.workspaces']?.config).toEqual({ maxWorkspaces: 1 });
149
149
  expect(version.features['context.uploads']?.config).toEqual({ storageLimitMb: 1 });
150
150
  }
151
151
  finally {
@@ -153,6 +153,39 @@ describe('TaskforceCore plan version policy', () => {
153
153
  fs.rmSync(projectRoot, { recursive: true, force: true });
154
154
  }
155
155
  });
156
+ it('migrates legacy workspace feature keys to the canonical key', () => {
157
+ const projectRoot = makeProjectRoot();
158
+ const core = openCore(projectRoot);
159
+ try {
160
+ core.updatePlanVersionFeatures({
161
+ planVersionId: 'free-v1',
162
+ features: [
163
+ { featureKey: 'workspace.workspaces', access: 'limited', config: { maxWorkspaces: 1 } }
164
+ ]
165
+ });
166
+ const db = core.db;
167
+ db.prepare(`
168
+ UPDATE plan_feature_matrix
169
+ SET feature_key = 'workspace.switching'
170
+ WHERE tenant_id = ? AND plan_version_id = ? AND feature_key = 'workspace.workspaces'
171
+ `).run('default', 'free-v1');
172
+ core.planEntitlementService.normalizeLegacyPlanDataIfNeeded();
173
+ const version = core.getPlanVersionFeatures('free', 'free-v1');
174
+ expect(version.features['workspace.workspaces']).toBeTruthy();
175
+ expect(version.features['workspace.switching']).toBeUndefined();
176
+ const rows = db.prepare(`
177
+ SELECT feature_key
178
+ FROM plan_feature_matrix
179
+ WHERE tenant_id = ? AND plan_version_id = ?
180
+ `).all('default', 'free-v1');
181
+ expect(rows.some((row) => row.feature_key === 'workspace.switching')).toBe(false);
182
+ expect(rows.some((row) => row.feature_key === 'workspace.workspaces')).toBe(true);
183
+ }
184
+ finally {
185
+ core.close();
186
+ fs.rmSync(projectRoot, { recursive: true, force: true });
187
+ }
188
+ });
156
189
  it('starts new plans disabled before pricing is configured', () => {
157
190
  const projectRoot = makeProjectRoot();
158
191
  const core = openCore(projectRoot);
@@ -92,4 +92,56 @@ describe('TaskforceCore planning entity persistence', () => {
92
92
  expect(clearedTask?.workstreamId).toBeNull();
93
93
  core.close();
94
94
  });
95
+ it('builds lightweight planning bootstrap summaries without hydrating full task details', () => {
96
+ const core = createCore();
97
+ const initiative = core.createInitiative({ title: 'Launch initiative' });
98
+ const deliveryWorkstream = core.createWorkstream({
99
+ title: 'Delivery',
100
+ initiativeId: initiative.id,
101
+ });
102
+ const billingWorkstream = core.createWorkstream({
103
+ title: 'Billing',
104
+ initiativeId: initiative.id,
105
+ });
106
+ core.createTask({
107
+ title: 'Ship onboarding copy',
108
+ workstreamId: deliveryWorkstream.id,
109
+ status: 'task',
110
+ });
111
+ core.createTask({
112
+ title: 'Review launch checklist',
113
+ workstreamId: deliveryWorkstream.id,
114
+ status: 'done',
115
+ });
116
+ core.createTask({
117
+ title: 'Update billing note',
118
+ workstreamId: billingWorkstream.id,
119
+ status: 'cancelled',
120
+ });
121
+ core.createTask({
122
+ title: 'Unlinked task',
123
+ status: 'task',
124
+ });
125
+ const bootstrap = core.listPlanningBootstrap();
126
+ const deliverySummary = bootstrap.workstreamTaskSummaries.find((entry) => entry.workstreamId === deliveryWorkstream.id);
127
+ const billingSummary = bootstrap.workstreamTaskSummaries.find((entry) => entry.workstreamId === billingWorkstream.id);
128
+ expect(bootstrap.initiatives.map((entry) => entry.id)).toEqual([initiative.id]);
129
+ expect(bootstrap.workstreams.map((entry) => entry.id)).toEqual([deliveryWorkstream.id, billingWorkstream.id]);
130
+ expect(deliverySummary).toMatchObject({
131
+ workstreamId: deliveryWorkstream.id,
132
+ taskCount: 2,
133
+ completedTaskCount: 1,
134
+ });
135
+ expect(deliverySummary?.tasks).toEqual(expect.arrayContaining([
136
+ expect.objectContaining({ title: 'Ship onboarding copy', status: 'task' }),
137
+ expect.objectContaining({ title: 'Review launch checklist', status: 'done' }),
138
+ ]));
139
+ expect(billingSummary).toMatchObject({
140
+ workstreamId: billingWorkstream.id,
141
+ taskCount: 1,
142
+ completedTaskCount: 1,
143
+ });
144
+ expect(bootstrap.workstreamTaskSummaries.some((entry) => entry.tasks.some((task) => task.title === 'Unlinked task'))).toBe(false);
145
+ core.close();
146
+ });
95
147
  });
@@ -31,7 +31,7 @@ function openCore(projectRoot) {
31
31
  core.updatePlanVersionFeatures({
32
32
  planVersionId: 'free-v1',
33
33
  features: [
34
- { featureKey: 'workspace.switching', access: 'enabled' }
34
+ { featureKey: 'workspace.workspaces', access: 'enabled' }
35
35
  ]
36
36
  });
37
37
  insertFreePriceMapping(core, 'free-v1');
@@ -44,7 +44,7 @@ describe('TaskforceCore onboarding policy', () => {
44
44
  try {
45
45
  expect(core.getOnboardingPolicy()).toEqual({
46
46
  mode: 'require_plan_selection',
47
- planVersionId: null
47
+ planId: null
48
48
  });
49
49
  }
50
50
  finally {
@@ -68,13 +68,13 @@ describe('TaskforceCore onboarding policy', () => {
68
68
  core.updatePlanVersionFeatures({
69
69
  planVersionId: 'free-v1',
70
70
  features: [
71
- { featureKey: 'workspace.switching', access: 'enabled' }
71
+ { featureKey: 'workspace.workspaces', access: 'enabled' }
72
72
  ]
73
73
  });
74
74
  try {
75
75
  expect(core.getOnboardingPolicy()).toEqual({
76
76
  mode: 'require_plan_selection',
77
- planVersionId: null
77
+ planId: null
78
78
  });
79
79
  }
80
80
  finally {
@@ -82,16 +82,16 @@ describe('TaskforceCore onboarding policy', () => {
82
82
  fs.rmSync(projectRoot, { recursive: true, force: true });
83
83
  }
84
84
  });
85
- it('fails validation for auto_grant_free without a plan version id', () => {
85
+ it('fails validation for auto_grant_free without a plan id', () => {
86
86
  const projectRoot = makeProjectRoot();
87
87
  const core = openCore(projectRoot);
88
88
  try {
89
89
  expect(core.validateOnboardingPolicy({
90
90
  mode: 'auto_grant_free',
91
- planVersionId: null
91
+ planId: null
92
92
  })).toEqual(expect.objectContaining({
93
93
  valid: false,
94
- code: 'ONBOARDING_PLAN_VERSION_REQUIRED'
94
+ code: 'ONBOARDING_PLAN_REQUIRED'
95
95
  }));
96
96
  }
97
97
  finally {
@@ -99,16 +99,16 @@ describe('TaskforceCore onboarding policy', () => {
99
99
  fs.rmSync(projectRoot, { recursive: true, force: true });
100
100
  }
101
101
  });
102
- it('fails validation when the configured version does not exist', () => {
102
+ it('fails validation when the configured plan does not exist', () => {
103
103
  const projectRoot = makeProjectRoot();
104
104
  const core = openCore(projectRoot);
105
105
  try {
106
106
  expect(core.validateOnboardingPolicy({
107
107
  mode: 'auto_grant_free',
108
- planVersionId: 'missing-v9'
108
+ planId: 'missing'
109
109
  })).toEqual(expect.objectContaining({
110
110
  valid: false,
111
- code: 'ONBOARDING_PLAN_VERSION_NOT_FOUND'
111
+ code: 'ONBOARDING_PLAN_NOT_FOUND'
112
112
  }));
113
113
  }
114
114
  finally {
@@ -116,14 +116,14 @@ describe('TaskforceCore onboarding policy', () => {
116
116
  fs.rmSync(projectRoot, { recursive: true, force: true });
117
117
  }
118
118
  });
119
- it('fails validation when the configured version belongs to a disabled plan', () => {
119
+ it('fails validation when the configured plan is disabled', () => {
120
120
  const projectRoot = makeProjectRoot();
121
121
  const core = openCore(projectRoot);
122
122
  try {
123
123
  core.upsertPlanCatalog({ planId: 'free', displayName: 'Free', status: 'archived' });
124
124
  expect(core.validateOnboardingPolicy({
125
125
  mode: 'auto_grant_free',
126
- planVersionId: 'free-v1'
126
+ planId: 'free'
127
127
  })).toEqual(expect.objectContaining({
128
128
  valid: false,
129
129
  code: 'ONBOARDING_PLAN_NOT_ENABLED'
@@ -134,14 +134,14 @@ describe('TaskforceCore onboarding policy', () => {
134
134
  fs.rmSync(projectRoot, { recursive: true, force: true });
135
135
  }
136
136
  });
137
- it('fails validation when auto_grant_free points at a version without free pricing', () => {
137
+ it('fails validation when auto_grant_free points at a plan whose default version lacks free pricing', () => {
138
138
  const projectRoot = makeProjectRoot();
139
139
  const core = openCore(projectRoot);
140
140
  try {
141
141
  core.upsertPlanCatalog({ planId: 'team-5', displayName: 'Team 5', status: 'active' });
142
142
  expect(core.validateOnboardingPolicy({
143
143
  mode: 'auto_grant_free',
144
- planVersionId: 'team-5-v1'
144
+ planId: 'team-5'
145
145
  })).toEqual(expect.objectContaining({
146
146
  valid: false,
147
147
  code: 'ONBOARDING_PLAN_MUST_BE_FREE'
@@ -152,13 +152,13 @@ describe('TaskforceCore onboarding policy', () => {
152
152
  fs.rmSync(projectRoot, { recursive: true, force: true });
153
153
  }
154
154
  });
155
- it('passes validation for require_plan_selection without a plan version id', () => {
155
+ it('passes validation for require_plan_selection without a plan id', () => {
156
156
  const projectRoot = makeProjectRoot();
157
157
  const core = openCore(projectRoot);
158
158
  try {
159
159
  expect(core.validateOnboardingPolicy({
160
160
  mode: 'require_plan_selection',
161
- planVersionId: null
161
+ planId: null
162
162
  })).toEqual({ valid: true });
163
163
  }
164
164
  finally {
@@ -172,7 +172,7 @@ describe('TaskforceCore onboarding policy', () => {
172
172
  try {
173
173
  const created = core.registerPasswordUser({
174
174
  email: 'implicit-onboarding@example.com',
175
- password: 'password123'
175
+ password: 'ValidPassphrase123!'
176
176
  });
177
177
  expect(created.planSelectionRequired).toBe(true);
178
178
  expect(created.checkoutPending).toBe(false);
@@ -201,15 +201,15 @@ describe('TaskforceCore onboarding policy', () => {
201
201
  billing: {
202
202
  onboardingPolicy: {
203
203
  mode: 'require_plan_selection',
204
- planVersionId: null
204
+ planId: null
205
205
  }
206
206
  }
207
207
  });
208
208
  const db = core.getDatabaseAdapter();
209
209
  db.prepare(`
210
- INSERT INTO users (id, tenant_id, email, display_name, auth_provider, external_auth_id, is_disabled, created_at, updated_at)
211
- VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)
212
- `).run('pending-u1', tenantId, 'pending@example.com', 'Pending', 'token', 'pending-u1', now, now);
210
+ INSERT INTO users (id, tenant_id, email, display_name, is_disabled, created_at, updated_at)
211
+ VALUES (?, ?, ?, ?, 0, ?, ?)
212
+ `).run('pending-u1', tenantId, 'pending@example.com', 'Pending', now, now);
213
213
  db.prepare(`
214
214
  INSERT INTO user_billing (user_id, tenant_id, entitlement_state, created_at, updated_at)
215
215
  VALUES (?, ?, ?, ?, ?)
@@ -248,9 +248,9 @@ describe('TaskforceCore onboarding policy', () => {
248
248
  core.upsertPlanCatalog({ planId: 'basic', displayName: 'Basic', status: 'active' });
249
249
  const db = core.getDatabaseAdapter();
250
250
  db.prepare(`
251
- INSERT INTO users (id, tenant_id, email, display_name, auth_provider, external_auth_id, is_disabled, created_at, updated_at)
252
- VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)
253
- `).run('checkout-u1', tenantId, 'checkout@example.com', 'Checkout', 'token', 'checkout-u1', now, now);
251
+ INSERT INTO users (id, tenant_id, email, display_name, is_disabled, created_at, updated_at)
252
+ VALUES (?, ?, ?, ?, 0, ?, ?)
253
+ `).run('checkout-u1', tenantId, 'checkout@example.com', 'Checkout', now, now);
254
254
  core.transitionCanonicalCommercialState({
255
255
  accountId: 'checkout-u1',
256
256
  planVersionId: 'basic-v1',
@@ -286,9 +286,9 @@ describe('TaskforceCore onboarding policy', () => {
286
286
  core.upsertPlanCatalog({ planId: 'basic', displayName: 'Basic', status: 'active' });
287
287
  const db = core.getDatabaseAdapter();
288
288
  db.prepare(`
289
- INSERT INTO users (id, tenant_id, email, display_name, auth_provider, external_auth_id, is_disabled, created_at, updated_at)
290
- VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)
291
- `).run('expired-checkout-u1', tenantId, 'expired@example.com', 'Expired Checkout', 'token', 'expired-checkout-u1', now, now);
289
+ INSERT INTO users (id, tenant_id, email, display_name, is_disabled, created_at, updated_at)
290
+ VALUES (?, ?, ?, ?, 0, ?, ?)
291
+ `).run('expired-checkout-u1', tenantId, 'expired@example.com', 'Expired Checkout', now, now);
292
292
  core.transitionCanonicalCommercialState({
293
293
  accountId: 'expired-checkout-u1',
294
294
  planVersionId: 'basic-v1',
@@ -340,4 +340,176 @@ describe('TaskforceCore onboarding policy', () => {
340
340
  fs.rmSync(projectRoot, { recursive: true, force: true });
341
341
  }
342
342
  });
343
+ it('treats an expired trialing entitlement as requiring plan reselection', () => {
344
+ const projectRoot = makeProjectRoot();
345
+ const tenantId = path.basename(projectRoot);
346
+ const core = openCore(projectRoot);
347
+ try {
348
+ const now = new Date().toISOString();
349
+ const expiredAt = new Date(Date.now() - (60 * 1000)).toISOString();
350
+ core.upsertPlanCatalog({ planId: 'basic', displayName: 'Basic', status: 'active' });
351
+ const db = core.getDatabaseAdapter();
352
+ db.prepare(`
353
+ INSERT INTO users (id, tenant_id, email, display_name, is_disabled, created_at, updated_at)
354
+ VALUES (?, ?, ?, ?, 0, ?, ?)
355
+ `).run('expired-trial-u1', tenantId, 'expired-trial@example.com', 'Expired Trial', now, now);
356
+ core.transitionCanonicalCommercialState({
357
+ accountId: 'expired-trial-u1',
358
+ planVersionId: 'basic-v1',
359
+ state: 'trialing',
360
+ source: 'system',
361
+ updatedBy: 'trial-start',
362
+ billingMirrorPlanId: 'basic',
363
+ effectiveAt: now,
364
+ effectiveUntil: expiredAt,
365
+ createdAt: now,
366
+ updatedAt: now
367
+ });
368
+ expect(core.getAccountAccessStatus('expired-trial-u1')).toEqual(expect.objectContaining({
369
+ gate: 'plan_selection_required',
370
+ canAccessApp: false,
371
+ canAccessPlans: true,
372
+ planSelectionRequired: true,
373
+ message: 'Choose a plan to continue.'
374
+ }));
375
+ expect(core.getAccountEntitlement('expired-trial-u1')).toEqual(expect.objectContaining({
376
+ state: 'pending_plan_selection',
377
+ effectiveUntil: null
378
+ }));
379
+ const entitlementRow = db.prepare(`
380
+ SELECT entitlement_state, effective_until, updated_by
381
+ FROM account_entitlements
382
+ WHERE tenant_id = ? AND account_id = ?
383
+ LIMIT 1
384
+ `).get(tenantId, 'expired-trial-u1');
385
+ expect(entitlementRow).toEqual(expect.objectContaining({
386
+ entitlement_state: 'pending_plan_selection',
387
+ effective_until: null,
388
+ updated_by: 'trial-expired'
389
+ }));
390
+ const billingRow = db.prepare(`
391
+ SELECT entitlement_state, tier, effective_until
392
+ FROM user_billing
393
+ WHERE tenant_id = ? AND user_id = ?
394
+ LIMIT 1
395
+ `).get(tenantId, 'expired-trial-u1');
396
+ expect(billingRow).toEqual(expect.objectContaining({
397
+ entitlement_state: 'pending_plan_selection',
398
+ tier: '',
399
+ effective_until: null
400
+ }));
401
+ }
402
+ finally {
403
+ core.close();
404
+ fs.rmSync(projectRoot, { recursive: true, force: true });
405
+ }
406
+ });
407
+ it('auto-heals a pending-plan-selection entitlement from an active Stripe billing mirror', () => {
408
+ const projectRoot = makeProjectRoot();
409
+ const tenantId = path.basename(projectRoot);
410
+ const core = openCore(projectRoot);
411
+ try {
412
+ const now = new Date().toISOString();
413
+ const nextRenewal = new Date(Date.now() + (24 * 60 * 60 * 1000)).toISOString();
414
+ core.upsertPlanCatalog({ planId: 'team-5', displayName: 'Team 5', status: 'active' });
415
+ const db = core.getDatabaseAdapter();
416
+ db.prepare(`
417
+ INSERT INTO users (id, tenant_id, email, display_name, is_disabled, created_at, updated_at)
418
+ VALUES (?, ?, ?, ?, 0, ?, ?)
419
+ `).run('mirror-heal-u1', tenantId, 'mirror-heal@example.com', 'Mirror Heal', now, now);
420
+ core.transitionCanonicalCommercialState({
421
+ accountId: 'mirror-heal-u1',
422
+ planVersionId: 'free-v1',
423
+ state: 'pending_plan_selection',
424
+ source: 'backfill',
425
+ updatedBy: 'backfill',
426
+ billingMirrorPlanId: null,
427
+ effectiveAt: now,
428
+ createdAt: now,
429
+ updatedAt: now
430
+ });
431
+ db.prepare(`
432
+ UPDATE user_billing
433
+ SET stripe_customer_id = ?, stripe_subscription_id = ?, entitlement_state = ?, tier = ?, effective_until = ?, updated_at = ?
434
+ WHERE tenant_id = ? AND user_id = ?
435
+ `).run('cus_heal', 'sub_heal', 'active', 'team-5', nextRenewal, now, tenantId, 'mirror-heal-u1');
436
+ expect(core.getAccountAccessStatus('mirror-heal-u1')).toEqual(expect.objectContaining({
437
+ gate: 'ok',
438
+ canAccessApp: true,
439
+ planSelectionRequired: false
440
+ }));
441
+ expect(core.getAccountEntitlement('mirror-heal-u1')).toEqual(expect.objectContaining({
442
+ state: 'active',
443
+ planId: 'team-5',
444
+ planVersionId: 'team-5-v1',
445
+ effectiveUntil: nextRenewal
446
+ }));
447
+ const entitlementRow = db.prepare(`
448
+ SELECT entitlement_state, plan_version_id, effective_until, updated_by
449
+ FROM account_entitlements
450
+ WHERE tenant_id = ? AND account_id = ?
451
+ LIMIT 1
452
+ `).get(tenantId, 'mirror-heal-u1');
453
+ expect(entitlementRow).toEqual(expect.objectContaining({
454
+ entitlement_state: 'active',
455
+ plan_version_id: 'team-5-v1',
456
+ effective_until: nextRenewal,
457
+ updated_by: 'billing-mirror-auto-heal'
458
+ }));
459
+ }
460
+ finally {
461
+ core.close();
462
+ fs.rmSync(projectRoot, { recursive: true, force: true });
463
+ }
464
+ });
465
+ it('auto-heals a pending-plan-selection entitlement from Stripe status and price mapping when mirror state is stale', () => {
466
+ const projectRoot = makeProjectRoot();
467
+ const tenantId = path.basename(projectRoot);
468
+ const core = openCore(projectRoot);
469
+ try {
470
+ const now = new Date().toISOString();
471
+ const nextRenewal = new Date(Date.now() + (24 * 60 * 60 * 1000)).toISOString();
472
+ core.upsertPlanCatalog({ planId: 'team-5', displayName: 'Team 5', status: 'active' });
473
+ const db = core.getDatabaseAdapter();
474
+ db.prepare(`
475
+ INSERT INTO users (id, tenant_id, email, display_name, is_disabled, created_at, updated_at)
476
+ VALUES (?, ?, ?, ?, 0, ?, ?)
477
+ `).run('mirror-price-heal-u1', tenantId, 'mirror-price-heal@example.com', 'Mirror Price Heal', now, now);
478
+ core.transitionCanonicalCommercialState({
479
+ accountId: 'mirror-price-heal-u1',
480
+ planVersionId: 'free-v1',
481
+ state: 'pending_plan_selection',
482
+ source: 'backfill',
483
+ updatedBy: 'checkout-expired',
484
+ billingMirrorPlanId: null,
485
+ effectiveAt: now,
486
+ createdAt: now,
487
+ updatedAt: now
488
+ });
489
+ db.prepare(`
490
+ UPDATE user_billing
491
+ SET stripe_customer_id = ?, stripe_subscription_id = ?, stripe_price_id = ?, stripe_status = ?, entitlement_state = ?, tier = ?, effective_until = ?, updated_at = ?
492
+ WHERE tenant_id = ? AND user_id = ?
493
+ `).run('cus_heal_2', 'sub_heal_2', 'price_team_5_month', 'active', 'pending_plan_selection', 'team-5', nextRenewal, now, tenantId, 'mirror-price-heal-u1');
494
+ db.prepare(`
495
+ INSERT INTO billing_price_mappings (tenant_id, plan_version_id, billing_interval, stripe_price_id, active, created_at, updated_at)
496
+ VALUES (?, ?, ?, ?, 1, ?, ?)
497
+ `).run(tenantId, 'team-5-v1', 'month', 'price_team_5_month', now, now);
498
+ expect(core.getAccountAccessStatus('mirror-price-heal-u1')).toEqual(expect.objectContaining({
499
+ gate: 'ok',
500
+ canAccessApp: true,
501
+ planSelectionRequired: false
502
+ }));
503
+ expect(core.getAccountEntitlement('mirror-price-heal-u1')).toEqual(expect.objectContaining({
504
+ state: 'active',
505
+ planId: 'team-5',
506
+ planVersionId: 'team-5-v1',
507
+ effectiveUntil: nextRenewal
508
+ }));
509
+ }
510
+ finally {
511
+ core.close();
512
+ fs.rmSync(projectRoot, { recursive: true, force: true });
513
+ }
514
+ });
343
515
  });
@@ -1,4 +1,5 @@
1
1
  import { DEFAULT_TYPE_VALUE } from '../shared/taxonomyDefaults.js';
2
+ import { buildTaskSyncChangeKey } from '../sync/taskSyncChangeKey.js';
2
3
  export function listTasksForSyncService(deps, workspaceId, cursor, limit) {
3
4
  const normalizedWorkspaceId = deps.normalizeWorkspaceId(workspaceId);
4
5
  const safeLimit = Math.max(1, Math.min(10000, Math.floor(limit)));
@@ -67,72 +68,8 @@ export function listWorkspaceSyncDeletesSinceService(deps, workspaceId, cursor,
67
68
  }))
68
69
  .filter((row) => row.taskId.length > 0 && Number.isFinite(Date.parse(row.deletedAt)));
69
70
  }
70
- function normalizeConflictTaxonomies(value) {
71
- if (!value || typeof value !== 'object')
72
- return {};
73
- const source = value;
74
- return Object.keys(source).sort().reduce((acc, key) => {
75
- const raw = source[key];
76
- if (Array.isArray(raw)) {
77
- acc[key] = raw.map((entry) => String(entry ?? '')).sort();
78
- }
79
- else if (raw === null || raw === undefined) {
80
- acc[key] = '';
81
- }
82
- else {
83
- acc[key] = String(raw);
84
- }
85
- return acc;
86
- }, {});
87
- }
88
- function normalizeConflictChecklistItems(value) {
89
- if (!Array.isArray(value))
90
- return [];
91
- return value
92
- .map((item) => {
93
- const entry = item && typeof item === 'object' ? item : {};
94
- return {
95
- id: String(entry.id || '').trim(),
96
- title: String(entry.title || '').trim(),
97
- isCompleted: Boolean(entry.isCompleted),
98
- order: entry.order === undefined || entry.order === null ? '' : String(entry.order),
99
- };
100
- })
101
- .sort((left, right) => {
102
- const leftOrder = Number.parseInt(left.order, 10);
103
- const rightOrder = Number.parseInt(right.order, 10);
104
- if (Number.isFinite(leftOrder) && Number.isFinite(rightOrder) && leftOrder !== rightOrder) {
105
- return leftOrder - rightOrder;
106
- }
107
- if (left.title !== right.title)
108
- return left.title.localeCompare(right.title);
109
- return left.id.localeCompare(right.id);
110
- });
111
- }
112
71
  function buildSyncConflictKey(task, archivedValue) {
113
- const source = task || {};
114
- return JSON.stringify({
115
- id: String(source.id || ''),
116
- title: String(source.title || ''),
117
- description: source.description === null || source.description === undefined ? '' : String(source.description),
118
- category: String(source.category || ''),
119
- type: String(source.type || ''),
120
- priority: source.priority === undefined || source.priority === null ? '' : String(source.priority),
121
- complexity: source.complexity === undefined || source.complexity === null ? '' : String(source.complexity),
122
- status: String(source.status || ''),
123
- approach: source.approach === undefined || source.approach === null ? '' : String(source.approach),
124
- canceledReason: source.canceledReason === undefined || source.canceledReason === null ? '' : String(source.canceledReason),
125
- scheduledDate: source.scheduledDate === undefined || source.scheduledDate === null ? '' : String(source.scheduledDate),
126
- dueDate: source.dueDate === undefined || source.dueDate === null ? '' : String(source.dueDate),
127
- scheduledWeekKey: source.scheduledWeekKey === undefined || source.scheduledWeekKey === null ? '' : String(source.scheduledWeekKey),
128
- orderInDay: source.orderInDay === undefined || source.orderInDay === null ? '' : String(source.orderInDay),
129
- workstreamId: source.workstreamId === undefined || source.workstreamId === null ? '' : String(source.workstreamId),
130
- referenceNumber: source.referenceNumber === undefined || source.referenceNumber === null ? '' : String(source.referenceNumber),
131
- assignee: source.assignee === undefined || source.assignee === null ? '' : String(source.assignee),
132
- taxonomies: normalizeConflictTaxonomies(source.taxonomies),
133
- checklistItems: normalizeConflictChecklistItems(source.checklistItems),
134
- isArchived: archivedValue ? '1' : '0'
135
- });
72
+ return buildTaskSyncChangeKey(task, archivedValue);
136
73
  }
137
74
  export function applyWorkspaceSyncSnapshotService(deps, input, workspaceId) {
138
75
  const normalizedWorkspaceId = deps.normalizeWorkspaceId(workspaceId);