@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.
- package/dist/Taskforce.module.css +257 -87
- package/dist/TaskforceCore.js +237 -230
- package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.js +1 -1
- package/dist/cli.js +34 -0
- package/dist/cliCloudTargetGuard.d.ts +5 -0
- package/dist/cliCloudTargetGuard.js +11 -0
- package/dist/components/activity/EntityActivityTimeline.js +15 -5
- package/dist/components/context/AttachmentPreviewHost.js +1 -1
- package/dist/components/context/ContextAttachmentManager.d.ts +3 -0
- package/dist/components/context/ContextAttachmentManager.js +116 -20
- package/dist/components/features/AnnotatedAttachmentWorkspace.js +24 -8
- package/dist/components/features/TaskSettings.d.ts +12 -1
- package/dist/components/features/TaskSettings.js +147 -40
- package/dist/components/features/agentCapabilities/AgentCapabilitiesPrototype.d.ts +1 -1
- package/dist/components/features/planning/PlanningEntitySummary.d.ts +3 -1
- package/dist/components/features/planning/PlanningEntitySummary.js +3 -2
- package/dist/components/features/planning/PlanningLinkPicker.d.ts +3 -1
- package/dist/components/features/planning/PlanningLinkPicker.js +5 -3
- package/dist/components/features/planning/PlanningModule.js +28 -14
- package/dist/components/features/planning/PlanningTaskOrderList.d.ts +1 -1
- package/dist/components/features/planning/PlanningTaskOrderList.js +17 -6
- package/dist/components/features/planning/PlanningVisualIdentity.d.ts +14 -0
- package/dist/components/features/planning/PlanningVisualIdentity.js +106 -0
- package/dist/components/features/planning/planningTaskArrangement.d.ts +1 -0
- package/dist/components/features/planning/planningTaskArrangement.js +5 -5
- package/dist/components/features/planning/planningWorkspaceModel.d.ts +11 -0
- package/dist/components/features/taskforceAgentEditorModel.js +3 -1
- package/dist/components/task/TaskActionHeader.d.ts +2 -1
- package/dist/components/task/TaskActionHeader.js +2 -2
- package/dist/components/task/TaskCard.d.ts +5 -0
- package/dist/components/task/TaskCard.js +101 -13
- package/dist/components/task/TaskContextUpload.d.ts +3 -0
- package/dist/components/task/TaskContextUpload.js +2 -2
- package/dist/components/task/TaskForm.d.ts +4 -0
- package/dist/components/task/TaskForm.js +32 -7
- package/dist/components/task/TaskImageReviews.js +28 -19
- package/dist/components/task/TaskKanban.d.ts +6 -1
- package/dist/components/task/TaskKanban.js +152 -6
- package/dist/components/task/TaskList.d.ts +5 -0
- package/dist/components/task/TaskList.js +3 -3
- package/dist/components/task/TaskStatusActions.js +11 -2
- package/dist/components/task/TaskWorkstreamPicker.js +2 -1
- package/dist/components/ui/DocumentTypeIcon.d.ts +7 -0
- package/dist/components/ui/DocumentTypeIcon.js +8 -0
- package/dist/components/ui/PasswordInput.d.ts +6 -0
- package/dist/components/ui/PasswordInput.js +11 -0
- package/dist/components/ui/TaxonomyDropdown.d.ts +9 -1
- package/dist/components/ui/TaxonomyDropdown.js +30 -3
- package/dist/components/ui/TopNoticeLayer.js +1 -1
- package/dist/components/views/AppShellHeader.js +32 -11
- package/dist/components/views/PlanComparisonPage.js +12 -0
- package/dist/components/views/StandaloneLayout.js +68 -39
- package/dist/components/views/WidgetView.js +10 -7
- package/dist/components/views/panels/PlanningDrawer.d.ts +7 -1
- package/dist/components/views/panels/PlanningDrawer.js +51 -54
- package/dist/components/views/standalone/modals/AccountSettingsModal.js +26 -17
- package/dist/config/envSchema.js +13 -0
- package/dist/config/localServiceCloudTarget.d.ts +10 -0
- package/dist/config/localServiceCloudTarget.js +76 -0
- package/dist/config/localServiceRuntimeContract.d.ts +13 -0
- package/dist/config/localServiceRuntimeContract.js +135 -0
- package/dist/core/AiProfileService.d.ts +1 -0
- package/dist/core/AiProfileService.js +48 -9
- package/dist/core/AttachmentLinkService.js +7 -4
- package/dist/core/DeletedTaskLifecycleService.d.ts +5 -0
- package/dist/core/DeletedTaskLifecycleService.js +55 -3
- package/dist/core/McpTokenService.d.ts +4 -0
- package/dist/core/McpTokenService.js +52 -0
- package/dist/core/TaskAttachmentLinksDurableMutationService.d.ts +8 -2
- package/dist/core/TaskAttachmentLinksDurableMutationService.js +51 -18
- package/dist/core/Taskforce.d.ts +124 -2
- package/dist/core/Taskforce.js +1175 -87
- package/dist/core/types.d.ts +8 -1
- package/dist/documentReviews/DocumentReviewCommentStore.d.ts +11 -0
- package/dist/documentReviews/DocumentReviewCommentStore.js +35 -2
- package/dist/documentReviews/service.d.ts +4 -0
- package/dist/documentReviews/service.js +141 -46
- package/dist/hooks/sync/useDurableLocalOutboxDrain.d.ts +1 -1
- package/dist/hooks/sync/useDurableTaskCreateMutation.js +1 -1
- package/dist/hooks/sync/useDurableTaskMetadataMutation.d.ts +1 -0
- package/dist/hooks/sync/useDurableTaskMetadataMutation.js +1 -0
- package/dist/hooks/sync/useDurableTaskStatusMutation.d.ts +1 -0
- package/dist/hooks/tasks/taskHttpMutation.js +1 -0
- package/dist/hooks/tasks/useCloudTaskSearch.d.ts +12 -0
- package/dist/hooks/tasks/useCloudTaskSearch.js +71 -0
- package/dist/hooks/tasks/useTaskDescriptionImageDraftRecovery.d.ts +16 -0
- package/dist/hooks/tasks/useTaskDescriptionImageDraftRecovery.js +197 -0
- package/dist/hooks/tasks/useTaskEditorNavigation.d.ts +16 -3
- package/dist/hooks/tasks/useTaskEditorNavigation.js +72 -13
- package/dist/hooks/tasks/useTaskFormActions.d.ts +9 -0
- package/dist/hooks/tasks/useTaskFormActions.js +96 -17
- package/dist/hooks/ui/useSettingsModel.d.ts +24 -2
- package/dist/hooks/ui/useSettingsModel.js +13 -2
- package/dist/hooks/ui/useSettingsPersistence.d.ts +12 -1
- package/dist/hooks/ui/useSettingsPersistence.js +74 -47
- package/dist/hooks/useFilterSort.d.ts +7 -3
- package/dist/hooks/useFilterSort.js +15 -5
- package/dist/hooks/useTaskData.d.ts +10 -1
- package/dist/hooks/useTaskData.js +216 -7
- package/dist/hooks/useTaskMutations.d.ts +10 -2
- package/dist/hooks/useTaskMutations.js +223 -43
- package/dist/hooks/useTaskforce.d.ts +94 -57
- package/dist/hooks/useTaskforce.js +428 -16
- package/dist/hooks/useUiNotice.d.ts +4 -0
- package/dist/hooks/useUiNotice.js +4 -0
- package/dist/mcp/adminWorkspaceRegistrar.js +3 -3
- package/dist/mcp/canonicalAssetHelpers.d.ts +13 -0
- package/dist/mcp/canonicalAssetHelpers.js +39 -5
- package/dist/mcp/collaborationRegistrar.js +15 -13
- package/dist/mcp/documentAssetRegistrar.js +60 -44
- package/dist/mcp/documentHelpers.d.ts +1 -0
- package/dist/mcp/documentHelpers.js +7 -3
- package/dist/mcp/localServiceProxy.js +4 -1
- package/dist/mcp/runtime.d.ts +16 -0
- package/dist/mcp/runtime.js +735 -210
- package/dist/mcp/taskAttachmentCommandAdapter.d.ts +2 -0
- package/dist/mcp/taskAttachmentCommandAdapter.js +14 -8
- package/dist/mcp/taskPlanningRegistrar.js +78 -67
- package/dist/mcp/toolCallCompatibility.js +38 -1
- package/dist/mcp/toolCatalog.d.ts +1 -0
- package/dist/mcp/toolRegistry.js +2 -2
- package/dist/migrations/taskSchemaMigrations.js +47 -0
- package/dist/runtime/appGateDefinitions.d.ts +30 -6
- package/dist/runtime/appGateDefinitions.js +6 -6
- package/dist/runtime/appGates.d.ts +12 -13
- package/dist/runtime/appGates.js +29 -13
- package/dist/runtime/nodeAppGates.d.ts +8 -0
- package/dist/runtime/nodeAppGates.js +27 -0
- package/dist/server/appAccess.d.ts +12 -0
- package/dist/server/appAccess.js +21 -0
- package/dist/server/authorizedWorkspaceAccess.d.ts +12 -0
- package/dist/server/authorizedWorkspaceAccess.js +40 -0
- package/dist/server/cloudCollectionRead.d.ts +25 -0
- package/dist/server/cloudCollectionRead.js +120 -0
- package/dist/server/cloudCollectionReadPool.d.ts +52 -0
- package/dist/server/cloudCollectionReadPool.js +140 -0
- package/dist/server/cloudCollectionReadWorker.d.ts +1 -0
- package/dist/server/cloudCollectionReadWorker.js +36 -0
- package/dist/server/index.d.ts +6 -11
- package/dist/server/index.js +67 -42
- package/dist/server/localServiceLease.d.ts +6 -0
- package/dist/server/localServiceLease.js +17 -1
- package/dist/server/localWorkspaceSyncServerRuntime.js +10 -3
- package/dist/server/rateLimit.d.ts +1 -0
- package/dist/server/rateLimit.js +4 -0
- package/dist/server/realtimeSyncWs.js +36 -12
- package/dist/server/routes/admin.js +1 -0
- package/dist/server/routes/agentRoles.js +3 -2
- package/dist/server/routes/agentSkills.js +3 -2
- package/dist/server/routes/agents.js +118 -16
- package/dist/server/routes/annotatedAttachments.js +3 -2
- package/dist/server/routes/auth.js +3 -1
- package/dist/server/routes/documents.d.ts +5 -2
- package/dist/server/routes/documents.js +160 -13
- package/dist/server/routes/durableTaskHttpRouters.d.ts +11 -2
- package/dist/server/routes/durableTaskHttpRouters.js +2 -2
- package/dist/server/routes/localService.js +4 -0
- package/dist/server/routes/primitives.d.ts +4 -0
- package/dist/server/routes/shared.d.ts +24 -0
- package/dist/server/routes/shared.js +114 -18
- package/dist/server/routes/sync.js +22 -0
- package/dist/server/routes/syncAuxRoutes.js +5 -2
- package/dist/server/routes/syncPullApplyRoutes.js +23 -4
- package/dist/server/routes/syncV3FeedRoutes.js +22 -18
- package/dist/server/routes/syncV3LocalCaptureRoutes.js +48 -4
- package/dist/server/routes/syncV3LocalFeedApplyRoutes.d.ts +1 -0
- package/dist/server/routes/syncV3LocalFeedApplyRoutes.js +26 -0
- package/dist/server/routes/syncV3OperationRoutes.js +27 -2
- package/dist/server/routes/tasks.js +299 -97
- package/dist/server/routes/workspaces.js +43 -1
- package/dist/server/routes.js +35 -7
- package/dist/server/workspaceDeletionAdmission.d.ts +23 -0
- package/dist/server/workspaceDeletionAdmission.js +50 -0
- package/dist/service/localServiceCli.js +52 -25
- package/dist/service/localServiceClientLifecycle.js +93 -6
- package/dist/services/codexAppServerModelGateway.d.ts +112 -0
- package/dist/services/codexAppServerModelGateway.js +438 -0
- package/dist/services/taskforceAgentMcpBridge.d.ts +7 -1
- package/dist/services/taskforceAgentMcpBridge.js +23 -3
- package/dist/shared/planningIdentity.d.ts +20 -0
- package/dist/shared/planningIdentity.js +79 -0
- package/dist/shared/taskforceAgentDefinition.js +7 -1
- package/dist/shared/taskforceAgentModels.d.ts +1 -0
- package/dist/shared/taskforceAgentModels.js +30 -1
- package/dist/storage/postgresAdapter.d.ts +20 -3
- package/dist/storage/postgresAdapter.js +110 -14
- package/dist/storage/postgresRequestDiagnostics.d.ts +8 -0
- package/dist/storage/postgresRequestDiagnostics.js +24 -0
- package/dist/storage/postgresWorker.js +13 -4
- package/dist/storage/postgresWorkerProtocol.d.ts +29 -0
- package/dist/storage/postgresWorkerProtocol.js +30 -0
- package/dist/storage/workspaceAssetStore.d.ts +32 -0
- package/dist/storage/workspaceAssetStore.js +70 -0
- package/dist/sync/cloudSyncApi.d.ts +4 -1
- package/dist/sync/cloudSyncApi.js +7 -2
- package/dist/sync/contentSyncState.d.ts +1 -1
- package/dist/sync/coordinator/localSyncExecutionPermitStore.d.ts +17 -0
- package/dist/sync/coordinator/localSyncExecutionPermitStore.js +77 -23
- package/dist/sync/coordinator/localWorkspaceSyncRunner.js +6 -2
- package/dist/sync/coordinator/workspaceSyncTransportGateway.js +4 -2
- package/dist/sync/engine/workspaceSyncEngineBrowserGatewayRemoteServices.js +20 -2
- package/dist/sync/engine/workspaceSyncEngineLocalServices.d.ts +4 -0
- package/dist/sync/engine/workspaceSyncEngineLocalServices.js +11 -0
- package/dist/sync/engine/workspaceSyncEngineRemoteServices.d.ts +16 -0
- package/dist/sync/engine/workspaceSyncEngineRemoteServices.js +2 -1
- package/dist/sync/engine/workspaceSyncEngineServerRemoteServices.d.ts +1 -1
- package/dist/sync/engine/workspaceSyncEngineServerRemoteServices.js +46 -4
- package/dist/sync/engine/workspaceSyncEngineServerRunnerOperations.d.ts +3 -1
- package/dist/sync/engine/workspaceSyncEngineServerRunnerOperations.js +51 -11
- package/dist/sync/engine/workspaceSyncEngineTransfers.d.ts +1 -1
- package/dist/sync/engine/workspaceSyncEngineTransfers.js +8 -3
- package/dist/sync/engine/workspaceSyncV2ChangeDispatcher.js +2 -0
- package/dist/sync/syncApplyHandlers.d.ts +3 -1
- package/dist/sync/syncApplyHandlers.js +29 -0
- package/dist/sync/syncService.d.ts +2 -0
- package/dist/sync/syncService.js +13 -1
- package/dist/sync/taskSyncPayload.d.ts +1 -0
- package/dist/sync/taskSyncPayload.js +6 -0
- package/dist/sync/v3/combinedFeedCapability.js +4 -0
- package/dist/sync/v3/durableTaskAttachmentLinksMutationRouter.d.ts +3 -0
- package/dist/sync/v3/durableTaskAttachmentLinksMutationRouter.js +31 -3
- package/dist/sync/v3/durableTaskChecklistMutationRouter.d.ts +2 -0
- package/dist/sync/v3/durableTaskChecklistMutationRouter.js +4 -0
- package/dist/sync/v3/durableTaskHttpBulkMutationRouter.d.ts +1 -0
- package/dist/sync/v3/durableTaskHttpBulkMutationRouter.js +47 -12
- package/dist/sync/v3/durableTaskHttpRootMutationRouter.d.ts +5 -3
- package/dist/sync/v3/durableTaskHttpRootMutationRouter.js +27 -2
- package/dist/sync/v3/localInitiativeOutboxService.js +4 -2
- package/dist/sync/v3/localOutboxDispatcher.js +5 -1
- package/dist/sync/v3/localTaskAttachmentLinksOutboxHandler.js +12 -2
- package/dist/sync/v3/localTaskAttachmentLinksOutboxService.d.ts +3 -0
- package/dist/sync/v3/localTaskAttachmentLinksOutboxService.js +62 -10
- package/dist/sync/v3/localTaskCreateOutboxService.js +15 -6
- package/dist/sync/v3/localTaskMetadataClient.d.ts +1 -0
- package/dist/sync/v3/localTaskMetadataClient.js +1 -0
- package/dist/sync/v3/localTaskMetadataOutboxHandler.js +5 -1
- package/dist/sync/v3/localTaskMetadataOutboxService.d.ts +2 -0
- package/dist/sync/v3/localTaskMetadataOutboxService.js +7 -0
- package/dist/sync/v3/localTaskRestoreOutboxService.js +14 -6
- package/dist/sync/v3/localTaskStatusClient.d.ts +1 -0
- package/dist/sync/v3/localTaskStatusClient.js +6 -1
- package/dist/sync/v3/localTaskStatusOutboxService.d.ts +2 -0
- package/dist/sync/v3/localTaskStatusOutboxService.js +54 -13
- package/dist/sync/v3/localWorkstreamOutboxService.js +4 -2
- package/dist/sync/v3/syncDurabilityStore.d.ts +15 -0
- package/dist/sync/v3/syncDurabilityStore.js +201 -43
- package/dist/sync/v3/taskAttachmentLinksMutationService.d.ts +7 -4
- package/dist/sync/v3/taskAttachmentLinksMutationService.js +37 -12
- package/dist/sync/v3/taskChecklistMutationService.d.ts +4 -2
- package/dist/sync/v3/taskChecklistMutationService.js +4 -0
- package/dist/sync/v3/taskCreateMutationService.d.ts +2 -2
- package/dist/sync/v3/taskCreateMutationService.js +7 -4
- package/dist/sync/v3/taskMetadataCoexistence.js +4 -0
- package/dist/sync/v3/taskMetadataMutationService.d.ts +3 -0
- package/dist/sync/v3/taskMetadataMutationService.js +4 -0
- package/dist/sync/v3/taskRestoreMutationService.d.ts +2 -2
- package/dist/sync/v3/taskRestoreMutationService.js +7 -4
- package/dist/sync/v3/taskStatusMutationService.d.ts +2 -0
- package/dist/sync/v3/taskStatusMutationService.js +4 -0
- package/dist/sync/v3/workflowAssignmentSync.d.ts +1 -0
- package/dist/sync/v3/workflowAssignmentSync.js +3 -0
- package/dist/sync/v3/workspaceSyncV2ProjectionExclusions.d.ts +1 -1
- package/dist/sync/v3/workspaceSyncV2ProjectionExclusions.js +14 -12
- package/dist/sync/v3/workspaceSyncV3AttachmentApplyObligationStore.d.ts +11 -0
- package/dist/sync/v3/workspaceSyncV3AttachmentApplyObligationStore.js +27 -10
- package/dist/sync/v3/workspaceSyncV3AttachmentAssetHydration.d.ts +2 -1
- package/dist/sync/v3/workspaceSyncV3AttachmentAssetHydration.js +52 -10
- package/dist/sync/v3/workspaceSyncV3CloudOperationHandler.js +10 -1
- package/dist/sync/v3/workspaceSyncV3CombinedFeedOrchestrator.js +15 -4
- package/dist/sync/v3/workspaceSyncV3CombinedLocalApply.js +66 -13
- package/dist/sync/v3/workspaceSyncV3CombinedRepairBaseline.d.ts +11 -0
- package/dist/sync/v3/workspaceSyncV3CombinedRepairBaseline.js +57 -22
- package/dist/sync/v3/workspaceSyncV3CombinedRepairMaterialization.d.ts +10 -4
- package/dist/sync/v3/workspaceSyncV3CombinedRepairMaterialization.js +124 -51
- package/dist/sync/v3/workspaceSyncV3CombinedRepairSnapshot.d.ts +10 -0
- package/dist/sync/v3/workspaceSyncV3CombinedRepairSnapshot.js +120 -28
- package/dist/sync/v3/workspaceSyncV3CombinedWireContract.d.ts +3 -1
- package/dist/sync/v3/workspaceSyncV3CombinedWireContract.js +18 -0
- package/dist/sync/v3/workspaceSyncV3CoverageRegistry.js +6 -0
- package/dist/sync/v3/workspaceSyncV3FeedProfile.d.ts +1 -1
- package/dist/sync/v3/workspaceSyncV3LegacyBoundaryAttestation.d.ts +1 -0
- package/dist/sync/v3/workspaceSyncV3LegacyBoundaryAttestation.js +23 -10
- package/dist/sync/v3/workspaceSyncV3NestedTaskApply.d.ts +3 -0
- package/dist/sync/v3/workspaceSyncV3NestedTaskApply.js +64 -13
- package/dist/sync/v3/workspaceSyncV3NestedTaskCodecs.d.ts +9 -0
- package/dist/sync/v3/workspaceSyncV3NestedTaskCodecs.js +26 -0
- package/dist/sync/v3/workspaceSyncV3NestedTaskProjection.d.ts +15 -5
- package/dist/sync/v3/workspaceSyncV3NestedTaskProjection.js +50 -14
- package/dist/sync/v3/workspaceSyncV3NestedTaskRepairSnapshot.d.ts +2 -1
- package/dist/sync/v3/workspaceSyncV3NestedTaskRepairSnapshot.js +11 -3
- package/dist/sync/v3/workspaceSyncV3OperationProtocol.d.ts +3 -0
- package/dist/sync/v3/workspaceSyncV3OperatorDiagnostics.d.ts +2 -0
- package/dist/sync/v3/workspaceSyncV3OperatorDiagnostics.js +19 -7
- package/dist/sync/v3/workspaceSyncV3RepairSqlBatching.d.ts +2 -0
- package/dist/sync/v3/workspaceSyncV3RepairSqlBatching.js +10 -0
- package/dist/sync/v3/workspaceSyncV3TaskCommentAudit.js +9 -2
- package/dist/sync/v3/workspaceSyncV3TaskCoverCoverage.d.ts +7 -0
- package/dist/sync/v3/workspaceSyncV3TaskCoverCoverage.js +37 -0
- package/dist/sync/v3/workspaceSyncV3TaskCoverWireContract.d.ts +6 -0
- package/dist/sync/v3/workspaceSyncV3TaskCoverWireContract.js +9 -0
- package/dist/sync/v3/workspaceSyncV3TaskRootProjection.d.ts +1 -1
- package/dist/sync/v3/workspaceSyncV3TaskRootProjection.js +1 -1
- package/dist/sync/workspacePullFeed.d.ts +6 -1
- package/dist/sync/workspacePullFeed.js +174 -162
- package/dist/sync/workspaceRepair.d.ts +3 -0
- package/dist/sync/workspaceRepair.js +67 -6
- package/dist/sync/workspaceSyncModel.d.ts +11 -0
- package/dist/sync/workspaceSyncModel.js +27 -2
- package/dist/types.d.ts +55 -1
- package/dist/types.js +6 -0
- package/dist/ui/assets/{AiIdentityRosterCard-DB1BLO_j.js → AiIdentityRosterCard-BZM4aQnM.js} +1 -1
- package/dist/ui/assets/AiProfilesModule-BbAEWtHH.js +1 -0
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-B68Wg-RV.js +3 -0
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-TzYu7pkG.css +1 -0
- package/dist/ui/assets/AssetTraySortControl-C8Rztyal.js +1 -0
- package/dist/ui/assets/ContextAttachmentManager-Cj3c7X9y.js +3 -0
- package/dist/ui/assets/{DocumentWorkspace-BC7sy5TG.js → DocumentWorkspace-B5uvzems.js} +4 -4
- package/dist/ui/assets/EntityActivityTimeline-BVFvCB5R.js +1 -0
- package/dist/ui/assets/PlanningModule-B5DWQOGz.js +1 -0
- package/dist/ui/assets/PlanningModule-CoIR9hbV.css +1 -0
- package/dist/ui/assets/PlansPage-AY4cGWco.js +1 -0
- package/dist/ui/assets/TaskContextUpload-DZxTVesW.js +1 -0
- package/dist/ui/assets/TaskContextUpload-qmBeUw3u.css +1 -0
- package/dist/ui/assets/TaskSettings-BeWAuyZj.js +12 -0
- package/dist/ui/assets/{TaskforceAgentsModule-CkAxW19K.js → TaskforceAgentsModule-BifCYtjR.js} +6 -6
- package/dist/ui/assets/WorkflowManagerModule-DEr5di_r.js +1 -0
- package/dist/ui/assets/index-I5zhdtgt.js +7 -0
- package/dist/ui/assets/index-VRFsx3wz.css +1 -0
- package/dist/ui/assets/{vendor-icons-B8ZNsH1g.js → vendor-icons-D9Lpw-j4.js} +1 -1
- package/dist/ui/index.html +3 -3
- package/dist/utils/constants.d.ts +2 -2
- package/dist/utils/constants.js +1 -1
- package/dist/utils/contextAttachmentUpload.d.ts +2 -0
- package/dist/utils/contextAttachmentUpload.js +7 -0
- package/dist/utils/contextFiles.d.ts +1 -1
- package/dist/utils/contextFiles.js +4 -4
- package/dist/utils/taskActivity.d.ts +3 -2
- package/dist/utils/taskActivity.js +19 -2
- package/dist/utils/taskEditorReconciliation.d.ts +40 -0
- package/dist/utils/taskEditorReconciliation.js +217 -0
- package/dist/utils/taskNormalization.d.ts +1 -0
- package/dist/utils/taskNormalization.js +39 -0
- package/dist/utils/taskSearch.js +2 -2
- package/dist/utils/uiNotice.d.ts +4 -0
- package/package.json +21 -4
- package/dist/ui/assets/AiProfilesModule-DPxQeF9N.js +0 -1
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-CBRZ8rca.css +0 -1
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-DHNYaNg6.js +0 -3
- package/dist/ui/assets/AssetTraySortControl-DphbyHVc.js +0 -1
- package/dist/ui/assets/ContextAttachmentManager-CMxSzHqc.js +0 -3
- package/dist/ui/assets/EntityActivityTimeline-pJ2ZjZ-k.js +0 -1
- package/dist/ui/assets/PlanningModule-CeTqXMMz.css +0 -1
- package/dist/ui/assets/PlanningModule-t5GbvVR9.js +0 -1
- package/dist/ui/assets/PlansPage-BVH_NfgT.js +0 -1
- package/dist/ui/assets/TaskContextUpload-Ds4LnDzV.css +0 -1
- package/dist/ui/assets/TaskContextUpload-bl4fOOUz.js +0 -1
- package/dist/ui/assets/TaskSettings-xVsxSa0g.js +0 -12
- package/dist/ui/assets/WorkflowManagerModule-BKqIGVnF.js +0 -1
- package/dist/ui/assets/index-BzJlVnNS.css +0 -1
- package/dist/ui/assets/index-Cx7vZJHL.js +0 -7
package/dist/core/Taskforce.js
CHANGED
|
@@ -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
|
-
|
|
1395
|
-
|
|
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
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
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
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
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
|
-
|
|
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 [
|
|
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
|
|
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
|
|
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
|
-
|
|
3950
|
-
|
|
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
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
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
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
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
|
-
|
|
4239
|
-
|
|
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
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
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
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
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
|
-
|
|
6922
|
-
|
|
6923
|
-
|
|
6924
|
-
|
|
6925
|
-
|
|
6926
|
-
|
|
6927
|
-
|
|
6928
|
-
|
|
6929
|
-
|
|
6930
|
-
|
|
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) => [
|
|
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
|
}
|