@taskforcehq/taskforce 0.3.327 → 0.3.329

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 (360) hide show
  1. package/dist/Taskforce.module.css +257 -87
  2. package/dist/TaskforceCore.js +237 -230
  3. package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.js +1 -1
  4. package/dist/cli.js +34 -0
  5. package/dist/cliCloudTargetGuard.d.ts +5 -0
  6. package/dist/cliCloudTargetGuard.js +11 -0
  7. package/dist/components/activity/EntityActivityTimeline.js +15 -5
  8. package/dist/components/context/AttachmentPreviewHost.js +1 -1
  9. package/dist/components/context/ContextAttachmentManager.d.ts +3 -0
  10. package/dist/components/context/ContextAttachmentManager.js +116 -20
  11. package/dist/components/features/AnnotatedAttachmentWorkspace.js +24 -8
  12. package/dist/components/features/TaskSettings.d.ts +12 -1
  13. package/dist/components/features/TaskSettings.js +147 -40
  14. package/dist/components/features/agentCapabilities/AgentCapabilitiesPrototype.d.ts +1 -1
  15. package/dist/components/features/planning/PlanningEntitySummary.d.ts +3 -1
  16. package/dist/components/features/planning/PlanningEntitySummary.js +3 -2
  17. package/dist/components/features/planning/PlanningLinkPicker.d.ts +3 -1
  18. package/dist/components/features/planning/PlanningLinkPicker.js +5 -3
  19. package/dist/components/features/planning/PlanningModule.js +28 -14
  20. package/dist/components/features/planning/PlanningTaskOrderList.d.ts +1 -1
  21. package/dist/components/features/planning/PlanningTaskOrderList.js +17 -6
  22. package/dist/components/features/planning/PlanningVisualIdentity.d.ts +14 -0
  23. package/dist/components/features/planning/PlanningVisualIdentity.js +106 -0
  24. package/dist/components/features/planning/planningTaskArrangement.d.ts +1 -0
  25. package/dist/components/features/planning/planningTaskArrangement.js +5 -5
  26. package/dist/components/features/planning/planningWorkspaceModel.d.ts +11 -0
  27. package/dist/components/features/taskforceAgentEditorModel.js +3 -1
  28. package/dist/components/task/TaskActionHeader.d.ts +2 -1
  29. package/dist/components/task/TaskActionHeader.js +2 -2
  30. package/dist/components/task/TaskCard.d.ts +5 -0
  31. package/dist/components/task/TaskCard.js +101 -13
  32. package/dist/components/task/TaskContextUpload.d.ts +3 -0
  33. package/dist/components/task/TaskContextUpload.js +2 -2
  34. package/dist/components/task/TaskForm.d.ts +4 -0
  35. package/dist/components/task/TaskForm.js +32 -7
  36. package/dist/components/task/TaskImageReviews.js +28 -19
  37. package/dist/components/task/TaskKanban.d.ts +6 -1
  38. package/dist/components/task/TaskKanban.js +152 -6
  39. package/dist/components/task/TaskList.d.ts +5 -0
  40. package/dist/components/task/TaskList.js +3 -3
  41. package/dist/components/task/TaskStatusActions.js +11 -2
  42. package/dist/components/task/TaskWorkstreamPicker.js +2 -1
  43. package/dist/components/ui/DocumentTypeIcon.d.ts +7 -0
  44. package/dist/components/ui/DocumentTypeIcon.js +8 -0
  45. package/dist/components/ui/PasswordInput.d.ts +6 -0
  46. package/dist/components/ui/PasswordInput.js +11 -0
  47. package/dist/components/ui/TaxonomyDropdown.d.ts +9 -1
  48. package/dist/components/ui/TaxonomyDropdown.js +30 -3
  49. package/dist/components/ui/TopNoticeLayer.js +1 -1
  50. package/dist/components/views/AppShellHeader.js +32 -11
  51. package/dist/components/views/PlanComparisonPage.js +12 -0
  52. package/dist/components/views/StandaloneLayout.js +68 -39
  53. package/dist/components/views/WidgetView.js +10 -7
  54. package/dist/components/views/panels/PlanningDrawer.d.ts +7 -1
  55. package/dist/components/views/panels/PlanningDrawer.js +51 -54
  56. package/dist/components/views/standalone/modals/AccountSettingsModal.js +26 -17
  57. package/dist/config/envSchema.js +13 -0
  58. package/dist/config/localServiceCloudTarget.d.ts +10 -0
  59. package/dist/config/localServiceCloudTarget.js +76 -0
  60. package/dist/config/localServiceRuntimeContract.d.ts +13 -0
  61. package/dist/config/localServiceRuntimeContract.js +135 -0
  62. package/dist/core/AiProfileService.d.ts +1 -0
  63. package/dist/core/AiProfileService.js +48 -9
  64. package/dist/core/AttachmentLinkService.js +7 -4
  65. package/dist/core/DeletedTaskLifecycleService.d.ts +5 -0
  66. package/dist/core/DeletedTaskLifecycleService.js +55 -3
  67. package/dist/core/McpTokenService.d.ts +4 -0
  68. package/dist/core/McpTokenService.js +52 -0
  69. package/dist/core/TaskAttachmentLinksDurableMutationService.d.ts +8 -2
  70. package/dist/core/TaskAttachmentLinksDurableMutationService.js +51 -18
  71. package/dist/core/Taskforce.d.ts +124 -2
  72. package/dist/core/Taskforce.js +1175 -87
  73. package/dist/core/types.d.ts +8 -1
  74. package/dist/documentReviews/DocumentReviewCommentStore.d.ts +11 -0
  75. package/dist/documentReviews/DocumentReviewCommentStore.js +35 -2
  76. package/dist/documentReviews/service.d.ts +4 -0
  77. package/dist/documentReviews/service.js +141 -46
  78. package/dist/hooks/sync/useDurableLocalOutboxDrain.d.ts +1 -1
  79. package/dist/hooks/sync/useDurableTaskCreateMutation.js +1 -1
  80. package/dist/hooks/sync/useDurableTaskMetadataMutation.d.ts +1 -0
  81. package/dist/hooks/sync/useDurableTaskMetadataMutation.js +1 -0
  82. package/dist/hooks/sync/useDurableTaskStatusMutation.d.ts +1 -0
  83. package/dist/hooks/tasks/taskHttpMutation.js +1 -0
  84. package/dist/hooks/tasks/useCloudTaskSearch.d.ts +12 -0
  85. package/dist/hooks/tasks/useCloudTaskSearch.js +71 -0
  86. package/dist/hooks/tasks/useTaskDescriptionImageDraftRecovery.d.ts +16 -0
  87. package/dist/hooks/tasks/useTaskDescriptionImageDraftRecovery.js +197 -0
  88. package/dist/hooks/tasks/useTaskEditorNavigation.d.ts +16 -3
  89. package/dist/hooks/tasks/useTaskEditorNavigation.js +72 -13
  90. package/dist/hooks/tasks/useTaskFormActions.d.ts +9 -0
  91. package/dist/hooks/tasks/useTaskFormActions.js +96 -17
  92. package/dist/hooks/ui/useSettingsModel.d.ts +24 -2
  93. package/dist/hooks/ui/useSettingsModel.js +13 -2
  94. package/dist/hooks/ui/useSettingsPersistence.d.ts +12 -1
  95. package/dist/hooks/ui/useSettingsPersistence.js +74 -47
  96. package/dist/hooks/useFilterSort.d.ts +7 -3
  97. package/dist/hooks/useFilterSort.js +15 -5
  98. package/dist/hooks/useTaskData.d.ts +10 -1
  99. package/dist/hooks/useTaskData.js +216 -7
  100. package/dist/hooks/useTaskMutations.d.ts +10 -2
  101. package/dist/hooks/useTaskMutations.js +223 -43
  102. package/dist/hooks/useTaskforce.d.ts +94 -57
  103. package/dist/hooks/useTaskforce.js +428 -16
  104. package/dist/hooks/useUiNotice.d.ts +4 -0
  105. package/dist/hooks/useUiNotice.js +4 -0
  106. package/dist/mcp/adminWorkspaceRegistrar.js +3 -3
  107. package/dist/mcp/canonicalAssetHelpers.d.ts +13 -0
  108. package/dist/mcp/canonicalAssetHelpers.js +39 -5
  109. package/dist/mcp/collaborationRegistrar.js +15 -13
  110. package/dist/mcp/documentAssetRegistrar.js +60 -44
  111. package/dist/mcp/documentHelpers.d.ts +1 -0
  112. package/dist/mcp/documentHelpers.js +7 -3
  113. package/dist/mcp/localServiceProxy.js +4 -1
  114. package/dist/mcp/runtime.d.ts +16 -0
  115. package/dist/mcp/runtime.js +735 -210
  116. package/dist/mcp/taskAttachmentCommandAdapter.d.ts +2 -0
  117. package/dist/mcp/taskAttachmentCommandAdapter.js +14 -8
  118. package/dist/mcp/taskPlanningRegistrar.js +78 -67
  119. package/dist/mcp/toolCallCompatibility.js +38 -1
  120. package/dist/mcp/toolCatalog.d.ts +1 -0
  121. package/dist/mcp/toolRegistry.js +2 -2
  122. package/dist/migrations/taskSchemaMigrations.js +47 -0
  123. package/dist/runtime/appGateDefinitions.d.ts +30 -6
  124. package/dist/runtime/appGateDefinitions.js +6 -6
  125. package/dist/runtime/appGates.d.ts +12 -13
  126. package/dist/runtime/appGates.js +29 -13
  127. package/dist/runtime/nodeAppGates.d.ts +8 -0
  128. package/dist/runtime/nodeAppGates.js +27 -0
  129. package/dist/server/appAccess.d.ts +12 -0
  130. package/dist/server/appAccess.js +21 -0
  131. package/dist/server/authorizedWorkspaceAccess.d.ts +12 -0
  132. package/dist/server/authorizedWorkspaceAccess.js +40 -0
  133. package/dist/server/cloudCollectionRead.d.ts +25 -0
  134. package/dist/server/cloudCollectionRead.js +120 -0
  135. package/dist/server/cloudCollectionReadPool.d.ts +52 -0
  136. package/dist/server/cloudCollectionReadPool.js +140 -0
  137. package/dist/server/cloudCollectionReadWorker.d.ts +1 -0
  138. package/dist/server/cloudCollectionReadWorker.js +36 -0
  139. package/dist/server/index.d.ts +6 -11
  140. package/dist/server/index.js +67 -42
  141. package/dist/server/localServiceLease.d.ts +6 -0
  142. package/dist/server/localServiceLease.js +17 -1
  143. package/dist/server/localWorkspaceSyncServerRuntime.js +10 -3
  144. package/dist/server/rateLimit.d.ts +1 -0
  145. package/dist/server/rateLimit.js +4 -0
  146. package/dist/server/realtimeSyncWs.js +36 -12
  147. package/dist/server/routes/admin.js +1 -0
  148. package/dist/server/routes/agentRoles.js +3 -2
  149. package/dist/server/routes/agentSkills.js +3 -2
  150. package/dist/server/routes/agents.js +118 -16
  151. package/dist/server/routes/annotatedAttachments.js +3 -2
  152. package/dist/server/routes/auth.js +3 -1
  153. package/dist/server/routes/documents.d.ts +5 -2
  154. package/dist/server/routes/documents.js +160 -13
  155. package/dist/server/routes/durableTaskHttpRouters.d.ts +11 -2
  156. package/dist/server/routes/durableTaskHttpRouters.js +2 -2
  157. package/dist/server/routes/localService.js +4 -0
  158. package/dist/server/routes/primitives.d.ts +4 -0
  159. package/dist/server/routes/shared.d.ts +24 -0
  160. package/dist/server/routes/shared.js +114 -18
  161. package/dist/server/routes/sync.js +22 -0
  162. package/dist/server/routes/syncAuxRoutes.js +5 -2
  163. package/dist/server/routes/syncPullApplyRoutes.js +23 -4
  164. package/dist/server/routes/syncV3FeedRoutes.js +22 -18
  165. package/dist/server/routes/syncV3LocalCaptureRoutes.js +48 -4
  166. package/dist/server/routes/syncV3LocalFeedApplyRoutes.d.ts +1 -0
  167. package/dist/server/routes/syncV3LocalFeedApplyRoutes.js +26 -0
  168. package/dist/server/routes/syncV3OperationRoutes.js +27 -2
  169. package/dist/server/routes/tasks.js +299 -97
  170. package/dist/server/routes/workspaces.js +43 -1
  171. package/dist/server/routes.js +35 -7
  172. package/dist/server/workspaceDeletionAdmission.d.ts +23 -0
  173. package/dist/server/workspaceDeletionAdmission.js +50 -0
  174. package/dist/service/localServiceCli.js +52 -25
  175. package/dist/service/localServiceClientLifecycle.js +93 -6
  176. package/dist/services/codexAppServerModelGateway.d.ts +112 -0
  177. package/dist/services/codexAppServerModelGateway.js +438 -0
  178. package/dist/services/taskforceAgentMcpBridge.d.ts +7 -1
  179. package/dist/services/taskforceAgentMcpBridge.js +23 -3
  180. package/dist/shared/planningIdentity.d.ts +20 -0
  181. package/dist/shared/planningIdentity.js +79 -0
  182. package/dist/shared/taskforceAgentDefinition.js +7 -1
  183. package/dist/shared/taskforceAgentModels.d.ts +1 -0
  184. package/dist/shared/taskforceAgentModels.js +30 -1
  185. package/dist/storage/postgresAdapter.d.ts +20 -3
  186. package/dist/storage/postgresAdapter.js +110 -14
  187. package/dist/storage/postgresRequestDiagnostics.d.ts +8 -0
  188. package/dist/storage/postgresRequestDiagnostics.js +24 -0
  189. package/dist/storage/postgresWorker.js +13 -4
  190. package/dist/storage/postgresWorkerProtocol.d.ts +29 -0
  191. package/dist/storage/postgresWorkerProtocol.js +30 -0
  192. package/dist/storage/workspaceAssetStore.d.ts +32 -0
  193. package/dist/storage/workspaceAssetStore.js +70 -0
  194. package/dist/sync/cloudSyncApi.d.ts +4 -1
  195. package/dist/sync/cloudSyncApi.js +7 -2
  196. package/dist/sync/contentSyncState.d.ts +1 -1
  197. package/dist/sync/coordinator/localSyncExecutionPermitStore.d.ts +17 -0
  198. package/dist/sync/coordinator/localSyncExecutionPermitStore.js +77 -23
  199. package/dist/sync/coordinator/localWorkspaceSyncRunner.js +6 -2
  200. package/dist/sync/coordinator/workspaceSyncTransportGateway.js +4 -2
  201. package/dist/sync/engine/workspaceSyncEngineBrowserGatewayRemoteServices.js +20 -2
  202. package/dist/sync/engine/workspaceSyncEngineLocalServices.d.ts +4 -0
  203. package/dist/sync/engine/workspaceSyncEngineLocalServices.js +11 -0
  204. package/dist/sync/engine/workspaceSyncEngineRemoteServices.d.ts +16 -0
  205. package/dist/sync/engine/workspaceSyncEngineRemoteServices.js +2 -1
  206. package/dist/sync/engine/workspaceSyncEngineServerRemoteServices.d.ts +1 -1
  207. package/dist/sync/engine/workspaceSyncEngineServerRemoteServices.js +46 -4
  208. package/dist/sync/engine/workspaceSyncEngineServerRunnerOperations.d.ts +3 -1
  209. package/dist/sync/engine/workspaceSyncEngineServerRunnerOperations.js +51 -11
  210. package/dist/sync/engine/workspaceSyncEngineTransfers.d.ts +1 -1
  211. package/dist/sync/engine/workspaceSyncEngineTransfers.js +8 -3
  212. package/dist/sync/engine/workspaceSyncV2ChangeDispatcher.js +2 -0
  213. package/dist/sync/syncApplyHandlers.d.ts +3 -1
  214. package/dist/sync/syncApplyHandlers.js +29 -0
  215. package/dist/sync/syncService.d.ts +2 -0
  216. package/dist/sync/syncService.js +13 -1
  217. package/dist/sync/taskSyncPayload.d.ts +1 -0
  218. package/dist/sync/taskSyncPayload.js +6 -0
  219. package/dist/sync/v3/combinedFeedCapability.js +4 -0
  220. package/dist/sync/v3/durableTaskAttachmentLinksMutationRouter.d.ts +3 -0
  221. package/dist/sync/v3/durableTaskAttachmentLinksMutationRouter.js +31 -3
  222. package/dist/sync/v3/durableTaskChecklistMutationRouter.d.ts +2 -0
  223. package/dist/sync/v3/durableTaskChecklistMutationRouter.js +4 -0
  224. package/dist/sync/v3/durableTaskHttpBulkMutationRouter.d.ts +1 -0
  225. package/dist/sync/v3/durableTaskHttpBulkMutationRouter.js +47 -12
  226. package/dist/sync/v3/durableTaskHttpRootMutationRouter.d.ts +5 -3
  227. package/dist/sync/v3/durableTaskHttpRootMutationRouter.js +27 -2
  228. package/dist/sync/v3/localInitiativeOutboxService.js +4 -2
  229. package/dist/sync/v3/localOutboxDispatcher.js +5 -1
  230. package/dist/sync/v3/localTaskAttachmentLinksOutboxHandler.js +12 -2
  231. package/dist/sync/v3/localTaskAttachmentLinksOutboxService.d.ts +3 -0
  232. package/dist/sync/v3/localTaskAttachmentLinksOutboxService.js +62 -10
  233. package/dist/sync/v3/localTaskCreateOutboxService.js +15 -6
  234. package/dist/sync/v3/localTaskMetadataClient.d.ts +1 -0
  235. package/dist/sync/v3/localTaskMetadataClient.js +1 -0
  236. package/dist/sync/v3/localTaskMetadataOutboxHandler.js +5 -1
  237. package/dist/sync/v3/localTaskMetadataOutboxService.d.ts +2 -0
  238. package/dist/sync/v3/localTaskMetadataOutboxService.js +7 -0
  239. package/dist/sync/v3/localTaskRestoreOutboxService.js +14 -6
  240. package/dist/sync/v3/localTaskStatusClient.d.ts +1 -0
  241. package/dist/sync/v3/localTaskStatusClient.js +6 -1
  242. package/dist/sync/v3/localTaskStatusOutboxService.d.ts +2 -0
  243. package/dist/sync/v3/localTaskStatusOutboxService.js +54 -13
  244. package/dist/sync/v3/localWorkstreamOutboxService.js +4 -2
  245. package/dist/sync/v3/syncDurabilityStore.d.ts +15 -0
  246. package/dist/sync/v3/syncDurabilityStore.js +201 -43
  247. package/dist/sync/v3/taskAttachmentLinksMutationService.d.ts +7 -4
  248. package/dist/sync/v3/taskAttachmentLinksMutationService.js +37 -12
  249. package/dist/sync/v3/taskChecklistMutationService.d.ts +4 -2
  250. package/dist/sync/v3/taskChecklistMutationService.js +4 -0
  251. package/dist/sync/v3/taskCreateMutationService.d.ts +2 -2
  252. package/dist/sync/v3/taskCreateMutationService.js +7 -4
  253. package/dist/sync/v3/taskMetadataCoexistence.js +4 -0
  254. package/dist/sync/v3/taskMetadataMutationService.d.ts +3 -0
  255. package/dist/sync/v3/taskMetadataMutationService.js +4 -0
  256. package/dist/sync/v3/taskRestoreMutationService.d.ts +2 -2
  257. package/dist/sync/v3/taskRestoreMutationService.js +7 -4
  258. package/dist/sync/v3/taskStatusMutationService.d.ts +2 -0
  259. package/dist/sync/v3/taskStatusMutationService.js +4 -0
  260. package/dist/sync/v3/workflowAssignmentSync.d.ts +1 -0
  261. package/dist/sync/v3/workflowAssignmentSync.js +3 -0
  262. package/dist/sync/v3/workspaceSyncV2ProjectionExclusions.d.ts +1 -1
  263. package/dist/sync/v3/workspaceSyncV2ProjectionExclusions.js +14 -12
  264. package/dist/sync/v3/workspaceSyncV3AttachmentApplyObligationStore.d.ts +11 -0
  265. package/dist/sync/v3/workspaceSyncV3AttachmentApplyObligationStore.js +27 -10
  266. package/dist/sync/v3/workspaceSyncV3AttachmentAssetHydration.d.ts +2 -1
  267. package/dist/sync/v3/workspaceSyncV3AttachmentAssetHydration.js +52 -10
  268. package/dist/sync/v3/workspaceSyncV3CloudOperationHandler.js +10 -1
  269. package/dist/sync/v3/workspaceSyncV3CombinedFeedOrchestrator.js +15 -4
  270. package/dist/sync/v3/workspaceSyncV3CombinedLocalApply.js +66 -13
  271. package/dist/sync/v3/workspaceSyncV3CombinedRepairBaseline.d.ts +11 -0
  272. package/dist/sync/v3/workspaceSyncV3CombinedRepairBaseline.js +57 -22
  273. package/dist/sync/v3/workspaceSyncV3CombinedRepairMaterialization.d.ts +10 -4
  274. package/dist/sync/v3/workspaceSyncV3CombinedRepairMaterialization.js +124 -51
  275. package/dist/sync/v3/workspaceSyncV3CombinedRepairSnapshot.d.ts +10 -0
  276. package/dist/sync/v3/workspaceSyncV3CombinedRepairSnapshot.js +120 -28
  277. package/dist/sync/v3/workspaceSyncV3CombinedWireContract.d.ts +3 -1
  278. package/dist/sync/v3/workspaceSyncV3CombinedWireContract.js +18 -0
  279. package/dist/sync/v3/workspaceSyncV3CoverageRegistry.js +6 -0
  280. package/dist/sync/v3/workspaceSyncV3FeedProfile.d.ts +1 -1
  281. package/dist/sync/v3/workspaceSyncV3LegacyBoundaryAttestation.d.ts +1 -0
  282. package/dist/sync/v3/workspaceSyncV3LegacyBoundaryAttestation.js +23 -10
  283. package/dist/sync/v3/workspaceSyncV3NestedTaskApply.d.ts +3 -0
  284. package/dist/sync/v3/workspaceSyncV3NestedTaskApply.js +64 -13
  285. package/dist/sync/v3/workspaceSyncV3NestedTaskCodecs.d.ts +9 -0
  286. package/dist/sync/v3/workspaceSyncV3NestedTaskCodecs.js +26 -0
  287. package/dist/sync/v3/workspaceSyncV3NestedTaskProjection.d.ts +15 -5
  288. package/dist/sync/v3/workspaceSyncV3NestedTaskProjection.js +50 -14
  289. package/dist/sync/v3/workspaceSyncV3NestedTaskRepairSnapshot.d.ts +2 -1
  290. package/dist/sync/v3/workspaceSyncV3NestedTaskRepairSnapshot.js +11 -3
  291. package/dist/sync/v3/workspaceSyncV3OperationProtocol.d.ts +3 -0
  292. package/dist/sync/v3/workspaceSyncV3OperatorDiagnostics.d.ts +2 -0
  293. package/dist/sync/v3/workspaceSyncV3OperatorDiagnostics.js +19 -7
  294. package/dist/sync/v3/workspaceSyncV3RepairSqlBatching.d.ts +2 -0
  295. package/dist/sync/v3/workspaceSyncV3RepairSqlBatching.js +10 -0
  296. package/dist/sync/v3/workspaceSyncV3TaskCommentAudit.js +9 -2
  297. package/dist/sync/v3/workspaceSyncV3TaskCoverCoverage.d.ts +7 -0
  298. package/dist/sync/v3/workspaceSyncV3TaskCoverCoverage.js +37 -0
  299. package/dist/sync/v3/workspaceSyncV3TaskCoverWireContract.d.ts +6 -0
  300. package/dist/sync/v3/workspaceSyncV3TaskCoverWireContract.js +9 -0
  301. package/dist/sync/v3/workspaceSyncV3TaskRootProjection.d.ts +1 -1
  302. package/dist/sync/v3/workspaceSyncV3TaskRootProjection.js +1 -1
  303. package/dist/sync/workspacePullFeed.d.ts +6 -1
  304. package/dist/sync/workspacePullFeed.js +174 -162
  305. package/dist/sync/workspaceRepair.d.ts +3 -0
  306. package/dist/sync/workspaceRepair.js +67 -6
  307. package/dist/sync/workspaceSyncModel.d.ts +11 -0
  308. package/dist/sync/workspaceSyncModel.js +27 -2
  309. package/dist/types.d.ts +55 -1
  310. package/dist/types.js +6 -0
  311. package/dist/ui/assets/{AiIdentityRosterCard-DB1BLO_j.js → AiIdentityRosterCard-BZM4aQnM.js} +1 -1
  312. package/dist/ui/assets/AiProfilesModule-BbAEWtHH.js +1 -0
  313. package/dist/ui/assets/AnnotatedAttachmentWorkspace-B68Wg-RV.js +3 -0
  314. package/dist/ui/assets/AnnotatedAttachmentWorkspace-TzYu7pkG.css +1 -0
  315. package/dist/ui/assets/AssetTraySortControl-C8Rztyal.js +1 -0
  316. package/dist/ui/assets/ContextAttachmentManager-Cj3c7X9y.js +3 -0
  317. package/dist/ui/assets/{DocumentWorkspace-BC7sy5TG.js → DocumentWorkspace-B5uvzems.js} +4 -4
  318. package/dist/ui/assets/EntityActivityTimeline-BVFvCB5R.js +1 -0
  319. package/dist/ui/assets/PlanningModule-B5DWQOGz.js +1 -0
  320. package/dist/ui/assets/PlanningModule-CoIR9hbV.css +1 -0
  321. package/dist/ui/assets/PlansPage-AY4cGWco.js +1 -0
  322. package/dist/ui/assets/TaskContextUpload-DZxTVesW.js +1 -0
  323. package/dist/ui/assets/TaskContextUpload-qmBeUw3u.css +1 -0
  324. package/dist/ui/assets/TaskSettings-BeWAuyZj.js +12 -0
  325. package/dist/ui/assets/{TaskforceAgentsModule-CkAxW19K.js → TaskforceAgentsModule-BifCYtjR.js} +6 -6
  326. package/dist/ui/assets/WorkflowManagerModule-DEr5di_r.js +1 -0
  327. package/dist/ui/assets/index-I5zhdtgt.js +7 -0
  328. package/dist/ui/assets/index-VRFsx3wz.css +1 -0
  329. package/dist/ui/assets/{vendor-icons-B8ZNsH1g.js → vendor-icons-D9Lpw-j4.js} +1 -1
  330. package/dist/ui/index.html +3 -3
  331. package/dist/utils/constants.d.ts +2 -2
  332. package/dist/utils/constants.js +1 -1
  333. package/dist/utils/contextAttachmentUpload.d.ts +2 -0
  334. package/dist/utils/contextAttachmentUpload.js +7 -0
  335. package/dist/utils/contextFiles.d.ts +1 -1
  336. package/dist/utils/contextFiles.js +4 -4
  337. package/dist/utils/taskActivity.d.ts +3 -2
  338. package/dist/utils/taskActivity.js +19 -2
  339. package/dist/utils/taskEditorReconciliation.d.ts +40 -0
  340. package/dist/utils/taskEditorReconciliation.js +217 -0
  341. package/dist/utils/taskNormalization.d.ts +1 -0
  342. package/dist/utils/taskNormalization.js +39 -0
  343. package/dist/utils/taskSearch.js +2 -2
  344. package/dist/utils/uiNotice.d.ts +4 -0
  345. package/package.json +21 -4
  346. package/dist/ui/assets/AiProfilesModule-DPxQeF9N.js +0 -1
  347. package/dist/ui/assets/AnnotatedAttachmentWorkspace-CBRZ8rca.css +0 -1
  348. package/dist/ui/assets/AnnotatedAttachmentWorkspace-DHNYaNg6.js +0 -3
  349. package/dist/ui/assets/AssetTraySortControl-DphbyHVc.js +0 -1
  350. package/dist/ui/assets/ContextAttachmentManager-CMxSzHqc.js +0 -3
  351. package/dist/ui/assets/EntityActivityTimeline-pJ2ZjZ-k.js +0 -1
  352. package/dist/ui/assets/PlanningModule-CeTqXMMz.css +0 -1
  353. package/dist/ui/assets/PlanningModule-t5GbvVR9.js +0 -1
  354. package/dist/ui/assets/PlansPage-BVH_NfgT.js +0 -1
  355. package/dist/ui/assets/TaskContextUpload-Ds4LnDzV.css +0 -1
  356. package/dist/ui/assets/TaskContextUpload-bl4fOOUz.js +0 -1
  357. package/dist/ui/assets/TaskSettings-xVsxSa0g.js +0 -12
  358. package/dist/ui/assets/WorkflowManagerModule-BKqIGVnF.js +0 -1
  359. package/dist/ui/assets/index-BzJlVnNS.css +0 -1
  360. package/dist/ui/assets/index-Cx7vZJHL.js +0 -7
@@ -3,11 +3,13 @@ import * as path from 'path';
3
3
  import * as os from 'os';
4
4
  import { createHash, pbkdf2Sync, randomBytes, timingSafeEqual } from 'crypto';
5
5
  import { fileURLToPath } from 'url';
6
+ import { assertPlanningIdentityColor, assertPlanningIdentityIcon, } from '../shared/planningIdentity.js';
6
7
  import { createDatabaseAdapter } from '../storage/createDatabaseAdapter.js';
7
8
  import { assertLocalDataOwnerPermit, } from '../server/localDataOwnerPermit.js';
8
9
  import { resolveLocalDatabaseIdentity } from '../server/localDatabaseIdentity.js';
9
10
  import { compareWorkspaceSyncStateRevisions, normalizeWorkspaceSyncStateRevision, } from '../sync/workspaceSyncState.js';
10
11
  import { WorkspaceSyncDurabilityStore } from '../sync/v3/syncDurabilityStore.js';
12
+ import { workspaceSyncV3RepairEntityReadBatchSize } from '../sync/v3/workspaceSyncV3RepairSqlBatching.js';
11
13
  import { isTaskMetadataActivityEventId } from '../sync/v3/taskMetadataActivityIdentity.js';
12
14
  import { isTaskLifecycleActivityEventId } from '../sync/v3/taskLifecycleActivityIdentity.js';
13
15
  import { resolveV3OwnedTaskLifecycleIds, resolveV3OwnedTaskMetadataIds, resolveV3OwnedTaskTaxonomyIds, } from '../sync/v3/taskMetadataCoexistence.js';
@@ -23,7 +25,7 @@ import { archiveAiProfileService, bindAiProfileConnectionService, getAiProfileBy
23
25
  import { syncCanonicalAttachmentLinksForTargetService, syncCanonicalTaskAttachmentLinksService, } from './AttachmentLinkService.js';
24
26
  import { listTaskAttachmentLinksForDurableSync as listTaskAttachmentLinksForDurableSyncService, listWorkspaceTaskAttachmentLinksForDurableSync as listWorkspaceTaskAttachmentLinksForDurableSyncService, normalizeTaskAttachmentDescriptorsForDurableSync as normalizeTaskAttachmentDescriptorsForDurableSyncService, resolveTaskAttachmentLinksForDurableSync as resolveTaskAttachmentLinksForDurableSyncService, } from './TaskAttachmentLinksDurableMutationService.js';
25
27
  import { EntityEventIdentityCollisionError, addEntityEventService, addTaskEventService, buildEntityActivityService, buildTaskActivityService, getEntityEventForSyncService, getTaskEventForSyncService, listEntityEventsInternalService, listWorkspaceEntityEventsForSyncService, listTaskEventsInternalService, listWorkspaceTaskEventsForSyncService, replaceEntityEventsService, replaceTaskCommentsService, replaceTaskEventsService, upsertEntityEventForSyncService, deleteEntityEventForSyncService, upsertTaskEventForSyncService, } from './TaskActivityService.js';
26
- import { addDeletedTaskRecordService, getDeletedTaskService, listDeletedTasksService, mapDeletedTaskRowService, normalizeTaskSnapshotForRestoreService, normalizeDeletedTaskRestoreLifecycleService, purgeExpiredDeletedTasksService, } from './DeletedTaskLifecycleService.js';
28
+ import { addDeletedTaskRecordService, getDeletedTaskService, listDeletedTasksService, listDeletedTasksReadOnlyService, mapDeletedTaskRowService, normalizeTaskSnapshotForRestoreService, normalizeDeletedTaskRestoreLifecycleService, purgeExpiredDeletedTasksService, } from './DeletedTaskLifecycleService.js';
27
29
  import { DELETED_TASK_RETENTION_DAYS } from '../shared/deletedTaskRetention.js';
28
30
  import { applyWorkspaceSyncSnapshotService, claimPushIdempotencyService, completePushIdempotencyClaimService, deleteDocumentWatermarkService, deleteDocumentWatermarkStrictService, getDocumentWatermarkService, listTasksForSyncService, listWorkspaceSyncDeletesSinceService, readPushIdempotencyService, releasePushIdempotencyClaimService, renewPushIdempotencyClaimService, recordSyncEventDurablyService, recordSyncEventService, upsertDocumentWatermarkService, upsertDocumentWatermarkStrictService, writePushIdempotencyService, } from './SyncReconciliationService.js';
29
31
  import { getGlobalSettingsService, hasPersistedGlobalSettingsService, readGlobalConfigService, updateGlobalSettingsService, writeGlobalConfigService, } from './GlobalSettingsService.js';
@@ -59,6 +61,7 @@ import { getImageReferenceLabel } from '../utils/imageReferences.js';
59
61
  import { getCanonicalDocumentName } from '../utils/documentNames.js';
60
62
  import { isPathInsideRoot, resolveStorageKeyPathInsideRoot } from '../utils/pathSafety.js';
61
63
  import { DEFAULT_TASKFORCE_THEME } from '../utils/theme.js';
64
+ import { normalizeTaskSearchQuery } from '../utils/taskSearch.js';
62
65
  import { ensureAssigneeTaskSchema, ensureAnnotatedAttachmentSessionsSchema, ensureAuthIdentitySchema, backfillPlanningAttachmentAssetIdentities, ensureBillingSchema, ensureDocumentReviewSessionsSchema, ensureDocumentReferenceSchema, ensureEntitlementsSchema, ensurePlanningReferenceSchema, ensureScheduleTaskSchema, ensureWorkstreamTaskOrderSchema, ensureTaskAssetsSchema, ensureTaskApproachRemoved, ensureAgentRolesSchema, ensureTaskforceAgentsSchema, ensureTaskforceAgentSkillsSchema, ensureTaskReferenceSchema, ensureTenantSchema, ensureWorkflowSchema, ensureWorkspaceAssetsSchema, ensureWorkspaceSchema, ensureLocalSyncCoordinatorSchema, ensureWorkspaceSyncDurabilitySchema } from '../migrations/taskSchemaMigrations.js';
63
66
  import { repairTaskUpdatedAtOnlySyncDrift } from '../migrations/taskUpdatedAtSyncDriftMigration.js';
64
67
  export class TaskforceRuleError extends Error {
@@ -1375,9 +1378,13 @@ export class TaskforceCore {
1375
1378
  // Initialize Database Adapter (SQLite default; Postgres scaffolded)
1376
1379
  const provider = config.database?.provider || 'sqlite';
1377
1380
  const connectionString = config.database?.connectionString || process.env.TASKFORCE_DATABASE_URL;
1381
+ const isCloudReadWorker = config.initializationMode === 'cloud-read-worker';
1378
1382
  if (provider === 'postgres' && !connectionString) {
1379
1383
  throw new Error('[Taskforce] Postgres provider requires a connection string (TASKFORCE_DATABASE_URL).');
1380
1384
  }
1385
+ if (isCloudReadWorker && (this.runtimeMode !== 'cloud' || provider !== 'postgres')) {
1386
+ throw new Error('[Taskforce] Cloud read-worker initialization requires cloud runtime with PostgreSQL.');
1387
+ }
1381
1388
  if (this.runtimeMode === 'local') {
1382
1389
  assertLocalDataOwnerPermit({
1383
1390
  permit: config.localDataOwnerPermit,
@@ -1391,8 +1398,10 @@ export class TaskforceCore {
1391
1398
  databaseIdentityKind: provider === 'sqlite' ? 'path' : 'opaque',
1392
1399
  });
1393
1400
  }
1394
- this.ensureDirectories();
1395
- this.migrateLegacyGlobalSettingsDbIfNeeded();
1401
+ if (!isCloudReadWorker) {
1402
+ this.ensureDirectories();
1403
+ this.migrateLegacyGlobalSettingsDbIfNeeded();
1404
+ }
1396
1405
  this.db = createDatabaseAdapter({
1397
1406
  provider,
1398
1407
  sqliteDbPath: this.dbPath,
@@ -1428,53 +1437,59 @@ export class TaskforceCore {
1428
1437
  throw new Error(`[Taskforce] Failed to initialize global settings DB at ${this.globalSettingsDbPath}: ${String(error?.message || error)}`);
1429
1438
  }
1430
1439
  }
1431
- this.initializeSchema();
1432
- ensureTaskApproachRemoved(this.db);
1433
- ensureTenantSchema(this.db);
1434
- ensureWorkspaceSchema(this.db);
1435
- ensureWorkspaceSyncDurabilitySchema(this.db);
1436
- if (this.runtimeMode === 'local') {
1437
- ensureLocalSyncCoordinatorSchema(this.db);
1438
- }
1439
- ensureScheduleTaskSchema(this.db);
1440
- ensureWorkstreamTaskOrderSchema(this.db);
1441
- ensureAssigneeTaskSchema(this.db);
1442
- ensureTaskReferenceSchema(this.db);
1443
- ensurePlanningReferenceSchema(this.db);
1444
- ensureTaskAssetsSchema(this.db);
1445
- ensureWorkspaceAssetsSchema(this.db);
1446
- backfillPlanningAttachmentAssetIdentities(this.db, this.tenantId);
1447
- ensureDocumentReferenceSchema(this.db);
1448
- ensureDocumentReviewSessionsSchema(this.db);
1449
- ensureAnnotatedAttachmentSessionsSchema(this.db);
1450
- ensureTaskforceAgentsSchema(this.db);
1451
- ensureAgentRolesSchema(this.db);
1452
- ensureTaskforceAgentSkillsSchema(this.db);
1453
- ensureWorkflowSchema(this.db);
1454
- ensureBillingSchema(this.db);
1455
- ensureEntitlementsSchema(this.db);
1456
- ensureAuthIdentitySchema(this.db);
1457
- this.performMigrationIfNeeded();
1458
- repairTaskUpdatedAtOnlySyncDrift({
1459
- db: this.db,
1460
- tenantId: this.tenantId,
1461
- loadTask: (taskId, workspaceId) => this.getTask(taskId, workspaceId),
1462
- });
1440
+ if (!isCloudReadWorker) {
1441
+ this.initializeSchema();
1442
+ ensureTaskApproachRemoved(this.db);
1443
+ ensureTenantSchema(this.db);
1444
+ ensureWorkspaceSchema(this.db);
1445
+ ensureWorkspaceSyncDurabilitySchema(this.db);
1446
+ if (this.runtimeMode === 'local') {
1447
+ ensureLocalSyncCoordinatorSchema(this.db);
1448
+ }
1449
+ ensureScheduleTaskSchema(this.db);
1450
+ ensureWorkstreamTaskOrderSchema(this.db);
1451
+ ensureAssigneeTaskSchema(this.db);
1452
+ ensureTaskReferenceSchema(this.db);
1453
+ ensurePlanningReferenceSchema(this.db);
1454
+ ensureTaskAssetsSchema(this.db);
1455
+ ensureWorkspaceAssetsSchema(this.db);
1456
+ backfillPlanningAttachmentAssetIdentities(this.db, this.tenantId);
1457
+ ensureDocumentReferenceSchema(this.db);
1458
+ ensureDocumentReviewSessionsSchema(this.db);
1459
+ ensureAnnotatedAttachmentSessionsSchema(this.db);
1460
+ ensureTaskforceAgentsSchema(this.db);
1461
+ ensureAgentRolesSchema(this.db);
1462
+ ensureTaskforceAgentSkillsSchema(this.db);
1463
+ ensureWorkflowSchema(this.db);
1464
+ ensureBillingSchema(this.db);
1465
+ ensureEntitlementsSchema(this.db);
1466
+ ensureAuthIdentitySchema(this.db);
1467
+ this.performMigrationIfNeeded();
1468
+ repairTaskUpdatedAtOnlySyncDrift({
1469
+ db: this.db,
1470
+ tenantId: this.tenantId,
1471
+ loadTask: (taskId, workspaceId) => this.getTask(taskId, workspaceId),
1472
+ });
1473
+ }
1463
1474
  this.planEntitlementService = new PlanEntitlementService(this.db, this.tenantId, {
1464
1475
  getWorkspaceOwnerUserId: (wid) => this.getWorkspaceOwnerUserId(wid),
1465
1476
  readGlobalConfig: () => this.readGlobalConfig(),
1466
1477
  writeGlobalConfig: (config) => this.writeGlobalConfig(config),
1467
1478
  });
1468
- this.planEntitlementService.repairPlanCatalogDefaultsIfNeeded();
1469
- this.planEntitlementService.normalizeLegacyPlanDataIfNeeded();
1470
- this.planEntitlementService.backfillAccountEntitlements();
1479
+ if (!isCloudReadWorker) {
1480
+ this.planEntitlementService.repairPlanCatalogDefaultsIfNeeded();
1481
+ this.planEntitlementService.normalizeLegacyPlanDataIfNeeded();
1482
+ this.planEntitlementService.backfillAccountEntitlements();
1483
+ }
1471
1484
  this.authTokenService = new AuthTokenService(this.db, this.tenantId);
1472
1485
  this.identityService = new AuthIdentityService(this.db, this.tenantId);
1473
1486
  this.authTokenService.setIdentityService(this.identityService);
1474
1487
  this.mcpTokenService = new McpTokenService(this.db, this.tenantId, {
1475
1488
  getAccountAccessStatus: (userId) => this.getAccountAccessStatus(userId),
1476
1489
  });
1477
- purgeExpiredDeletedTasksService(this.getDeletedTaskLifecycleServiceDeps());
1490
+ if (!isCloudReadWorker) {
1491
+ purgeExpiredDeletedTasksService(this.getDeletedTaskLifecycleServiceDeps());
1492
+ }
1478
1493
  }
1479
1494
  close() {
1480
1495
  if (this.globalSettingsDb && this.globalSettingsDb.open) {
@@ -1751,6 +1766,7 @@ export class TaskforceCore {
1751
1766
  order_in_day INTEGER,
1752
1767
  workstream_order INTEGER,
1753
1768
  workstream_id TEXT,
1769
+ card_cover_asset_id TEXT,
1754
1770
  FOREIGN KEY(workstream_id) REFERENCES workstreams(id) ON DELETE SET NULL
1755
1771
  );
1756
1772
 
@@ -1832,6 +1848,20 @@ export class TaskforceCore {
1832
1848
  FOREIGN KEY(initiative_id) REFERENCES initiatives(id) ON DELETE SET NULL
1833
1849
  );
1834
1850
 
1851
+ CREATE TABLE IF NOT EXISTS planning_entity_identities (
1852
+ tenant_id TEXT NOT NULL DEFAULT 'default',
1853
+ workspace_id TEXT NOT NULL DEFAULT 'default',
1854
+ entity_type TEXT NOT NULL,
1855
+ entity_id TEXT NOT NULL,
1856
+ icon TEXT,
1857
+ color TEXT,
1858
+ updated_at TEXT NOT NULL,
1859
+ PRIMARY KEY (tenant_id, workspace_id, entity_type, entity_id)
1860
+ );
1861
+
1862
+ CREATE INDEX IF NOT EXISTS idx_planning_entity_identities_updated
1863
+ ON planning_entity_identities (tenant_id, workspace_id, updated_at, entity_type, entity_id);
1864
+
1835
1865
  CREATE TABLE IF NOT EXISTS workspace_workstream_reference_sequences (
1836
1866
  tenant_id TEXT NOT NULL DEFAULT 'default',
1837
1867
  workspace_id TEXT NOT NULL DEFAULT 'default',
@@ -2222,6 +2252,9 @@ export class TaskforceCore {
2222
2252
  if (!taskColumns.some((c) => c.name === 'workstream_id')) {
2223
2253
  this.db.exec(`ALTER TABLE tasks ADD COLUMN workstream_id TEXT`);
2224
2254
  }
2255
+ if (!taskColumns.some((c) => c.name === 'card_cover_asset_id')) {
2256
+ this.db.exec(`ALTER TABLE tasks ADD COLUMN card_cover_asset_id TEXT`);
2257
+ }
2225
2258
  const initiativeColumns = this.db.prepare(`PRAGMA table_info(initiatives)`).all();
2226
2259
  if (!initiativeColumns.some((c) => c.name === 'comments_json')) {
2227
2260
  this.db.exec(`ALTER TABLE initiatives ADD COLUMN comments_json TEXT`);
@@ -2258,6 +2291,12 @@ export class TaskforceCore {
2258
2291
  catch {
2259
2292
  // Best-effort schema repair for existing deployments.
2260
2293
  }
2294
+ try {
2295
+ this.db.exec(`ALTER TABLE tasks ADD COLUMN IF NOT EXISTS card_cover_asset_id TEXT`);
2296
+ }
2297
+ catch {
2298
+ // Best-effort schema repair for existing deployments.
2299
+ }
2261
2300
  try {
2262
2301
  this.db.exec(`ALTER TABLE initiatives ADD COLUMN IF NOT EXISTS comments_json TEXT`);
2263
2302
  this.db.exec(`ALTER TABLE initiatives ADD COLUMN IF NOT EXISTS attachments_json TEXT`);
@@ -3120,7 +3159,17 @@ export class TaskforceCore {
3120
3159
  return ['manualComplexityEnabled'];
3121
3160
  }
3122
3161
  getUserBackedProjectConfigKeys() {
3123
- return ['theme', 'jsonBackupEnabled', 'checklistDropdownEnabled', 'showTaskCardStatusLabel'];
3162
+ return [
3163
+ 'theme',
3164
+ 'jsonBackupEnabled',
3165
+ 'checklistDropdownEnabled',
3166
+ 'showTaskCardStatusLabel',
3167
+ 'showTaskCardDescription',
3168
+ 'showTaskCardAttachments',
3169
+ 'showTaskCardChecklistProgress',
3170
+ 'showTaskCardLatestActivity',
3171
+ 'tintTaskCardActivityBackground',
3172
+ ];
3124
3173
  }
3125
3174
  isWorkspaceBackedProjectConfigKey(key) {
3126
3175
  return this.getWorkspaceBackedProjectConfigKeys().includes(key);
@@ -3163,6 +3212,21 @@ export class TaskforceCore {
3163
3212
  if (typeof source.showTaskCardStatusLabel === 'boolean') {
3164
3213
  next.showTaskCardStatusLabel = source.showTaskCardStatusLabel;
3165
3214
  }
3215
+ if (typeof source.showTaskCardDescription === 'boolean') {
3216
+ next.showTaskCardDescription = source.showTaskCardDescription;
3217
+ }
3218
+ if (typeof source.showTaskCardAttachments === 'boolean') {
3219
+ next.showTaskCardAttachments = source.showTaskCardAttachments;
3220
+ }
3221
+ if (typeof source.showTaskCardChecklistProgress === 'boolean') {
3222
+ next.showTaskCardChecklistProgress = source.showTaskCardChecklistProgress;
3223
+ }
3224
+ if (typeof source.showTaskCardLatestActivity === 'boolean') {
3225
+ next.showTaskCardLatestActivity = source.showTaskCardLatestActivity;
3226
+ }
3227
+ if (typeof source.tintTaskCardActivityBackground === 'boolean') {
3228
+ next.tintTaskCardActivityBackground = source.tintTaskCardActivityBackground;
3229
+ }
3166
3230
  return next;
3167
3231
  }
3168
3232
  stripPersistedProjectConfigKeys(config) {
@@ -3679,7 +3743,7 @@ export class TaskforceCore {
3679
3743
  const originalFilename = row.original_filename ? String(row.original_filename) : path.basename(storageKey);
3680
3744
  const assetKind = String(canonicalAsset?.kind || row.kind || '').trim();
3681
3745
  const isCanonicalDocument = Boolean(canonicalAsset && isValidCanonicalDocumentAsset(canonicalAsset));
3682
- const displayName = isCanonicalDocument
3746
+ const caption = isCanonicalDocument
3683
3747
  ? getCanonicalDocumentName({
3684
3748
  logicalName: canonicalAsset?.logicalName || (row.logical_name ? String(row.logical_name) : null),
3685
3749
  originalFilename,
@@ -3687,11 +3751,7 @@ export class TaskforceCore {
3687
3751
  assetId: String(row.asset_id || '').trim() || null
3688
3752
  })
3689
3753
  : (canonicalAsset?.logicalName || (row.logical_name ? String(row.logical_name) : originalFilename));
3690
- const caption = isCanonicalDocument
3691
- ? displayName
3692
- : (row.display_name
3693
- ? String(row.display_name)
3694
- : displayName);
3754
+ const displayName = row.display_name ? String(row.display_name) : originalFilename;
3695
3755
  const referenceNumber = typeof canonicalAsset?.referenceNumber === 'number'
3696
3756
  ? canonicalAsset.referenceNumber
3697
3757
  : (Number.isFinite(Number(row.reference_number))
@@ -3825,6 +3885,9 @@ export class TaskforceCore {
3825
3885
  orderInDay: this.sanitizeOrderInDay(row.order_in_day),
3826
3886
  workstreamOrder: this.sanitizeOrderInDay(row.workstream_order),
3827
3887
  workstreamId: this.sanitizeWorkstreamId(row.workstream_id),
3888
+ cardCoverAssetId: typeof row.card_cover_asset_id === 'string' && row.card_cover_asset_id.trim()
3889
+ ? row.card_cover_asset_id.trim()
3890
+ : null,
3828
3891
  taxonomies: JSON.parse(row.taxonomies || '{}'),
3829
3892
  createdBy: this.normalizeCreatedBy(row.created_by, workspaceId, { preserveGenericAi: true }),
3830
3893
  createdByActor: this.resolveWorkspaceActor(workspaceId, row.created_by),
@@ -3857,6 +3920,11 @@ export class TaskforceCore {
3857
3920
  title: String(row.title || '').trim() || 'Untitled Initiative',
3858
3921
  description: typeof row.description === 'string' ? row.description : null,
3859
3922
  ownerId: typeof row.owner_id === 'string' && row.owner_id.trim().length > 0 ? row.owner_id.trim() : null,
3923
+ icon: typeof row.identity_icon === 'string' && row.identity_icon.trim().length > 0 ? row.identity_icon.trim() : null,
3924
+ color: typeof row.identity_color === 'string' && row.identity_color.trim().length > 0 ? row.identity_color.trim() : null,
3925
+ identityUpdatedAt: typeof row.identity_updated_at === 'string' && row.identity_updated_at.trim().length > 0
3926
+ ? row.identity_updated_at.trim()
3927
+ : null,
3860
3928
  createdAt: String(row.created_at || '').trim() || new Date().toISOString(),
3861
3929
  updatedAt: typeof row.updated_at === 'string' && row.updated_at.trim().length > 0 ? row.updated_at.trim() : undefined,
3862
3930
  order: row.sort_order == null ? null : Number(row.sort_order),
@@ -3890,6 +3958,11 @@ export class TaskforceCore {
3890
3958
  title: String(row.title || '').trim() || 'Untitled Workstream',
3891
3959
  description: typeof row.description === 'string' ? row.description : null,
3892
3960
  ownerId: typeof row.owner_id === 'string' && row.owner_id.trim().length > 0 ? row.owner_id.trim() : null,
3961
+ icon: typeof row.identity_icon === 'string' && row.identity_icon.trim().length > 0 ? row.identity_icon.trim() : null,
3962
+ color: typeof row.identity_color === 'string' && row.identity_color.trim().length > 0 ? row.identity_color.trim() : null,
3963
+ identityUpdatedAt: typeof row.identity_updated_at === 'string' && row.identity_updated_at.trim().length > 0
3964
+ ? row.identity_updated_at.trim()
3965
+ : null,
3893
3966
  createdAt: String(row.created_at || '').trim() || new Date().toISOString(),
3894
3967
  updatedAt: typeof row.updated_at === 'string' && row.updated_at.trim().length > 0 ? row.updated_at.trim() : undefined,
3895
3968
  order: row.sort_order == null ? null : Number(row.sort_order),
@@ -3945,9 +4018,15 @@ export class TaskforceCore {
3945
4018
  return null;
3946
4019
  const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
3947
4020
  const row = this.db.prepare(`
3948
- SELECT *
3949
- FROM initiatives
3950
- WHERE tenant_id = ? AND workspace_id = ? AND id = ?
4021
+ SELECT i.*, identity.icon AS identity_icon, identity.color AS identity_color,
4022
+ identity.updated_at AS identity_updated_at
4023
+ FROM initiatives i
4024
+ LEFT JOIN planning_entity_identities identity
4025
+ ON identity.tenant_id = i.tenant_id
4026
+ AND identity.workspace_id = i.workspace_id
4027
+ AND identity.entity_type = 'initiative'
4028
+ AND identity.entity_id = i.id
4029
+ WHERE i.tenant_id = ? AND i.workspace_id = ? AND i.id = ?
3951
4030
  LIMIT 1
3952
4031
  `).get(this.tenantId, normalizedWorkspaceId, normalizedId);
3953
4032
  if (!row)
@@ -3957,10 +4036,16 @@ export class TaskforceCore {
3957
4036
  listInitiatives(workspaceId) {
3958
4037
  const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
3959
4038
  const rows = this.db.prepare(`
3960
- SELECT *
3961
- FROM initiatives
3962
- WHERE tenant_id = ? AND workspace_id = ?
3963
- ORDER BY COALESCE(sort_order, 2147483647), created_at ASC
4039
+ SELECT i.*, identity.icon AS identity_icon, identity.color AS identity_color,
4040
+ identity.updated_at AS identity_updated_at
4041
+ FROM initiatives i
4042
+ LEFT JOIN planning_entity_identities identity
4043
+ ON identity.tenant_id = i.tenant_id
4044
+ AND identity.workspace_id = i.workspace_id
4045
+ AND identity.entity_type = 'initiative'
4046
+ AND identity.entity_id = i.id
4047
+ WHERE i.tenant_id = ? AND i.workspace_id = ?
4048
+ ORDER BY COALESCE(i.sort_order, 2147483647), i.created_at ASC, i.id ASC
3964
4049
  `).all(this.tenantId, normalizedWorkspaceId);
3965
4050
  return rows.map((row) => this.rowToInitiative(row));
3966
4051
  }
@@ -3968,10 +4053,16 @@ export class TaskforceCore {
3968
4053
  const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
3969
4054
  const safeLimit = Math.max(1, Math.min(10000, Math.floor(limit)));
3970
4055
  const rows = this.db.prepare(`
3971
- SELECT *
3972
- FROM initiatives
3973
- WHERE tenant_id = ? AND workspace_id = ? AND id > ?
3974
- ORDER BY id ASC
4056
+ SELECT i.*, identity.icon AS identity_icon, identity.color AS identity_color,
4057
+ identity.updated_at AS identity_updated_at
4058
+ FROM initiatives i
4059
+ LEFT JOIN planning_entity_identities identity
4060
+ ON identity.tenant_id = i.tenant_id
4061
+ AND identity.workspace_id = i.workspace_id
4062
+ AND identity.entity_type = 'initiative'
4063
+ AND identity.entity_id = i.id
4064
+ WHERE i.tenant_id = ? AND i.workspace_id = ? AND i.id > ?
4065
+ ORDER BY i.id ASC
3975
4066
  LIMIT ?
3976
4067
  `).all(this.tenantId, normalizedWorkspaceId, String(afterId || ''), safeLimit);
3977
4068
  return rows.map((row) => this.rowToInitiative(row));
@@ -4234,9 +4325,15 @@ export class TaskforceCore {
4234
4325
  return null;
4235
4326
  const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
4236
4327
  const row = this.db.prepare(`
4237
- SELECT *
4238
- FROM workstreams
4239
- WHERE tenant_id = ? AND workspace_id = ? AND id = ?
4328
+ SELECT w.*, identity.icon AS identity_icon, identity.color AS identity_color,
4329
+ identity.updated_at AS identity_updated_at
4330
+ FROM workstreams w
4331
+ LEFT JOIN planning_entity_identities identity
4332
+ ON identity.tenant_id = w.tenant_id
4333
+ AND identity.workspace_id = w.workspace_id
4334
+ AND identity.entity_type = 'workstream'
4335
+ AND identity.entity_id = w.id
4336
+ WHERE w.tenant_id = ? AND w.workspace_id = ? AND w.id = ?
4240
4337
  LIMIT 1
4241
4338
  `).get(this.tenantId, normalizedWorkspaceId, normalizedId);
4242
4339
  if (!row)
@@ -4246,10 +4343,16 @@ export class TaskforceCore {
4246
4343
  listWorkstreams(workspaceId) {
4247
4344
  const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
4248
4345
  const rows = this.db.prepare(`
4249
- SELECT *
4250
- FROM workstreams
4251
- WHERE tenant_id = ? AND workspace_id = ?
4252
- ORDER BY COALESCE(sort_order, 2147483647), created_at ASC
4346
+ SELECT w.*, identity.icon AS identity_icon, identity.color AS identity_color,
4347
+ identity.updated_at AS identity_updated_at
4348
+ FROM workstreams w
4349
+ LEFT JOIN planning_entity_identities identity
4350
+ ON identity.tenant_id = w.tenant_id
4351
+ AND identity.workspace_id = w.workspace_id
4352
+ AND identity.entity_type = 'workstream'
4353
+ AND identity.entity_id = w.id
4354
+ WHERE w.tenant_id = ? AND w.workspace_id = ?
4355
+ ORDER BY COALESCE(w.sort_order, 2147483647), w.created_at ASC, w.id ASC
4253
4356
  `).all(this.tenantId, normalizedWorkspaceId);
4254
4357
  return rows.map((row) => this.rowToWorkstream(row));
4255
4358
  }
@@ -4257,14 +4360,157 @@ export class TaskforceCore {
4257
4360
  const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
4258
4361
  const safeLimit = Math.max(1, Math.min(10000, Math.floor(limit)));
4259
4362
  const rows = this.db.prepare(`
4260
- SELECT *
4261
- FROM workstreams
4262
- WHERE tenant_id = ? AND workspace_id = ? AND id > ?
4263
- ORDER BY id ASC
4363
+ SELECT w.*, identity.icon AS identity_icon, identity.color AS identity_color,
4364
+ identity.updated_at AS identity_updated_at
4365
+ FROM workstreams w
4366
+ LEFT JOIN planning_entity_identities identity
4367
+ ON identity.tenant_id = w.tenant_id
4368
+ AND identity.workspace_id = w.workspace_id
4369
+ AND identity.entity_type = 'workstream'
4370
+ AND identity.entity_id = w.id
4371
+ WHERE w.tenant_id = ? AND w.workspace_id = ? AND w.id > ?
4372
+ ORDER BY w.id ASC
4264
4373
  LIMIT ?
4265
4374
  `).all(this.tenantId, normalizedWorkspaceId, String(afterId || ''), safeLimit);
4266
4375
  return rows.map((row) => this.rowToWorkstream(row));
4267
4376
  }
4377
+ listPlanningEntityIdentities(workspaceId) {
4378
+ const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
4379
+ const rows = this.db.prepare(`
4380
+ SELECT entity_type, entity_id, icon, color, updated_at
4381
+ FROM planning_entity_identities
4382
+ WHERE tenant_id = ? AND workspace_id = ?
4383
+ ORDER BY updated_at ASC, entity_type ASC, entity_id ASC
4384
+ `).all(this.tenantId, normalizedWorkspaceId);
4385
+ return rows.map((row) => ({
4386
+ entityType: row.entity_type === 'workstream' ? 'workstream' : 'initiative',
4387
+ entityId: String(row.entity_id || '').trim(),
4388
+ icon: assertPlanningIdentityIcon(row.icon),
4389
+ color: assertPlanningIdentityColor(row.color),
4390
+ updatedAt: String(row.updated_at || '').trim(),
4391
+ }));
4392
+ }
4393
+ writePlanningEntityIdentity(identity, workspaceId) {
4394
+ const existing = this.db.prepare(`
4395
+ SELECT icon, color, updated_at
4396
+ FROM planning_entity_identities
4397
+ WHERE tenant_id = ? AND workspace_id = ? AND entity_type = ? AND entity_id = ?
4398
+ LIMIT 1
4399
+ `).get(this.tenantId, workspaceId, identity.entityType, identity.entityId);
4400
+ if (existing) {
4401
+ this.db.prepare(`
4402
+ UPDATE planning_entity_identities
4403
+ SET icon = ?, color = ?, updated_at = ?
4404
+ WHERE tenant_id = ? AND workspace_id = ? AND entity_type = ? AND entity_id = ?
4405
+ `).run(identity.icon, identity.color, identity.updatedAt, this.tenantId, workspaceId, identity.entityType, identity.entityId);
4406
+ }
4407
+ else {
4408
+ this.db.prepare(`
4409
+ INSERT INTO planning_entity_identities (
4410
+ tenant_id, workspace_id, entity_type, entity_id, icon, color, updated_at
4411
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
4412
+ `).run(this.tenantId, workspaceId, identity.entityType, identity.entityId, identity.icon, identity.color, identity.updatedAt);
4413
+ }
4414
+ return identity;
4415
+ }
4416
+ updatePlanningEntityIdentity(entityType, entityId, patch, workspaceId) {
4417
+ const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
4418
+ const normalizedEntityId = String(entityId || '').trim();
4419
+ const entity = entityType === 'initiative'
4420
+ ? this.getInitiative(normalizedEntityId, normalizedWorkspaceId)
4421
+ : this.getWorkstream(normalizedEntityId, normalizedWorkspaceId);
4422
+ if (!entity) {
4423
+ throw new TaskforceRuleError(`${entityType === 'initiative' ? 'Initiative' : 'Workstream'} not found: ${normalizedEntityId}`, 404, entityType === 'initiative' ? 'INITIATIVE_NOT_FOUND' : 'WORKSTREAM_NOT_FOUND');
4424
+ }
4425
+ if (entity.isArchived) {
4426
+ throw new TaskforceRuleError('Restore the planning entity before changing its visual identity.', 409, 'PLANNING_IDENTITY_ARCHIVED');
4427
+ }
4428
+ if (!Object.prototype.hasOwnProperty.call(patch, 'icon')
4429
+ && !Object.prototype.hasOwnProperty.call(patch, 'color')) {
4430
+ throw new TaskforceRuleError('Planning identity update must include icon or color.', 400, 'PLANNING_IDENTITY_INVALID');
4431
+ }
4432
+ let icon;
4433
+ let color;
4434
+ try {
4435
+ icon = Object.prototype.hasOwnProperty.call(patch, 'icon')
4436
+ ? assertPlanningIdentityIcon(patch.icon)
4437
+ : assertPlanningIdentityIcon(entity.icon);
4438
+ color = Object.prototype.hasOwnProperty.call(patch, 'color')
4439
+ ? assertPlanningIdentityColor(patch.color)
4440
+ : assertPlanningIdentityColor(entity.color);
4441
+ }
4442
+ catch (error) {
4443
+ throw new TaskforceRuleError(error instanceof Error ? error.message : 'Planning identity is invalid.', 400, 'PLANNING_IDENTITY_INVALID');
4444
+ }
4445
+ this.writePlanningEntityIdentity({
4446
+ entityType,
4447
+ entityId: normalizedEntityId,
4448
+ icon,
4449
+ color,
4450
+ updatedAt: new Date().toISOString(),
4451
+ }, normalizedWorkspaceId);
4452
+ this.emitRealtimeMutation({
4453
+ workspaceId: normalizedWorkspaceId,
4454
+ entityType,
4455
+ changeType: 'identity-updated',
4456
+ entityIds: [normalizedEntityId],
4457
+ affectedCollections: ['planning'],
4458
+ });
4459
+ return entityType === 'initiative'
4460
+ ? this.getInitiative(normalizedEntityId, normalizedWorkspaceId)
4461
+ : this.getWorkstream(normalizedEntityId, normalizedWorkspaceId);
4462
+ }
4463
+ applyPlanningEntityIdentityForSync(identity, workspaceId) {
4464
+ const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId || undefined);
4465
+ const entityType = identity.entityType === 'workstream' ? 'workstream' : 'initiative';
4466
+ const entityId = String(identity.entityId || '').trim();
4467
+ const updatedAt = String(identity.updatedAt || '').trim();
4468
+ if (!entityId || !Number.isFinite(Date.parse(updatedAt))) {
4469
+ throw new TaskforceRuleError('Planning identity sync payload is invalid.', 400, 'PLANNING_IDENTITY_INVALID');
4470
+ }
4471
+ const entityExists = entityType === 'initiative'
4472
+ ? this.initiativeExistsForSync(entityId, normalizedWorkspaceId)
4473
+ : this.workstreamExistsForSync(entityId, normalizedWorkspaceId);
4474
+ if (!entityExists)
4475
+ return null;
4476
+ const incoming = {
4477
+ entityType,
4478
+ entityId,
4479
+ icon: assertPlanningIdentityIcon(identity.icon),
4480
+ color: assertPlanningIdentityColor(identity.color),
4481
+ updatedAt: new Date(Date.parse(updatedAt)).toISOString(),
4482
+ };
4483
+ const existing = this.db.prepare(`
4484
+ SELECT icon, color, updated_at
4485
+ FROM planning_entity_identities
4486
+ WHERE tenant_id = ? AND workspace_id = ? AND entity_type = ? AND entity_id = ?
4487
+ LIMIT 1
4488
+ `).get(this.tenantId, normalizedWorkspaceId, entityType, entityId);
4489
+ if (existing) {
4490
+ const existingUpdatedAt = String(existing.updated_at || '').trim();
4491
+ const timeComparison = incoming.updatedAt.localeCompare(existingUpdatedAt);
4492
+ const incomingTieBreak = `${incoming.icon || ''}\u0000${incoming.color || ''}`;
4493
+ const existingTieBreak = `${String(existing.icon || '')}\u0000${String(existing.color || '')}`;
4494
+ if (timeComparison < 0 || (timeComparison === 0 && incomingTieBreak <= existingTieBreak)) {
4495
+ return {
4496
+ entityType,
4497
+ entityId,
4498
+ icon: assertPlanningIdentityIcon(existing.icon),
4499
+ color: assertPlanningIdentityColor(existing.color),
4500
+ updatedAt: existingUpdatedAt,
4501
+ };
4502
+ }
4503
+ }
4504
+ const applied = this.writePlanningEntityIdentity(incoming, normalizedWorkspaceId);
4505
+ this.emitRealtimeMutation({
4506
+ workspaceId: normalizedWorkspaceId,
4507
+ entityType,
4508
+ changeType: 'identity-updated',
4509
+ entityIds: [entityId],
4510
+ affectedCollections: ['planning'],
4511
+ });
4512
+ return applied;
4513
+ }
4268
4514
  listPlanningBootstrap(workspaceId, diagnostics) {
4269
4515
  const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
4270
4516
  const startedAt = Date.now();
@@ -5273,6 +5519,7 @@ export class TaskforceCore {
5273
5519
  generateEntityId: (prefix) => this.generateEntityId(prefix),
5274
5520
  normalizeWorkspaceId: (workspaceId) => this.normalizeWorkspaceId(workspaceId),
5275
5521
  resolveWorkspaceActor: (workspaceId, actorRef) => this.resolveWorkspaceActor(workspaceId, actorRef),
5522
+ resolveWorkspaceActors: (workspaceId, actorRefs) => this.resolveWorkspaceActors(workspaceId, actorRefs),
5276
5523
  sanitizeOrderInDay: (value) => this.sanitizeOrderInDay(value),
5277
5524
  sanitizeDateOnly: (value) => this.sanitizeDateOnly(value),
5278
5525
  normalizeTaskEventActor: (actorRef, workspaceId) => this.normalizeTaskEventActor(actorRef, workspaceId),
@@ -5302,6 +5549,10 @@ export class TaskforceCore {
5302
5549
  }
5303
5550
  return apply(durableDeleteOwnedKeys);
5304
5551
  })(),
5552
+ listDurableDeleteOwnedTaskIds: (workspaceId) => new Set(new WorkspaceSyncDurabilityStore(this.db)
5553
+ .listTaskMetadataOwnership({ tenantId: this.tenantId, workspaceId })
5554
+ .filter((ownership) => ownership.activeDeleteOperation || ownership.deleteOwned)
5555
+ .map((ownership) => ownership.taskId)),
5305
5556
  };
5306
5557
  }
5307
5558
  getSyncReconciliationServiceDeps() {
@@ -5348,6 +5599,489 @@ export class TaskforceCore {
5348
5599
  config: this.readConfig()
5349
5600
  };
5350
5601
  }
5602
+ listTaskSummaries(workspaceId, archived) {
5603
+ const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
5604
+ const rows = this.db.prepare(`
5605
+ WITH ranked_events AS (
5606
+ SELECT e.entity_id,
5607
+ e.id,
5608
+ e.action,
5609
+ e.actor,
5610
+ e.actor_type,
5611
+ e.details_json,
5612
+ e.created_at,
5613
+ ROW_NUMBER() OVER (
5614
+ PARTITION BY e.entity_id
5615
+ ORDER BY e.created_at DESC, e.id DESC
5616
+ ) AS event_rank
5617
+ FROM entity_events e
5618
+ JOIN tasks scoped_task
5619
+ ON scoped_task.tenant_id = e.tenant_id
5620
+ AND scoped_task.workspace_id = e.workspace_id
5621
+ AND scoped_task.id = e.entity_id
5622
+ WHERE e.tenant_id = ? AND e.workspace_id = ?
5623
+ AND e.entity_type = 'task'
5624
+ AND scoped_task.is_archived = ?
5625
+ -- Comment activity is represented by the comment itself; the
5626
+ -- paired audit event is intentionally hidden by taskActivity.
5627
+ AND e.action <> 'task-comment-added'
5628
+ -- Durable text-only autosaves retain an empty task-updated event
5629
+ -- for stable replay identity, but full task activity hides it.
5630
+ -- Match that visibility rule before ranking so an internal event
5631
+ -- cannot displace the latest meaningful card activity. REPLACE is
5632
+ -- intentionally used instead of dialect-specific JSON operators.
5633
+ AND NOT (
5634
+ e.action = 'task-updated'
5635
+ AND REPLACE(COALESCE(e.details_json, ''), ' ', '') LIKE '%"saveSource":"autosave"%'
5636
+ AND REPLACE(COALESCE(e.details_json, ''), ' ', '') LIKE '%"changes":{}%'
5637
+ )
5638
+ )
5639
+ SELECT t.id, t.reference_number, t.local_reference_number, t.title, t.status, t.priority,
5640
+ t.complexity, t.type, t.category, t.created_at, t.updated_at, t.completed_at,
5641
+ t.canceled_reason, t.is_archived, t.scheduled_date, t.due_date,
5642
+ t.scheduled_week_key, t.order_in_day, t.workstream_order, t.workstream_id,
5643
+ t.card_cover_asset_id, t.taxonomies, t.created_by, t.assignee,
5644
+ event.id AS latest_event_id,
5645
+ event.action AS latest_event_action,
5646
+ event.actor AS latest_event_actor,
5647
+ event.actor_type AS latest_event_actor_type,
5648
+ event.details_json AS latest_event_details_json,
5649
+ event.created_at AS latest_event_at
5650
+ FROM tasks t
5651
+ LEFT JOIN ranked_events event
5652
+ ON event.entity_id = t.id AND event.event_rank = 1
5653
+ WHERE t.tenant_id = ? AND t.workspace_id = ? AND t.is_archived = ?
5654
+ ORDER BY ${archived ? 't.completed_at DESC, t.created_at DESC' : 't.created_at DESC'}
5655
+ `).all(this.tenantId, normalizedWorkspaceId, archived ? 1 : 0, this.tenantId, normalizedWorkspaceId, archived ? 1 : 0);
5656
+ const comments = this.db.prepare(`
5657
+ WITH ranked_comments AS (
5658
+ SELECT c.task_id,
5659
+ c.id,
5660
+ c.text,
5661
+ c.author,
5662
+ c.timestamp,
5663
+ COUNT(*) OVER (PARTITION BY c.task_id) AS count,
5664
+ MAX(c.timestamp) OVER (PARTITION BY c.task_id) AS revision,
5665
+ ROW_NUMBER() OVER (
5666
+ PARTITION BY c.task_id
5667
+ ORDER BY c.timestamp DESC, c.id DESC
5668
+ ) AS comment_rank
5669
+ FROM comments c
5670
+ JOIN tasks t
5671
+ ON t.tenant_id = c.tenant_id
5672
+ AND t.workspace_id = c.workspace_id
5673
+ AND t.id = c.task_id
5674
+ WHERE c.tenant_id = ? AND c.workspace_id = ? AND t.is_archived = ?
5675
+ )
5676
+ SELECT task_id, id, text, author, timestamp, count, revision
5677
+ FROM ranked_comments
5678
+ WHERE comment_rank = 1
5679
+ `).all(this.tenantId, normalizedWorkspaceId, archived ? 1 : 0);
5680
+ const checklist = this.db.prepare(`
5681
+ SELECT c.task_id, COUNT(*) AS count,
5682
+ SUM(CASE WHEN c.is_completed = 1 THEN 1 ELSE 0 END) AS completed_count,
5683
+ MAX(COALESCE(c.updated_at, c.created_at)) AS revision
5684
+ FROM checklist_items c
5685
+ JOIN tasks t ON t.tenant_id = c.tenant_id AND t.workspace_id = c.workspace_id AND t.id = c.task_id
5686
+ WHERE c.tenant_id = ? AND c.workspace_id = ? AND t.is_archived = ?
5687
+ GROUP BY c.task_id
5688
+ `).all(this.tenantId, normalizedWorkspaceId, archived ? 1 : 0);
5689
+ const attachments = this.db.prepare(`
5690
+ SELECT l.task_id, COUNT(a.asset_id) AS count,
5691
+ MAX(CASE WHEN l.asset_id = t.card_cover_asset_id THEN l.asset_id ELSE NULL END) AS cover_asset_id,
5692
+ MAX(CASE WHEN l.asset_id = t.card_cover_asset_id THEN a.storage_provider ELSE NULL END) AS cover_storage_provider,
5693
+ MAX(CASE WHEN l.asset_id = t.card_cover_asset_id THEN a.storage_key ELSE NULL END) AS cover_storage_key,
5694
+ MAX(CASE WHEN l.asset_id = t.card_cover_asset_id THEN a.original_filename ELSE NULL END) AS cover_original_filename,
5695
+ MAX(CASE WHEN l.asset_id = t.card_cover_asset_id THEN COALESCE(l.display_name, a.logical_name) ELSE NULL END) AS cover_display_name,
5696
+ MAX(CASE
5697
+ WHEN COALESCE(a.updated_at, '') > COALESCE(l.updated_at, '') THEN a.updated_at
5698
+ ELSE l.updated_at
5699
+ END) AS revision
5700
+ FROM workspace_asset_links l
5701
+ JOIN tasks t ON t.tenant_id = l.tenant_id AND t.workspace_id = l.workspace_id AND t.id = l.task_id
5702
+ JOIN workspace_assets a ON a.tenant_id = l.tenant_id AND a.workspace_id = l.workspace_id AND a.asset_id = l.asset_id AND a.deleted_at IS NULL
5703
+ WHERE l.tenant_id = ? AND l.workspace_id = ? AND t.is_archived = ? AND l.target_type = 'task' AND l.deleted_at IS NULL
5704
+ GROUP BY l.task_id
5705
+ `).all(this.tenantId, normalizedWorkspaceId, archived ? 1 : 0);
5706
+ const relationships = this.db.prepare(`
5707
+ SELECT task_id, COUNT(*) AS count, MAX(updated_at) AS revision
5708
+ FROM (
5709
+ SELECT r.source_task_id AS task_id, r.updated_at
5710
+ FROM task_relationships r
5711
+ JOIN tasks t ON t.tenant_id = r.tenant_id AND t.workspace_id = r.workspace_id AND t.id = r.source_task_id
5712
+ WHERE r.tenant_id = ? AND r.workspace_id = ? AND r.deleted_at IS NULL AND t.is_archived = ?
5713
+ UNION ALL
5714
+ SELECT r.target_task_id AS task_id, r.updated_at
5715
+ FROM task_relationships r
5716
+ JOIN tasks t ON t.tenant_id = r.tenant_id AND t.workspace_id = r.workspace_id AND t.id = r.target_task_id
5717
+ WHERE r.tenant_id = ? AND r.workspace_id = ? AND r.deleted_at IS NULL
5718
+ AND r.target_task_id <> r.source_task_id AND t.is_archived = ?
5719
+ ) scoped_relationships
5720
+ GROUP BY task_id
5721
+ `).all(this.tenantId, normalizedWorkspaceId, archived ? 1 : 0, this.tenantId, normalizedWorkspaceId, archived ? 1 : 0);
5722
+ const countByTask = (entries) => new Map(entries.map((entry) => [String(entry.task_id || ''), Number(entry.count || 0)]));
5723
+ const commentCountByTask = countByTask(comments);
5724
+ const attachmentCountByTask = countByTask(attachments);
5725
+ const commentRevisionByTask = new Map(comments.map((entry) => [String(entry.task_id || ''), String(entry.revision || '')]));
5726
+ const attachmentRevisionByTask = new Map(attachments.map((entry) => [String(entry.task_id || ''), String(entry.revision || '')]));
5727
+ const coverAttachmentByTask = new Map(attachments.flatMap((entry) => {
5728
+ const taskId = String(entry.task_id || '');
5729
+ const assetId = String(entry.cover_asset_id || '').trim();
5730
+ const storageKey = String(entry.cover_storage_key || '').trim();
5731
+ const storageProvider = String(entry.cover_storage_provider || 'local').trim();
5732
+ if (!taskId || !assetId || !storageKey)
5733
+ return [];
5734
+ const isExternalHttpReference = storageProvider === 'external' && /^https?:\/\//i.test(storageKey);
5735
+ return [[taskId, {
5736
+ assetId,
5737
+ path: isExternalHttpReference
5738
+ ? storageKey
5739
+ : storageProvider === 'external'
5740
+ ? `/api/taskforce/context-link?path=${encodeURIComponent(storageKey)}`
5741
+ : `/api/taskforce/context/${encodeURIComponent(storageKey)}`,
5742
+ ...(isExternalHttpReference ? {} : { fsPath: storageKey }),
5743
+ displayName: entry.cover_display_name ? String(entry.cover_display_name) : undefined,
5744
+ originalFilename: entry.cover_original_filename ? String(entry.cover_original_filename) : undefined,
5745
+ linkRole: 'image',
5746
+ taskId,
5747
+ }]];
5748
+ }));
5749
+ const checklistByTask = new Map(checklist.map((entry) => [String(entry.task_id || ''), {
5750
+ total: Number(entry.count || 0),
5751
+ completed: Number(entry.completed_count || 0),
5752
+ revision: String(entry.revision || ''),
5753
+ }]));
5754
+ const relationshipCountByTask = countByTask(relationships);
5755
+ const relationshipRevisionByTask = new Map(relationships.map((entry) => [
5756
+ String(entry.task_id || ''),
5757
+ String(entry.revision || ''),
5758
+ ]));
5759
+ const latestActivityByTask = new Map();
5760
+ const latestActivityRevisionByTask = new Map();
5761
+ const considerLatestActivity = (taskId, item) => {
5762
+ const existing = latestActivityByTask.get(taskId);
5763
+ const nextTimestamp = Date.parse(String(item.timestamp || ''));
5764
+ const existingTimestamp = existing ? Date.parse(String(existing.timestamp || '')) : Number.NaN;
5765
+ const isNewer = !existing
5766
+ || (Number.isFinite(nextTimestamp) && !Number.isFinite(existingTimestamp))
5767
+ || (Number.isFinite(nextTimestamp) && Number.isFinite(existingTimestamp) && nextTimestamp > existingTimestamp)
5768
+ || (nextTimestamp === existingTimestamp && item.id.localeCompare(existing.id) > 0);
5769
+ if (!isNewer)
5770
+ return;
5771
+ latestActivityByTask.set(taskId, item);
5772
+ latestActivityRevisionByTask.set(taskId, `${item.timestamp}:${item.id}`);
5773
+ };
5774
+ for (const entry of comments) {
5775
+ const taskId = String(entry.task_id || '').trim();
5776
+ const commentId = String(entry.id || '').trim();
5777
+ const timestamp = String(entry.timestamp || '');
5778
+ if (!taskId || !commentId)
5779
+ continue;
5780
+ considerLatestActivity(taskId, {
5781
+ id: `comment:${commentId}`,
5782
+ type: 'comment',
5783
+ timestamp,
5784
+ comment: {
5785
+ text: String(entry.text || ''),
5786
+ author: String(entry.author || 'user').trim() || 'user',
5787
+ },
5788
+ });
5789
+ }
5790
+ for (const row of rows) {
5791
+ const taskId = String(row.id || '').trim();
5792
+ const eventId = String(row.latest_event_id || '').trim();
5793
+ const timestamp = String(row.latest_event_at || '');
5794
+ if (!taskId || !eventId)
5795
+ continue;
5796
+ let details = {};
5797
+ try {
5798
+ const parsed = row.latest_event_details_json ? JSON.parse(String(row.latest_event_details_json)) : {};
5799
+ details = parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
5800
+ }
5801
+ catch {
5802
+ details = {};
5803
+ }
5804
+ considerLatestActivity(taskId, {
5805
+ id: `event:${eventId}`,
5806
+ type: 'event',
5807
+ timestamp,
5808
+ event: {
5809
+ action: String(row.latest_event_action || ''),
5810
+ actor: String(row.latest_event_actor || 'system').trim() || 'system',
5811
+ actorType: row.latest_event_actor_type === 'human' || row.latest_event_actor_type === 'ai'
5812
+ ? row.latest_event_actor_type
5813
+ : 'system',
5814
+ details,
5815
+ },
5816
+ });
5817
+ }
5818
+ return rows.map((row) => {
5819
+ const id = String(row.id || '').trim();
5820
+ const scheduledDate = this.sanitizeDateOnly(row.scheduled_date);
5821
+ const dueDate = this.sanitizeDateOnly(row.due_date);
5822
+ const referenceNumber = this.sanitizeTaskReferenceNumber(row.reference_number);
5823
+ const localReferenceNumber = this.sanitizeTaskReferenceNumber(row.local_reference_number);
5824
+ let taxonomies = {};
5825
+ try {
5826
+ taxonomies = JSON.parse(row.taxonomies || '{}');
5827
+ }
5828
+ catch {
5829
+ taxonomies = {};
5830
+ }
5831
+ const checklistCounts = checklistByTask.get(id) || { total: 0, completed: 0, revision: '' };
5832
+ const commentCount = commentCountByTask.get(id) || 0;
5833
+ const attachmentCount = attachmentCountByTask.get(id) || 0;
5834
+ const relationshipCount = relationshipCountByTask.get(id) || 0;
5835
+ const detailRevision = JSON.stringify([
5836
+ String(row.updated_at || row.created_at || ''),
5837
+ commentCount,
5838
+ commentRevisionByTask.get(id) || '',
5839
+ checklistCounts.total,
5840
+ checklistCounts.completed,
5841
+ checklistCounts.revision,
5842
+ attachmentCount,
5843
+ attachmentRevisionByTask.get(id) || '',
5844
+ relationshipCount,
5845
+ relationshipRevisionByTask.get(id) || '',
5846
+ latestActivityRevisionByTask.get(id) || '',
5847
+ ]);
5848
+ return {
5849
+ id,
5850
+ referenceNumber,
5851
+ localReferenceNumber,
5852
+ referenceLabel: deriveTaskReferenceLabel({ referenceNumber, localReferenceNumber }),
5853
+ title: String(row.title || ''),
5854
+ status: this.normalizeStatusValue(row.status),
5855
+ priority: this.normalizePriorityValue(row.priority),
5856
+ complexity: Number(row.complexity || 3),
5857
+ type: String(row.type || 'task'),
5858
+ category: String(row.category || 'default'),
5859
+ createdAt: String(row.created_at || ''),
5860
+ updatedAt: row.updated_at ? String(row.updated_at) : undefined,
5861
+ completedAt: row.completed_at ? String(row.completed_at) : null,
5862
+ canceledReason: row.canceled_reason ? String(row.canceled_reason) : null,
5863
+ isArchived: row.is_archived === 1 || row.is_archived === true,
5864
+ scheduledDate,
5865
+ dueDate,
5866
+ scheduledWeekKey: row.scheduled_week_key || (scheduledDate ? this.dateOnlyToIsoWeekKey(scheduledDate) : null),
5867
+ orderInDay: this.sanitizeOrderInDay(row.order_in_day),
5868
+ workstreamOrder: this.sanitizeOrderInDay(row.workstream_order),
5869
+ workstreamId: this.sanitizeWorkstreamId(row.workstream_id),
5870
+ cardCoverAssetId: typeof row.card_cover_asset_id === 'string' && row.card_cover_asset_id.trim()
5871
+ ? row.card_cover_asset_id.trim()
5872
+ : null,
5873
+ cardCoverAttachment: coverAttachmentByTask.get(id) || null,
5874
+ taxonomies,
5875
+ createdBy: this.normalizeCreatedBy(row.created_by, normalizedWorkspaceId, { preserveGenericAi: true }),
5876
+ assignee: this.sanitizeAssignee(row.assignee, normalizedWorkspaceId),
5877
+ isSummary: true,
5878
+ commentCount,
5879
+ checklistCount: checklistCounts.total,
5880
+ completedChecklistCount: checklistCounts.completed,
5881
+ attachmentCount,
5882
+ relationshipCount,
5883
+ latestActivity: latestActivityByTask.get(id) || null,
5884
+ detailRevision,
5885
+ };
5886
+ });
5887
+ }
5888
+ listActiveTaskSummaries(workspaceId) {
5889
+ return { tasks: this.listTaskSummaries(workspaceId, false) };
5890
+ }
5891
+ listArchiveSummaries(workspaceId) {
5892
+ return { archived: this.listTaskSummaries(workspaceId, true) };
5893
+ }
5894
+ getTaskDetailRevision(id, workspaceId) {
5895
+ const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
5896
+ const taskId = String(id || '').trim();
5897
+ if (!taskId)
5898
+ return null;
5899
+ const row = this.db.prepare(`
5900
+ WITH target_task AS (
5901
+ SELECT id, created_at, updated_at
5902
+ FROM tasks
5903
+ WHERE tenant_id = ? AND workspace_id = ? AND id = ?
5904
+ ),
5905
+ comment_stats AS (
5906
+ SELECT COUNT(*) AS count, MAX(c.timestamp) AS revision
5907
+ FROM comments c
5908
+ JOIN target_task t ON t.id = c.task_id
5909
+ WHERE c.tenant_id = ? AND c.workspace_id = ?
5910
+ ),
5911
+ latest_comment AS (
5912
+ SELECT c.id, c.timestamp
5913
+ FROM comments c
5914
+ JOIN target_task t ON t.id = c.task_id
5915
+ WHERE c.tenant_id = ? AND c.workspace_id = ?
5916
+ ORDER BY c.timestamp DESC, c.id DESC
5917
+ LIMIT 1
5918
+ ),
5919
+ checklist_stats AS (
5920
+ SELECT COUNT(*) AS count,
5921
+ SUM(CASE WHEN c.is_completed = 1 THEN 1 ELSE 0 END) AS completed_count,
5922
+ MAX(COALESCE(c.updated_at, c.created_at)) AS revision
5923
+ FROM checklist_items c
5924
+ JOIN target_task t ON t.id = c.task_id
5925
+ WHERE c.tenant_id = ? AND c.workspace_id = ?
5926
+ ),
5927
+ attachment_stats AS (
5928
+ SELECT COUNT(a.asset_id) AS count,
5929
+ MAX(CASE
5930
+ WHEN COALESCE(a.updated_at, '') > COALESCE(l.updated_at, '') THEN a.updated_at
5931
+ ELSE l.updated_at
5932
+ END) AS revision
5933
+ FROM workspace_asset_links l
5934
+ JOIN target_task t ON t.id = l.task_id
5935
+ JOIN workspace_assets a
5936
+ ON a.tenant_id = l.tenant_id
5937
+ AND a.workspace_id = l.workspace_id
5938
+ AND a.asset_id = l.asset_id
5939
+ AND a.deleted_at IS NULL
5940
+ WHERE l.tenant_id = ? AND l.workspace_id = ?
5941
+ AND l.target_type = 'task' AND l.deleted_at IS NULL
5942
+ ),
5943
+ relationship_rows AS (
5944
+ SELECT r.updated_at
5945
+ FROM task_relationships r
5946
+ JOIN target_task t ON t.id = r.source_task_id
5947
+ WHERE r.tenant_id = ? AND r.workspace_id = ? AND r.deleted_at IS NULL
5948
+ UNION ALL
5949
+ SELECT r.updated_at
5950
+ FROM task_relationships r
5951
+ JOIN target_task t ON t.id = r.target_task_id
5952
+ WHERE r.tenant_id = ? AND r.workspace_id = ? AND r.deleted_at IS NULL
5953
+ AND r.target_task_id <> r.source_task_id
5954
+ ),
5955
+ relationship_stats AS (
5956
+ SELECT COUNT(*) AS count, MAX(updated_at) AS revision
5957
+ FROM relationship_rows
5958
+ ),
5959
+ latest_event AS (
5960
+ SELECT e.id, e.created_at
5961
+ FROM entity_events e
5962
+ JOIN target_task t ON t.id = e.entity_id
5963
+ WHERE e.tenant_id = ? AND e.workspace_id = ?
5964
+ AND e.entity_type = 'task'
5965
+ AND e.action <> 'task-comment-added'
5966
+ AND NOT (
5967
+ e.action = 'task-updated'
5968
+ AND REPLACE(COALESCE(e.details_json, ''), ' ', '') LIKE '%"saveSource":"autosave"%'
5969
+ AND REPLACE(COALESCE(e.details_json, ''), ' ', '') LIKE '%"changes":{}%'
5970
+ )
5971
+ ORDER BY e.created_at DESC, e.id DESC
5972
+ LIMIT 1
5973
+ )
5974
+ SELECT t.created_at, t.updated_at,
5975
+ comments.count AS comment_count, comments.revision AS comment_revision,
5976
+ latest_comment.id AS latest_comment_id,
5977
+ latest_comment.timestamp AS latest_comment_at,
5978
+ checklist.count AS checklist_count,
5979
+ checklist.completed_count AS completed_checklist_count,
5980
+ checklist.revision AS checklist_revision,
5981
+ attachments.count AS attachment_count,
5982
+ attachments.revision AS attachment_revision,
5983
+ relationships.count AS relationship_count,
5984
+ relationships.revision AS relationship_revision,
5985
+ latest_event.id AS latest_event_id,
5986
+ latest_event.created_at AS latest_event_at
5987
+ FROM target_task t
5988
+ CROSS JOIN comment_stats comments
5989
+ CROSS JOIN checklist_stats checklist
5990
+ CROSS JOIN attachment_stats attachments
5991
+ CROSS JOIN relationship_stats relationships
5992
+ LEFT JOIN latest_comment ON 1 = 1
5993
+ LEFT JOIN latest_event ON 1 = 1
5994
+ `).get(this.tenantId, normalizedWorkspaceId, taskId, this.tenantId, normalizedWorkspaceId, this.tenantId, normalizedWorkspaceId, this.tenantId, normalizedWorkspaceId, this.tenantId, normalizedWorkspaceId, this.tenantId, normalizedWorkspaceId, this.tenantId, normalizedWorkspaceId, this.tenantId, normalizedWorkspaceId);
5995
+ if (!row)
5996
+ return null;
5997
+ const latestCommentId = String(row.latest_comment_id || '').trim();
5998
+ const latestCommentAt = String(row.latest_comment_at || '');
5999
+ const latestEventId = String(row.latest_event_id || '').trim();
6000
+ const latestEventAt = String(row.latest_event_at || '');
6001
+ const commentTimestamp = Date.parse(latestCommentAt);
6002
+ const eventTimestamp = Date.parse(latestEventAt);
6003
+ const commentActivityId = latestCommentId ? `comment:${latestCommentId}` : '';
6004
+ const eventActivityId = latestEventId ? `event:${latestEventId}` : '';
6005
+ const latestActivityRevision = !latestEventId
6006
+ || (latestCommentId && ((Number.isFinite(commentTimestamp) && !Number.isFinite(eventTimestamp))
6007
+ || commentTimestamp > eventTimestamp
6008
+ || (commentTimestamp === eventTimestamp && commentActivityId.localeCompare(eventActivityId) > 0)))
6009
+ ? (latestCommentId ? `${latestCommentAt}:${commentActivityId}` : '')
6010
+ : `${latestEventAt}:${eventActivityId}`;
6011
+ return JSON.stringify([
6012
+ String(row.updated_at || row.created_at || ''),
6013
+ Number(row.comment_count || 0),
6014
+ String(row.comment_revision || ''),
6015
+ Number(row.checklist_count || 0),
6016
+ Number(row.completed_checklist_count || 0),
6017
+ String(row.checklist_revision || ''),
6018
+ Number(row.attachment_count || 0),
6019
+ String(row.attachment_revision || ''),
6020
+ Number(row.relationship_count || 0),
6021
+ String(row.relationship_revision || ''),
6022
+ latestActivityRevision,
6023
+ ]);
6024
+ }
6025
+ getTaskDetailSnapshot(id, workspaceId) {
6026
+ const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
6027
+ return this.db.transaction(() => {
6028
+ if (this.db.provider === 'postgres') {
6029
+ this.db.exec('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ');
6030
+ }
6031
+ const task = this.getTask(id, normalizedWorkspaceId);
6032
+ if (!task)
6033
+ return null;
6034
+ const detailRevision = this.getTaskDetailRevision(id, normalizedWorkspaceId);
6035
+ return detailRevision ? { ...task, detailRevision } : null;
6036
+ })();
6037
+ }
6038
+ updateTaskIfDetailRevisionMatches(id, expectedDetailRevision, updates, workspaceId, options) {
6039
+ const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
6040
+ const updated = this.db.transaction(() => {
6041
+ if (this.db.provider === 'postgres') {
6042
+ this.db.exec('SET TRANSACTION ISOLATION LEVEL SERIALIZABLE');
6043
+ }
6044
+ const currentDetailRevision = this.getTaskDetailRevision(id, normalizedWorkspaceId);
6045
+ if (currentDetailRevision === null)
6046
+ return null;
6047
+ if (currentDetailRevision !== expectedDetailRevision) {
6048
+ throw new TaskforceRuleError('Task changed after this editor was loaded. Reload the latest task before saving.', 409, 'TASK_DETAIL_REVISION_CONFLICT', { currentDetailRevision });
6049
+ }
6050
+ const task = this.updateTask(id, updates, normalizedWorkspaceId, {
6051
+ actorRef: options?.actorRef,
6052
+ saveSource: options?.saveSource,
6053
+ persistBackup: false,
6054
+ emitRealtime: false,
6055
+ });
6056
+ if (!task)
6057
+ return null;
6058
+ const detailRevision = this.getTaskDetailRevision(id, normalizedWorkspaceId);
6059
+ return detailRevision ? { ...task, detailRevision } : null;
6060
+ })();
6061
+ if (updated) {
6062
+ this.persistBackup();
6063
+ this.emitRealtimeMutation({
6064
+ workspaceId: normalizedWorkspaceId,
6065
+ entityType: 'task',
6066
+ changeType: 'upsert',
6067
+ entityIds: [updated.id],
6068
+ });
6069
+ }
6070
+ return updated;
6071
+ }
6072
+ assertTaskDetailRevisionForUpdate(id, expectedDetailRevision, workspaceId) {
6073
+ const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
6074
+ const lockSql = this.db.provider === 'postgres'
6075
+ ? `SELECT id FROM tasks WHERE tenant_id = ? AND workspace_id = ? AND id = ? FOR UPDATE`
6076
+ : `SELECT id FROM tasks WHERE tenant_id = ? AND workspace_id = ? AND id = ?`;
6077
+ const task = this.db.prepare(lockSql).get(this.tenantId, normalizedWorkspaceId, id);
6078
+ if (!task)
6079
+ throw new TaskforceRuleError('Task not found.', 404, 'TASK_NOT_FOUND');
6080
+ const currentDetailRevision = this.getTaskDetailRevision(id, normalizedWorkspaceId);
6081
+ if (currentDetailRevision !== expectedDetailRevision) {
6082
+ throw new TaskforceRuleError('Task changed after this editor was loaded. Reload the latest task before saving.', 409, 'TASK_DETAIL_REVISION_CONFLICT', { currentDetailRevision });
6083
+ }
6084
+ }
5351
6085
  /**
5352
6086
  * Filter-pushdown listing used by read-only surfaces that don't need the
5353
6087
  * full task graph (MCP list_tasks, CLI summaries). Avoids the per-task
@@ -5360,9 +6094,11 @@ export class TaskforceCore {
5360
6094
  const clauses = [
5361
6095
  'tenant_id = ?',
5362
6096
  'workspace_id = ?',
5363
- 'is_archived = 0',
5364
6097
  ];
5365
6098
  const params = [this.tenantId, normalizedWorkspaceId];
6099
+ if (!filters.includeArchived) {
6100
+ clauses.push('is_archived = 0');
6101
+ }
5366
6102
  if (filters.category) {
5367
6103
  clauses.push('category = ?');
5368
6104
  params.push(filters.category);
@@ -5467,6 +6203,127 @@ export class TaskforceCore {
5467
6203
  };
5468
6204
  });
5469
6205
  }
6206
+ listTaskCountsByWorkstream(workspaceId, workstreamId) {
6207
+ const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
6208
+ const clauses = [
6209
+ 'tenant_id = ?',
6210
+ 'workspace_id = ?',
6211
+ 'workstream_id IS NOT NULL',
6212
+ "TRIM(workstream_id) <> ''",
6213
+ ];
6214
+ const params = [this.tenantId, normalizedWorkspaceId];
6215
+ const normalizedWorkstreamId = this.sanitizeWorkstreamId(workstreamId);
6216
+ if (normalizedWorkstreamId) {
6217
+ clauses.push('workstream_id = ?');
6218
+ params.push(normalizedWorkstreamId);
6219
+ }
6220
+ const rows = this.db.prepare(`
6221
+ SELECT
6222
+ workstream_id,
6223
+ COUNT(*) AS total,
6224
+ SUM(CASE WHEN is_archived = 0 THEN 1 ELSE 0 END) AS active,
6225
+ SUM(CASE WHEN is_archived = 1 THEN 1 ELSE 0 END) AS archived
6226
+ FROM tasks
6227
+ WHERE ${clauses.join(' AND ')}
6228
+ GROUP BY workstream_id
6229
+ `).all(...params);
6230
+ return rows.map((row) => ({
6231
+ workstreamId: String(row.workstream_id || '').trim(),
6232
+ total: Number(row.total || 0),
6233
+ active: Number(row.active || 0),
6234
+ archived: Number(row.archived || 0),
6235
+ }));
6236
+ }
6237
+ listTaskReferenceSummaries(taskIds, workspaceId) {
6238
+ const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
6239
+ const normalizedIds = Array.from(new Set(taskIds.map((id) => String(id || '').trim()).filter(Boolean)));
6240
+ if (normalizedIds.length === 0)
6241
+ return [];
6242
+ const placeholders = normalizedIds.map(() => '?').join(', ');
6243
+ const rows = this.db.prepare(`
6244
+ SELECT id, reference_number, local_reference_number, title, status
6245
+ FROM tasks
6246
+ WHERE tenant_id = ?
6247
+ AND workspace_id = ?
6248
+ AND id IN (${placeholders})
6249
+ `).all(this.tenantId, normalizedWorkspaceId, ...normalizedIds);
6250
+ return rows.map((row) => {
6251
+ const referenceNumber = this.sanitizeTaskReferenceNumber(row.reference_number);
6252
+ const localReferenceNumber = this.sanitizeTaskReferenceNumber(row.local_reference_number);
6253
+ return {
6254
+ id: String(row.id || '').trim(),
6255
+ referenceNumber,
6256
+ localReferenceNumber,
6257
+ referenceLabel: deriveTaskReferenceLabel({ referenceNumber, localReferenceNumber }),
6258
+ title: String(row.title || ''),
6259
+ status: this.normalizeStatusValue(row.status),
6260
+ };
6261
+ });
6262
+ }
6263
+ searchTaskSummaries(query, scope = 'all', workspaceId, options) {
6264
+ const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
6265
+ const normalizedQuery = normalizeTaskSearchQuery(String(query || ''));
6266
+ if (!normalizedQuery.raw)
6267
+ return [];
6268
+ const exactReferenceNumber = parseTaskReference(normalizedQuery.raw);
6269
+ const exactLocalReferenceNumber = parseLocalTaskReference(normalizedQuery.raw);
6270
+ const searchableText = `
6271
+ COALESCE(title, '') || ' ' ||
6272
+ COALESCE(description, '') || ' ' ||
6273
+ COALESCE(id, '') || ' ' ||
6274
+ CASE WHEN reference_number IS NULL THEN '' ELSE 'T-' || CAST(reference_number AS TEXT) END || ' ' ||
6275
+ CASE WHEN local_reference_number IS NULL THEN '' ELSE 'LT-' || CAST(local_reference_number AS TEXT) END || ' ' ||
6276
+ COALESCE(category, '') || ' ' ||
6277
+ COALESCE(type, '') || ' ' ||
6278
+ COALESCE(CAST(priority AS TEXT), '') || ' ' ||
6279
+ COALESCE(status, '') || ' ' ||
6280
+ COALESCE(assignee, '') || ' ' ||
6281
+ COALESCE(canceled_reason, '')
6282
+ `;
6283
+ const escapedTerms = normalizedQuery.tokens.map((token) => (`%${token.replace(/[\\%_]/g, '\\$&')}%`));
6284
+ const clauses = [
6285
+ 'tenant_id = ?',
6286
+ 'workspace_id = ?',
6287
+ ...escapedTerms.map(() => `LOWER(${searchableText}) LIKE ? ESCAPE '\\'`),
6288
+ ];
6289
+ const params = [
6290
+ this.tenantId,
6291
+ normalizedWorkspaceId,
6292
+ ...escapedTerms,
6293
+ ];
6294
+ if (scope === 'active')
6295
+ clauses.push('is_archived = 0');
6296
+ if (scope === 'archive')
6297
+ clauses.push('is_archived = 1');
6298
+ const limit = Math.max(1, Math.min(51, Math.trunc(options?.limit ?? 50)));
6299
+ const offset = Math.max(0, Math.trunc(options?.offset ?? 0));
6300
+ const rows = this.db.prepare(`
6301
+ SELECT id, reference_number, local_reference_number, title, status, assignee, category, priority, is_archived
6302
+ FROM tasks
6303
+ WHERE ${clauses.join(' AND ')}
6304
+ ORDER BY
6305
+ CASE WHEN id = ? OR reference_number = ? OR local_reference_number = ? THEN 0 ELSE 1 END,
6306
+ created_at DESC,
6307
+ id DESC
6308
+ LIMIT ? OFFSET ?
6309
+ `).all(...params, normalizedQuery.raw, exactReferenceNumber || -1, exactLocalReferenceNumber || -1, limit, offset);
6310
+ return rows.map((row) => {
6311
+ const referenceNumber = this.sanitizeTaskReferenceNumber(row.reference_number);
6312
+ const localReferenceNumber = this.sanitizeTaskReferenceNumber(row.local_reference_number);
6313
+ return {
6314
+ id: String(row.id || '').trim(),
6315
+ referenceNumber,
6316
+ localReferenceNumber,
6317
+ referenceLabel: deriveTaskReferenceLabel({ referenceNumber, localReferenceNumber }),
6318
+ title: String(row.title || ''),
6319
+ status: this.normalizeStatusValue(row.status),
6320
+ assignee: String(row.assignee || '').trim() || null,
6321
+ category: String(row.category || ''),
6322
+ priority: this.normalizePriorityValue(row.priority),
6323
+ isArchived: row.is_archived === 1 || row.is_archived === true,
6324
+ };
6325
+ });
6326
+ }
5470
6327
  getDataVersion() {
5471
6328
  if (this.db.provider === 'sqlite') {
5472
6329
  const version = this.db.pragma('data_version', { simple: true });
@@ -5511,6 +6368,9 @@ export class TaskforceCore {
5511
6368
  listDeletedTasks(workspaceId) {
5512
6369
  return listDeletedTasksService(this.getDeletedTaskLifecycleServiceDeps(), workspaceId);
5513
6370
  }
6371
+ listDeletedTasksReadOnly(workspaceId) {
6372
+ return listDeletedTasksReadOnlyService(this.getDeletedTaskLifecycleServiceDeps(), workspaceId);
6373
+ }
5514
6374
  getDeletedTask(id, workspaceId) {
5515
6375
  return getDeletedTaskService(this.getDeletedTaskLifecycleServiceDeps(), id, workspaceId);
5516
6376
  }
@@ -5594,11 +6454,13 @@ export class TaskforceCore {
5594
6454
  const deletedStatsSql = this.db.provider === 'postgres'
5595
6455
  ? `SELECT count(*) as total,
5596
6456
  sum(case when task_snapshot_json::jsonb ->> 'status' = 'done' then 1 else 0 end) as done_count,
5597
- sum(case when task_snapshot_json::jsonb ->> 'status' = 'cancelled' then 1 else 0 end) as cancelled_count
6457
+ sum(case when task_snapshot_json::jsonb ->> 'status' = 'cancelled' then 1 else 0 end) as cancelled_count,
6458
+ sum(case when coalesce(task_snapshot_json::jsonb ->> 'status', '') not in ('done', 'cancelled') then 1 else 0 end) as other_count
5598
6459
  FROM deleted_tasks WHERE tenant_id = ? AND workspace_id = ?`
5599
6460
  : `SELECT count(*) as total,
5600
6461
  sum(case when json_extract(task_snapshot_json, '$.status') = 'done' then 1 else 0 end) as done_count,
5601
- sum(case when json_extract(task_snapshot_json, '$.status') = 'cancelled' then 1 else 0 end) as cancelled_count
6462
+ sum(case when json_extract(task_snapshot_json, '$.status') = 'cancelled' then 1 else 0 end) as cancelled_count,
6463
+ sum(case when coalesce(json_extract(task_snapshot_json, '$.status'), '') not in ('done', 'cancelled') then 1 else 0 end) as other_count
5602
6464
  FROM deleted_tasks WHERE tenant_id = ? AND workspace_id = ?`;
5603
6465
  const deletedStats = this.db.prepare(deletedStatsSql).get(this.tenantId, normalizedWorkspaceId);
5604
6466
  return {
@@ -5612,6 +6474,7 @@ export class TaskforceCore {
5612
6474
  total: Number(deletedStats.total || 0),
5613
6475
  doneCount: Number(deletedStats.done_count || 0),
5614
6476
  cancelledCount: Number(deletedStats.cancelled_count || 0),
6477
+ otherCount: Number(deletedStats.other_count || 0),
5615
6478
  retentionDays: DELETED_TASK_RETENTION_DAYS
5616
6479
  }
5617
6480
  };
@@ -6889,6 +7752,20 @@ export class TaskforceCore {
6889
7752
  runTaskMetadataOwnershipTransaction: (input) => this.runTaskMetadataOwnershipTransaction(input),
6890
7753
  };
6891
7754
  }
7755
+ buildUnresolvedWorkspaceActor(raw) {
7756
+ const normalized = raw.toLowerCase();
7757
+ const unresolvedKind = (normalized.startsWith('ai-profile-') || this.isKnownAiModelName(normalized)) ? 'ai' : 'human';
7758
+ return {
7759
+ id: raw,
7760
+ kind: unresolvedKind,
7761
+ label: this.formatActorLabel(raw),
7762
+ username: this.normalizeAiProfileUsername(raw, raw),
7763
+ icon: unresolvedKind === 'ai' ? GENERIC_AI_ACTOR_ICON : 'User',
7764
+ color: unresolvedKind === 'ai' ? GENERIC_AI_ACTOR_COLOR : '#22c55e',
7765
+ isNamed: true,
7766
+ meta: { unresolved: true }
7767
+ };
7768
+ }
6892
7769
  resolveWorkspaceActor(workspaceId, actorRef) {
6893
7770
  const raw = String(actorRef || '').trim();
6894
7771
  const normalized = raw.toLowerCase();
@@ -6918,17 +7795,81 @@ export class TaskforceCore {
6918
7795
  if (workspaceUser) {
6919
7796
  return this.workspaceUserToActor(workspaceUser);
6920
7797
  }
6921
- const unresolvedKind = (normalized.startsWith('ai-profile-') || this.isKnownAiModelName(normalized)) ? 'ai' : 'human';
6922
- return {
6923
- id: raw,
6924
- kind: unresolvedKind,
6925
- label: this.formatActorLabel(raw),
6926
- username: this.normalizeAiProfileUsername(raw, raw),
6927
- icon: unresolvedKind === 'ai' ? GENERIC_AI_ACTOR_ICON : 'User',
6928
- color: unresolvedKind === 'ai' ? GENERIC_AI_ACTOR_COLOR : '#22c55e',
6929
- isNamed: true,
6930
- meta: { unresolved: true }
6931
- };
7798
+ return this.buildUnresolvedWorkspaceActor(raw);
7799
+ }
7800
+ resolveWorkspaceActors(workspaceId, actorRefs) {
7801
+ const rawActorRefs = [...new Set(actorRefs
7802
+ .map((actorRef) => String(actorRef || '').trim())
7803
+ .filter(Boolean))];
7804
+ const resolvedActors = new Map();
7805
+ const lookupActorRefs = [];
7806
+ for (const raw of rawActorRefs) {
7807
+ const normalized = raw.toLowerCase();
7808
+ if (normalized === 'unassigned' || normalized === 'none' || normalized === 'null') {
7809
+ resolvedActors.set(raw, null);
7810
+ }
7811
+ else if (this.isHumanActorReference(raw)) {
7812
+ resolvedActors.set(raw, this.buildHumanActorFallback());
7813
+ }
7814
+ else if (this.isGenericAiActorReference(raw)) {
7815
+ resolvedActors.set(raw, this.buildGenericAiActor());
7816
+ }
7817
+ else {
7818
+ lookupActorRefs.push(raw);
7819
+ }
7820
+ }
7821
+ if (lookupActorRefs.length === 0)
7822
+ return resolvedActors;
7823
+ const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
7824
+ const aiProfileRows = this.db.prepare(`
7825
+ SELECT *
7826
+ FROM ai_profiles
7827
+ WHERE tenant_id = ?
7828
+ AND workspace_id = ?
7829
+ ORDER BY COALESCE(last_active_at, updated_at, created_at) DESC, id ASC
7830
+ `).all(this.tenantId, normalizedWorkspaceId);
7831
+ const aiProfileRowsById = new Map(aiProfileRows
7832
+ .map((row) => [String(row.id || '').trim(), row])
7833
+ .filter(([id]) => Boolean(id)));
7834
+ const aiProfileRowsByAlias = new Map();
7835
+ for (const row of aiProfileRows) {
7836
+ const aliases = new Set([String(row.name || '').trim().toLowerCase(), String(row.username || '').trim().toLowerCase()]);
7837
+ for (const alias of aliases) {
7838
+ if (!alias)
7839
+ continue;
7840
+ const matches = aiProfileRowsByAlias.get(alias) ?? [];
7841
+ matches.push(row);
7842
+ aiProfileRowsByAlias.set(alias, matches);
7843
+ }
7844
+ }
7845
+ const workspaceUsersById = new Map(this.listWorkspaceUsers(normalizedWorkspaceId)
7846
+ .map((user) => [user.userId, user]));
7847
+ for (const raw of lookupActorRefs) {
7848
+ const directAiProfileRow = aiProfileRowsById.get(raw);
7849
+ if (directAiProfileRow) {
7850
+ resolvedActors.set(raw, this.aiProfileToActor(this.aiProfileRowToRecord(directAiProfileRow)));
7851
+ continue;
7852
+ }
7853
+ const aliasMatches = aiProfileRowsByAlias.get(raw.toLowerCase()) ?? [];
7854
+ const aliasRowsById = new Map(aliasMatches
7855
+ .map((row) => [String(row.id || '').trim(), row])
7856
+ .filter(([id]) => Boolean(id)));
7857
+ const canonicalAliasIds = [...new Set(aliasMatches
7858
+ .map((row) => this.resolveCanonicalAiProfileId(aliasRowsById, String(row.id || '').trim()))
7859
+ .filter(Boolean))];
7860
+ if (canonicalAliasIds.length === 1) {
7861
+ const canonicalAiProfileRow = aiProfileRowsById.get(canonicalAliasIds[0]);
7862
+ if (canonicalAiProfileRow) {
7863
+ resolvedActors.set(raw, this.aiProfileToActor(this.aiProfileRowToRecord(canonicalAiProfileRow)));
7864
+ continue;
7865
+ }
7866
+ }
7867
+ const workspaceUser = workspaceUsersById.get(raw);
7868
+ resolvedActors.set(raw, workspaceUser
7869
+ ? this.workspaceUserToActor(workspaceUser)
7870
+ : this.buildUnresolvedWorkspaceActor(raw));
7871
+ }
7872
+ return resolvedActors;
6932
7873
  }
6933
7874
  listAssignableActors(workspaceId) {
6934
7875
  const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
@@ -7835,6 +8776,9 @@ export class TaskforceCore {
7835
8776
  bindMcpOAuthConnectorGrantAiProfile(input) {
7836
8777
  return this.mcpTokenService.bindMcpOAuthConnectorGrantAiProfile(input);
7837
8778
  }
8779
+ listMcpOAuthRestorableAiProfileIds(input) {
8780
+ return this.mcpTokenService.listMcpOAuthRestorableAiProfileIds(input);
8781
+ }
7838
8782
  recordMcpOAuthConnectorGrantUsage(input) {
7839
8783
  return this.mcpTokenService.recordMcpOAuthConnectorGrantUsage(input);
7840
8784
  }
@@ -8566,6 +9510,12 @@ export class TaskforceCore {
8566
9510
  return inferredAssignee || 'unassigned';
8567
9511
  })();
8568
9512
  const normalizedAttachments = normalizedInput.attachments ?? [];
9513
+ const requestedCardCoverAssetId = typeof normalizedInput.cardCoverAssetId === 'string'
9514
+ ? normalizedInput.cardCoverAssetId.trim()
9515
+ : null;
9516
+ if (requestedCardCoverAssetId && !normalizedAttachments.some((item) => (typeof item !== 'string' && item.assetId === requestedCardCoverAssetId && item.linkRole === 'image'))) {
9517
+ throw new TaskforceRuleError('Task card cover must reference an image already attached to this task.', 400, 'TASK_CARD_COVER_INVALID');
9518
+ }
8569
9519
  const normalizedComments = this.normalizeIncomingComments(normalizedInput.comments, normalizedWorkspaceId);
8570
9520
  const normalizedDescription = this.normalizeGeneratedDescription(normalizedInput.description, createdBySignal);
8571
9521
  const workstreamId = this.sanitizeWorkstreamId(normalizedInput.workstreamId);
@@ -8598,6 +9548,7 @@ export class TaskforceCore {
8598
9548
  ? this.nextWorkstreamTaskOrder(workstreamId, normalizedWorkspaceId)
8599
9549
  : null,
8600
9550
  workstreamId,
9551
+ cardCoverAssetId: requestedCardCoverAssetId,
8601
9552
  checklistItems: Array.isArray(normalizedInput.checklistItems)
8602
9553
  ? normalizedInput.checklistItems.map((item, index) => ({
8603
9554
  id: String(item?.id || this.generateEntityId('checklist')),
@@ -8628,7 +9579,7 @@ export class TaskforceCore {
8628
9579
  local_reference_number,
8629
9580
  created_at, updated_at, is_archived, taxonomies, created_by, assignee,
8630
9581
  scheduled_date, due_date, scheduled_week_key, order_in_day, workstream_order,
8631
- workstream_id
9582
+ workstream_id, card_cover_asset_id
8632
9583
  ) VALUES (
8633
9584
  @id, @tenantId, @title, @description, @status, @priority, @complexity, @type, @category,
8634
9585
  @workspaceId,
@@ -8636,7 +9587,7 @@ export class TaskforceCore {
8636
9587
  @localReferenceNumber,
8637
9588
  @createdAt, @updatedAt, 0, @taxonomies, @createdBy, @assignee,
8638
9589
  @scheduledDate, @dueDate, @scheduledWeekKey, @orderInDay, @workstreamOrder,
8639
- @workstreamId
9590
+ @workstreamId, @cardCoverAssetId
8640
9591
  )
8641
9592
  `);
8642
9593
  stmt.run({
@@ -9366,9 +10317,11 @@ export class TaskforceCore {
9366
10317
  return null;
9367
10318
  const patchFields = Object.keys(patch);
9368
10319
  const archivedNestedContentWrite = options?.allowArchivedNestedContentSyncWrite === true
9369
- && patchFields.some((field) => ['comments', 'attachments', 'checklistItems'].includes(field))
10320
+ && patchFields.some((field) => [
10321
+ 'comments', 'attachments', 'checklistItems', 'cardCoverAssetId',
10322
+ ].includes(field))
9370
10323
  && patchFields.every((field) => [
9371
- 'id', 'comments', 'attachments', 'checklistItems',
10324
+ 'id', 'comments', 'attachments', 'checklistItems', 'cardCoverAssetId',
9372
10325
  ].includes(field));
9373
10326
  if (existing.isArchived && !archivedNestedContentWrite) {
9374
10327
  throw new TaskforceRuleError('Archived tasks are read-only. Unarchive first to make changes.', 409, 'TASK_ARCHIVED_READ_ONLY');
@@ -9422,6 +10375,23 @@ export class TaskforceCore {
9422
10375
  if (Object.prototype.hasOwnProperty.call(patch, 'workstreamOrder')) {
9423
10376
  patch.workstreamOrder = this.sanitizeOrderInDay(patch.workstreamOrder);
9424
10377
  }
10378
+ if (Object.prototype.hasOwnProperty.call(patch, 'cardCoverAssetId')) {
10379
+ const requestedCoverAssetId = typeof patch.cardCoverAssetId === 'string'
10380
+ ? patch.cardCoverAssetId.trim()
10381
+ : null;
10382
+ if (requestedCoverAssetId) {
10383
+ const attachment = (patch.attachments || existing.attachments || []).find((item) => (typeof item !== 'string' && item.assetId === requestedCoverAssetId && item.linkRole === 'image'));
10384
+ if (!attachment) {
10385
+ throw new TaskforceRuleError('Task card cover must reference an image already attached to this task.', 400, 'TASK_CARD_COVER_INVALID');
10386
+ }
10387
+ }
10388
+ patch.cardCoverAssetId = requestedCoverAssetId;
10389
+ }
10390
+ else if (Object.prototype.hasOwnProperty.call(patch, 'attachments') && existing.cardCoverAssetId) {
10391
+ const coverStillAttached = (patch.attachments || []).some((item) => (typeof item !== 'string' && item.assetId === existing.cardCoverAssetId && item.linkRole === 'image'));
10392
+ if (!coverStillAttached)
10393
+ patch.cardCoverAssetId = null;
10394
+ }
9425
10395
  const hasScheduledDatePatch = Object.prototype.hasOwnProperty.call(patch, 'scheduledDate');
9426
10396
  const hasScheduledWeekKeyPatch = Object.prototype.hasOwnProperty.call(patch, 'scheduledWeekKey');
9427
10397
  const hasOrderInDayPatch = Object.prototype.hasOwnProperty.call(patch, 'orderInDay');
@@ -9486,7 +10456,7 @@ export class TaskforceCore {
9486
10456
  const mappableFields = [
9487
10457
  'title', 'description', 'status', 'completedAt', 'priority', 'complexity', 'type', 'category',
9488
10458
  'canceledReason', 'createdBy', 'assignee', 'scheduledDate', 'dueDate',
9489
- 'scheduledWeekKey', 'orderInDay', 'workstreamOrder', 'workstreamId', 'referenceNumber', 'localReferenceNumber'
10459
+ 'scheduledWeekKey', 'orderInDay', 'workstreamOrder', 'workstreamId', 'cardCoverAssetId', 'referenceNumber', 'localReferenceNumber'
9490
10460
  ];
9491
10461
  const propertyToColumn = {
9492
10462
  canceledReason: 'canceled_reason',
@@ -9500,6 +10470,7 @@ export class TaskforceCore {
9500
10470
  orderInDay: 'order_in_day',
9501
10471
  workstreamOrder: 'workstream_order',
9502
10472
  workstreamId: 'workstream_id',
10473
+ cardCoverAssetId: 'card_cover_asset_id',
9503
10474
  };
9504
10475
  mappableFields.forEach(f => {
9505
10476
  if (patch[f] !== undefined) {
@@ -10398,6 +11369,123 @@ export class TaskforceCore {
10398
11369
  listTasksForSync(workspaceId, cursor, limit) {
10399
11370
  return listTasksForSyncService(this.getSyncReconciliationServiceDeps(), workspaceId, cursor, limit);
10400
11371
  }
11372
+ /**
11373
+ * Full task inventory for authoritative repair without the per-task UI
11374
+ * hydration performed by rowToTask. Repair serializes task roots, comments,
11375
+ * and checklist items separately; task activity, actor decoration, and
11376
+ * resolved attachment bodies are not part of that canonical inventory.
11377
+ */
11378
+ listTasksForAuthoritativeRepair(workspaceId) {
11379
+ const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
11380
+ const rows = this.db.prepare(`
11381
+ SELECT *
11382
+ FROM tasks
11383
+ WHERE tenant_id = ? AND workspace_id = ?
11384
+ ORDER BY COALESCE(updated_at, created_at) ASC, id ASC
11385
+ `).all(this.tenantId, normalizedWorkspaceId);
11386
+ if (rows.length === 0)
11387
+ return [];
11388
+ const commentsByTaskId = new Map();
11389
+ const checklistByTaskId = new Map();
11390
+ const taskIds = rows.map((row) => String(row.id || '').trim()).filter(Boolean);
11391
+ const batchSize = workspaceSyncV3RepairEntityReadBatchSize(this.db);
11392
+ const normalizedCommentAuthor = new Map();
11393
+ for (let offset = 0; offset < taskIds.length; offset += batchSize) {
11394
+ const batch = taskIds.slice(offset, offset + batchSize);
11395
+ const placeholders = batch.map(() => '?').join(', ');
11396
+ const commentRows = this.db.prepare(`
11397
+ SELECT id, task_id, author, submitted_author, text, timestamp
11398
+ FROM comments
11399
+ WHERE tenant_id = ? AND workspace_id = ? AND task_id IN (${placeholders})
11400
+ ORDER BY task_id ASC, timestamp ASC
11401
+ `).all(this.tenantId, normalizedWorkspaceId, ...batch);
11402
+ for (const comment of commentRows) {
11403
+ const taskId = String(comment.task_id || '').trim();
11404
+ const rawAuthor = String(comment.author || 'user');
11405
+ let author = normalizedCommentAuthor.get(rawAuthor);
11406
+ if (!author) {
11407
+ author = this.normalizeCommentAuthor(rawAuthor, normalizedWorkspaceId);
11408
+ normalizedCommentAuthor.set(rawAuthor, author);
11409
+ }
11410
+ const items = commentsByTaskId.get(taskId) || [];
11411
+ items.push({
11412
+ id: String(comment.id || '').trim(),
11413
+ author,
11414
+ submittedAuthor: String(comment.submitted_author || comment.author || 'user').trim() || 'user',
11415
+ text: String(comment.text || ''),
11416
+ timestamp: String(comment.timestamp || ''),
11417
+ });
11418
+ commentsByTaskId.set(taskId, items);
11419
+ }
11420
+ const checklistRows = this.db.prepare(`
11421
+ SELECT id, task_id, title, is_completed, sort_order, created_at, updated_at
11422
+ FROM checklist_items
11423
+ WHERE tenant_id = ? AND workspace_id = ? AND task_id IN (${placeholders})
11424
+ ORDER BY task_id ASC, sort_order ASC, created_at ASC
11425
+ `).all(this.tenantId, normalizedWorkspaceId, ...batch);
11426
+ for (const checklistRow of checklistRows) {
11427
+ const item = this.rowToChecklistItem(checklistRow);
11428
+ const items = checklistByTaskId.get(item.taskId) || [];
11429
+ items.push(item);
11430
+ checklistByTaskId.set(item.taskId, items);
11431
+ }
11432
+ }
11433
+ const sanitizedAssigneeByValue = new Map();
11434
+ const normalizedCreatedBy = new Map();
11435
+ return rows.map((row) => {
11436
+ const taskId = String(row.id || '').trim();
11437
+ const rawAssignee = String(row.assignee || '');
11438
+ let sanitizedAssignee = sanitizedAssigneeByValue.get(rawAssignee);
11439
+ if (sanitizedAssignee === undefined) {
11440
+ sanitizedAssignee = this.sanitizeAssignee(row.assignee, normalizedWorkspaceId);
11441
+ sanitizedAssigneeByValue.set(rawAssignee, sanitizedAssignee);
11442
+ }
11443
+ const assignee = this.repairTaskAssignee(taskId, normalizedWorkspaceId, row.assignee, sanitizedAssignee) ? sanitizedAssignee : String(row.assignee || '').trim() || 'unassigned';
11444
+ const rawCreatedBy = String(row.created_by || '');
11445
+ if (!normalizedCreatedBy.has(rawCreatedBy)) {
11446
+ normalizedCreatedBy.set(rawCreatedBy, this.normalizeCreatedBy(row.created_by, normalizedWorkspaceId, { preserveGenericAi: true }));
11447
+ }
11448
+ const scheduledDate = this.sanitizeDateOnly(row.scheduled_date);
11449
+ const dueDate = this.sanitizeDateOnly(row.due_date);
11450
+ const scheduledWeekKey = row.scheduled_week_key
11451
+ || (scheduledDate ? this.dateOnlyToIsoWeekKey(scheduledDate) : null);
11452
+ const referenceNumber = this.sanitizeTaskReferenceNumber(row.reference_number);
11453
+ const localReferenceNumber = this.sanitizeTaskReferenceNumber(row.local_reference_number);
11454
+ return {
11455
+ id: taskId,
11456
+ referenceNumber,
11457
+ localReferenceNumber,
11458
+ referenceLabel: deriveTaskReferenceLabel({ referenceNumber, localReferenceNumber }),
11459
+ title: row.title,
11460
+ description: row.description,
11461
+ status: this.normalizeStatusValue(row.status),
11462
+ priority: this.normalizePriorityValue(row.priority),
11463
+ complexity: row.complexity,
11464
+ type: row.type,
11465
+ category: row.category,
11466
+ createdAt: row.created_at,
11467
+ updatedAt: row.updated_at,
11468
+ completedAt: row.completed_at,
11469
+ canceledReason: row.canceled_reason,
11470
+ isArchived: row.is_archived === 1,
11471
+ scheduledDate,
11472
+ dueDate,
11473
+ scheduledWeekKey,
11474
+ orderInDay: this.sanitizeOrderInDay(row.order_in_day),
11475
+ workstreamOrder: this.sanitizeOrderInDay(row.workstream_order),
11476
+ workstreamId: this.sanitizeWorkstreamId(row.workstream_id),
11477
+ cardCoverAssetId: typeof row.card_cover_asset_id === 'string' && row.card_cover_asset_id.trim()
11478
+ ? row.card_cover_asset_id.trim()
11479
+ : null,
11480
+ taxonomies: JSON.parse(row.taxonomies || '{}'),
11481
+ createdBy: normalizedCreatedBy.get(rawCreatedBy),
11482
+ assignee,
11483
+ comments: commentsByTaskId.get(taskId) || [],
11484
+ attachments: [],
11485
+ checklistItems: checklistByTaskId.get(taskId) || [],
11486
+ };
11487
+ });
11488
+ }
10401
11489
  listWorkspaceSyncDeletesSince(workspaceId, cursor, limit) {
10402
11490
  return listWorkspaceSyncDeletesSinceService(this.getSyncReconciliationServiceDeps(), workspaceId, cursor, limit);
10403
11491
  }