@taskforcehq/taskforce 0.3.301 → 0.3.303

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/dist/Taskforce.module.css +37 -2
  2. package/dist/TaskforceCore.filter-persistence.test.js +1 -1
  3. package/dist/TaskforceCore.js +72 -33
  4. package/dist/TaskforceCore.test.js +848 -130
  5. package/dist/components/features/TaskSettings.js +14 -82
  6. package/dist/components/features/TaskSettings.test.js +33 -70
  7. package/dist/components/features/settings/TaxonomyLibraryManager.js +8 -2
  8. package/dist/components/features/settings/TaxonomyLibraryManager.test.js +1 -1
  9. package/dist/components/task/TaskActionHeader.js +6 -3
  10. package/dist/components/task/TaskCard.js +6 -3
  11. package/dist/components/task/TaskForm.js +2 -2
  12. package/dist/components/task/TaskForm.test.js +41 -2
  13. package/dist/components/task/TaskKanban.test.js +17 -0
  14. package/dist/components/ui/ReferenceBadgeButton.js +1 -1
  15. package/dist/components/views/PlansPage.d.ts +3 -1
  16. package/dist/components/views/PlansPage.js +109 -49
  17. package/dist/components/views/PlansPage.test.d.ts +1 -0
  18. package/dist/components/views/PlansPage.test.js +100 -0
  19. package/dist/components/views/StandaloneLayout.d.ts +1 -0
  20. package/dist/components/views/StandaloneLayout.js +473 -256
  21. package/dist/components/views/panels/PlanningDrawer.js +18 -1
  22. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +48 -0
  23. package/dist/components/views/standalone/modals/AccountHubModal.js +36 -0
  24. package/dist/components/views/standalone/modals/AccountHubModal.test.d.ts +1 -0
  25. package/dist/components/views/standalone/modals/AccountHubModal.test.js +26 -0
  26. package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.d.ts +9 -0
  27. package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.js +8 -0
  28. package/dist/components/views/standalone/modals/EditProfileModal.d.ts +26 -0
  29. package/dist/components/views/standalone/modals/EditProfileModal.js +10 -0
  30. package/dist/components/views/standalone/modals/HelpModal.d.ts +9 -0
  31. package/dist/components/views/standalone/modals/HelpModal.js +8 -0
  32. package/dist/components/views/standalone/modals/ScheduleWarningModal.d.ts +13 -0
  33. package/dist/components/views/standalone/modals/ScheduleWarningModal.js +6 -0
  34. package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +61 -0
  35. package/dist/components/views/standalone/modals/SyncStatusModal.js +38 -0
  36. package/dist/components/views/standalone/modals/TeamManagementModal.d.ts +64 -0
  37. package/dist/components/views/standalone/modals/TeamManagementModal.js +10 -0
  38. package/dist/components/views/standalone/modals/types.d.ts +2 -0
  39. package/dist/components/views/standalone/modals/types.js +1 -0
  40. package/dist/components/views/teamManagementAccess.d.ts +1 -1
  41. package/dist/components/views/teamManagementAccess.js +2 -2
  42. package/dist/components/views/teamManagementAccess.test.js +3 -3
  43. package/dist/config/deployment.js +2 -2
  44. package/dist/config/deployment.test.js +5 -3
  45. package/dist/core/AiProfileService.js +7 -2
  46. package/dist/core/AiProfiles.test.js +19 -0
  47. package/dist/core/EntitlementsPolicy.test.js +2 -2
  48. package/dist/core/GlobalSettingsService.js +78 -18
  49. package/dist/core/PlanEntitlementService.d.ts +29 -5
  50. package/dist/core/PlanEntitlementService.js +297 -219
  51. package/dist/core/SignupMonetizationSettings.test.js +173 -49
  52. package/dist/core/SyncReconciliationService.d.ts +2 -0
  53. package/dist/core/SyncReconciliationService.js +3 -0
  54. package/dist/core/SystemAdmin.test.js +186 -96
  55. package/dist/core/TaskAuditTimeline.test.js +40 -0
  56. package/dist/core/Taskforce.d.ts +100 -10
  57. package/dist/core/Taskforce.ids.test.js +133 -0
  58. package/dist/core/Taskforce.js +459 -127
  59. package/dist/core/Taskforce.mcpAuth.test.js +0 -2
  60. package/dist/core/UserProfileAvatarDrafts.test.d.ts +1 -0
  61. package/dist/core/UserProfileAvatarDrafts.test.js +111 -0
  62. package/dist/core/taskReferencePolicy.d.ts +28 -0
  63. package/dist/core/taskReferencePolicy.js +189 -0
  64. package/dist/core/types.d.ts +13 -12
  65. package/dist/hooks/tasks/useTaskFormActions.js +3 -0
  66. package/dist/hooks/useSyncOrchestrator.js +17 -3
  67. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +97 -5
  68. package/dist/hooks/useTaskData.js +3 -2
  69. package/dist/hooks/useTaskData.test.js +66 -0
  70. package/dist/hooks/useTaskforce.d.ts +8 -0
  71. package/dist/hooks/useTaskforce.js +72 -19
  72. package/dist/hooks/useTaskforce.sync-behavior.test.js +185 -6
  73. package/dist/mcp/aiProfileBootstrap.d.ts +29 -0
  74. package/dist/mcp/aiProfileBootstrap.js +80 -0
  75. package/dist/mcp/aiProfileBootstrap.test.d.ts +1 -0
  76. package/dist/mcp/aiProfileBootstrap.test.js +68 -0
  77. package/dist/mcp/clientIntegrations.d.ts +2 -8
  78. package/dist/mcp/clientIntegrations.js +21 -40
  79. package/dist/mcp/clientIntegrations.test.js +28 -2
  80. package/dist/mcp/clientProfileDefaults.d.ts +12 -0
  81. package/dist/mcp/clientProfileDefaults.js +37 -0
  82. package/dist/mcp/runtime.d.ts +10 -7
  83. package/dist/mcp/runtime.js +131 -48
  84. package/dist/mcp/runtime.test.js +321 -83
  85. package/dist/migrations/billingSchemaParity.test.js +3 -0
  86. package/dist/migrations/taskSchemaMigrations.js +68 -1
  87. package/dist/qaOpen.test.d.ts +1 -0
  88. package/dist/qaOpen.test.js +56 -0
  89. package/dist/server/index.cookieProxy.test.js +2 -0
  90. package/dist/server/index.d.ts +1 -0
  91. package/dist/server/index.js +17 -1
  92. package/dist/server/index.test.js +15 -1
  93. package/dist/server/mockStripe.d.ts +1 -0
  94. package/dist/server/mockStripe.js +2 -0
  95. package/dist/server/routes/admin.js +105 -45
  96. package/dist/server/routes/auth.d.ts +6 -0
  97. package/dist/server/routes/auth.js +505 -30
  98. package/dist/server/routes/billing.js +517 -24
  99. package/dist/server/routes/billing.test.js +967 -45
  100. package/dist/server/routes/documents.js +6 -1
  101. package/dist/server/routes/shared.d.ts +1 -1
  102. package/dist/server/routes/shared.js +16 -6
  103. package/dist/server/routes/sync.integration.test.js +290 -0
  104. package/dist/server/routes/sync.js +234 -1922
  105. package/dist/server/routes/tasks.js +18 -4
  106. package/dist/server/routes.js +29 -22
  107. package/dist/server/routes.test.js +639 -82
  108. package/dist/shared/runtimeContract.d.ts +1 -1
  109. package/dist/sync/cloudSyncApi.d.ts +1 -0
  110. package/dist/sync/collaborationSyncPayload.d.ts +17 -0
  111. package/dist/sync/collaborationSyncPayload.js +131 -0
  112. package/dist/sync/collaborationSyncPayload.test.d.ts +1 -0
  113. package/dist/sync/collaborationSyncPayload.test.js +104 -0
  114. package/dist/sync/collaborationSyncState.d.ts +15 -0
  115. package/dist/sync/collaborationSyncState.js +60 -0
  116. package/dist/sync/contentSyncPayload.d.ts +45 -0
  117. package/dist/sync/contentSyncPayload.js +104 -0
  118. package/dist/sync/contentSyncPayload.test.d.ts +1 -0
  119. package/dist/sync/contentSyncPayload.test.js +104 -0
  120. package/dist/sync/contentSyncState.d.ts +98 -0
  121. package/dist/sync/contentSyncState.js +1004 -0
  122. package/dist/sync/planningSyncPayload.d.ts +9 -0
  123. package/dist/sync/planningSyncPayload.js +82 -0
  124. package/dist/sync/planningSyncPayload.test.d.ts +1 -0
  125. package/dist/sync/planningSyncPayload.test.js +68 -0
  126. package/dist/sync/syncApplyHandlers.d.ts +83 -0
  127. package/dist/sync/syncApplyHandlers.js +445 -0
  128. package/dist/sync/syncService.d.ts +1 -0
  129. package/dist/sync/syncService.js +21 -8
  130. package/dist/sync/taskSyncPayload.d.ts +33 -0
  131. package/dist/sync/taskSyncPayload.js +126 -0
  132. package/dist/sync/taskSyncPayload.test.d.ts +1 -0
  133. package/dist/sync/taskSyncPayload.test.js +54 -0
  134. package/dist/sync/workspacePullFeed.d.ts +38 -0
  135. package/dist/sync/workspacePullFeed.js +349 -0
  136. package/dist/sync/workspacePullFeed.test.d.ts +1 -0
  137. package/dist/sync/workspacePullFeed.test.js +278 -0
  138. package/dist/sync/workspaceSyncModel.d.ts +4 -1
  139. package/dist/sync/workspaceSyncModel.js +9 -2
  140. package/dist/sync/workspaceSyncModel.test.js +29 -0
  141. package/dist/test/setup.js +1 -1
  142. package/dist/types.d.ts +1 -0
  143. package/dist/ui/assets/{AgentsModule-Bdq3T1Ts.js → AgentsModule-CpsTmrIW.js} +1 -1
  144. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-jKUVo-PJ.js → AnnotatedAttachmentWorkspace-C_TmXZwA.js} +1 -1
  145. package/dist/ui/assets/{DocumentWorkspace-DuQa-uIC.js → DocumentWorkspace-C3GyzrWm.js} +2 -2
  146. package/dist/ui/assets/{InitiativesModule-C8Pi2VTk.js → InitiativesModule-4-Rh2K2n.js} +1 -1
  147. package/dist/ui/assets/PlansPage-BP7AYOqr.js +1 -0
  148. package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +8 -0
  149. package/dist/ui/assets/{TaskSettings-BwB9NtmJ.css → TaskSettings-CnIBL_Eb.css} +1 -1
  150. package/dist/ui/assets/{WorkflowsModule-b6TLuHwJ.js → WorkflowsModule-CGk9s3NJ.js} +1 -1
  151. package/dist/ui/assets/index-CLIMgR6g.js +6 -0
  152. package/dist/ui/assets/index-DneETxcu.css +1 -0
  153. package/dist/ui/assets/{vendor-icons-QZyhEwgT.js → vendor-icons-pWocEipT.js} +1 -1
  154. package/dist/ui/index.html +3 -3
  155. package/dist/utils/accountProfileSummaryCache.d.ts +31 -0
  156. package/dist/utils/accountProfileSummaryCache.js +97 -0
  157. package/dist/utils/accountProfileSummaryCache.test.d.ts +1 -0
  158. package/dist/utils/accountProfileSummaryCache.test.js +63 -0
  159. package/dist/utils/taskActivity.js +17 -4
  160. package/dist/utils/taskActivity.test.js +27 -0
  161. package/dist/utils/taskNormalization.js +10 -2
  162. package/dist/utils/taskReferences.d.ts +11 -1
  163. package/dist/utils/taskReferences.js +33 -1
  164. package/package.json +4 -3
  165. package/scripts/qa-open-lib.mjs +175 -0
  166. package/scripts/qa-open-lib.mjs.d.ts +49 -0
  167. package/scripts/qa-open.mjs +352 -0
  168. package/dist/ui/assets/PlansPage-B6BDECSi.js +0 -1
  169. package/dist/ui/assets/TaskSettings-DUJgRTPU.js +0 -8
  170. package/dist/ui/assets/index-BiiF1cLQ.css +0 -1
  171. package/dist/ui/assets/index-xBWLPOEb.js +0 -6
@@ -0,0 +1,445 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { createHash } from 'crypto';
4
+ import { compareWorkspaceMutationOrdering } from './workspaceSyncSurface.js';
5
+ export function resolveSyncConflictOrdering(incomingWatermark, existingWatermark, incomingTieBreak, existingTieBreak) {
6
+ return compareWorkspaceMutationOrdering(String(incomingWatermark || '').trim(), String(existingWatermark || '').trim(), String(incomingTieBreak || '').trim(), String(existingTieBreak || '').trim());
7
+ }
8
+ export function applyPlanningSyncChanges(input) {
9
+ const { core, workspaceId, incomingInitiatives, incomingWorkstreams, incomingTaxonomies, incomingTaxonomyState, incomingTaxonomiesUpdatedAt, workspaceTaxonomySyncMeta, recordSyncDiagnostic, } = input;
10
+ if (incomingInitiatives.size > 0) {
11
+ for (const initiative of incomingInitiatives.values()) {
12
+ try {
13
+ core.upsertInitiativeForSync({
14
+ id: initiative.id,
15
+ referenceNumber: initiative.referenceNumber,
16
+ referenceLabel: initiative.referenceLabel,
17
+ title: initiative.title,
18
+ description: initiative.description,
19
+ ownerId: initiative.ownerId,
20
+ createdAt: initiative.createdAt,
21
+ updatedAt: initiative.updatedAt,
22
+ order: initiative.order,
23
+ isArchived: initiative.isArchived
24
+ }, workspaceId);
25
+ }
26
+ catch (error) {
27
+ recordSyncDiagnostic(workspaceId, 'apply', 'Failed to upsert initiative during sync apply.', {
28
+ source: 'apply.initiativeUpsert',
29
+ initiativeId: initiative.id,
30
+ error: String(error?.message || error || '')
31
+ });
32
+ throw error;
33
+ }
34
+ }
35
+ }
36
+ if (incomingWorkstreams.size > 0) {
37
+ for (const workstream of incomingWorkstreams.values()) {
38
+ try {
39
+ core.upsertWorkstreamForSync({
40
+ id: workstream.id,
41
+ referenceNumber: workstream.referenceNumber,
42
+ referenceLabel: workstream.referenceLabel,
43
+ initiativeId: workstream.initiativeId,
44
+ title: workstream.title,
45
+ description: workstream.description,
46
+ ownerId: workstream.ownerId,
47
+ createdAt: workstream.createdAt,
48
+ updatedAt: workstream.updatedAt,
49
+ order: workstream.order,
50
+ isArchived: workstream.isArchived
51
+ }, workspaceId);
52
+ }
53
+ catch (error) {
54
+ recordSyncDiagnostic(workspaceId, 'apply', 'Failed to upsert workstream during sync apply.', {
55
+ source: 'apply.workstreamUpsert',
56
+ workstreamId: workstream.id,
57
+ error: String(error?.message || error || '')
58
+ });
59
+ throw error;
60
+ }
61
+ }
62
+ }
63
+ if (incomingTaxonomyState || Array.isArray(incomingTaxonomies)) {
64
+ const nextTaxonomyState = incomingTaxonomyState || { taxonomies: incomingTaxonomies || [] };
65
+ if (!incomingTaxonomiesUpdatedAt) {
66
+ if (typeof core.updateTaxonomyState === 'function') {
67
+ core.updateTaxonomyState(nextTaxonomyState, workspaceId);
68
+ }
69
+ else {
70
+ core.updateTaxonomies(Array.isArray(incomingTaxonomies) ? incomingTaxonomies : []);
71
+ }
72
+ const fallbackHash = createHash('sha256').update(JSON.stringify(nextTaxonomyState)).digest('hex');
73
+ workspaceTaxonomySyncMeta.set(workspaceId, {
74
+ updatedAt: new Date().toISOString(),
75
+ hash: fallbackHash
76
+ });
77
+ }
78
+ else {
79
+ const incomingTaxonomyHash = createHash('sha256').update(JSON.stringify(nextTaxonomyState)).digest('hex');
80
+ const currentMeta = workspaceTaxonomySyncMeta.get(workspaceId) || { updatedAt: '', hash: '' };
81
+ const compare = resolveSyncConflictOrdering(incomingTaxonomiesUpdatedAt, currentMeta.updatedAt, incomingTaxonomyHash, currentMeta.hash);
82
+ if (compare >= 0) {
83
+ if (typeof core.updateTaxonomyState === 'function') {
84
+ core.updateTaxonomyState(nextTaxonomyState, workspaceId);
85
+ }
86
+ else {
87
+ core.updateTaxonomies(Array.isArray(incomingTaxonomies) ? incomingTaxonomies : []);
88
+ }
89
+ workspaceTaxonomySyncMeta.set(workspaceId, {
90
+ updatedAt: incomingTaxonomiesUpdatedAt,
91
+ hash: incomingTaxonomyHash
92
+ });
93
+ }
94
+ }
95
+ }
96
+ }
97
+ export function applyAiProfileAndMemberSyncChanges(input) {
98
+ const { core, workspaceId, incomingAiProfiles, workspaceMembers, recordSyncDiagnostic, assertSyncedAiProfilesPersisted, createSyncRouteError, } = input;
99
+ if (incomingAiProfiles.size > 0) {
100
+ for (const profile of incomingAiProfiles.values()) {
101
+ try {
102
+ core.upsertAiProfileForSync({
103
+ id: profile.id,
104
+ workspaceId,
105
+ profileToken: profile.profileToken,
106
+ name: profile.name,
107
+ username: profile.username,
108
+ icon: profile.icon,
109
+ color: profile.color,
110
+ surfaceType: profile.surfaceType,
111
+ providerMetadata: profile.providerMetadata,
112
+ createdAt: profile.createdAt,
113
+ updatedAt: profile.updatedAt,
114
+ lastActiveAt: profile.lastActiveAt
115
+ });
116
+ }
117
+ catch (error) {
118
+ const details = {
119
+ source: 'apply.aiProfileUpsert',
120
+ workspaceId,
121
+ profileId: profile.id,
122
+ error: String(error?.message || error || ''),
123
+ errorCode: String(error?.code || '').trim() || null
124
+ };
125
+ recordSyncDiagnostic(workspaceId, 'apply', 'Failed to upsert AI profile during sync apply.', details);
126
+ throw createSyncRouteError(`Failed to persist AI profile ${profile.id} during sync apply: ${String(error?.message || error || 'Unknown error')}`, details, { code: 'AI_PROFILE_SYNC_APPLY_FAILED', statusCode: 500 });
127
+ }
128
+ }
129
+ assertSyncedAiProfilesPersisted(workspaceId, incomingAiProfiles);
130
+ }
131
+ if (Array.isArray(workspaceMembers)) {
132
+ core.replaceWorkspaceUsersForSync({
133
+ workspaceId,
134
+ users: workspaceMembers
135
+ .filter((member) => String(member?.workspaceId || workspaceId).trim() === workspaceId)
136
+ .map((member) => ({
137
+ userId: member.userId,
138
+ email: member.email,
139
+ displayName: member.displayName,
140
+ role: member.role,
141
+ permissionMode: member.permissionMode,
142
+ betaAccess: member.betaAccess,
143
+ status: member.status,
144
+ disabled: member.disabled,
145
+ createdAt: member.createdAt,
146
+ updatedAt: member.updatedAt,
147
+ emailVerifiedAt: member.emailVerifiedAt
148
+ }))
149
+ });
150
+ }
151
+ }
152
+ export async function applyContentSyncChanges(input) {
153
+ const { core, workspaceId, incomingDocuments, incomingDocumentDeletes, incomingAssets, incomingAssetDeletes, basePath, hasObjectStorageClient, hasDocumentSyncMetadataDrift, hasBinaryAssetSyncMetadataDrift, writeWorkspaceSyncDocument, deleteWorkspaceSyncDocument, writeWorkspaceSyncBinaryAsset, deleteWorkspaceSyncBinaryAsset, getExistingBinaryAsset, recordSyncDiagnostic, options, } = input;
154
+ if (incomingDocuments.length > 0) {
155
+ for (const doc of incomingDocuments) {
156
+ try {
157
+ const existingDoc = core.getDocumentWatermark(workspaceId, doc.path);
158
+ const incomingHash = createHash('sha256').update(String(doc.content || '')).digest('hex');
159
+ const existingUpdatedAt = String(existingDoc?.updatedAt || '').trim();
160
+ const existingHash = String(existingDoc?.contentHash || '').trim();
161
+ const compare = resolveSyncConflictOrdering(doc.updatedAt, existingUpdatedAt, incomingHash, existingHash);
162
+ const metadataDrift = hasDocumentSyncMetadataDrift(workspaceId, doc, options);
163
+ if (compare < 0)
164
+ continue;
165
+ if (compare === 0 && existingHash === incomingHash && !metadataDrift)
166
+ continue;
167
+ await writeWorkspaceSyncDocument(basePath, doc, workspaceId, options);
168
+ }
169
+ catch (error) {
170
+ recordSyncDiagnostic(workspaceId, 'apply', 'Failed to apply synced document change.', {
171
+ source: 'documents.apply.write',
172
+ path: doc.path,
173
+ assetId: typeof doc.assetId === 'string' ? doc.assetId : null,
174
+ error: String(error?.message || error || '')
175
+ });
176
+ }
177
+ }
178
+ }
179
+ if (incomingDocumentDeletes.size > 0) {
180
+ for (const [docPath, deletedAt] of incomingDocumentDeletes.entries()) {
181
+ const existingDoc = core.getDocumentWatermark(workspaceId, docPath);
182
+ const existingUpdatedAt = String(existingDoc?.updatedAt || '').trim();
183
+ const existingHash = String(existingDoc?.contentHash || '').trim();
184
+ const compare = resolveSyncConflictOrdering(deletedAt, existingUpdatedAt, `delete:${docPath}`, existingHash);
185
+ if (existingDoc && compare < 0)
186
+ continue;
187
+ deleteWorkspaceSyncDocument(basePath, docPath, workspaceId);
188
+ }
189
+ }
190
+ if (incomingAssets.length > 0) {
191
+ for (const asset of incomingAssets) {
192
+ try {
193
+ const existing = getExistingBinaryAsset(asset.path, workspaceId);
194
+ const incomingHash = createHash('sha256').update(Buffer.from(String(asset.contentBase64 || ''), 'base64')).digest('hex');
195
+ const existingUpdatedAt = String(existing?.updatedAt || '').trim();
196
+ const existingHash = String(existing?.contentSha256 || '').trim();
197
+ const localFileMissing = !hasObjectStorageClient() && !fs.existsSync(path.resolve(basePath, asset.path));
198
+ const compare = resolveSyncConflictOrdering(asset.updatedAt, existingUpdatedAt, incomingHash, existingHash);
199
+ const metadataDrift = hasBinaryAssetSyncMetadataDrift(workspaceId, asset, options);
200
+ if (compare < 0)
201
+ continue;
202
+ if (compare === 0 && existingHash === incomingHash && !localFileMissing && !metadataDrift)
203
+ continue;
204
+ await writeWorkspaceSyncBinaryAsset(basePath, asset, workspaceId, options);
205
+ }
206
+ catch (error) {
207
+ recordSyncDiagnostic(workspaceId, 'apply', 'Failed to apply synced asset change.', {
208
+ source: 'assets.apply.write',
209
+ path: asset.path,
210
+ assetId: typeof asset.assetId === 'string' ? asset.assetId : null,
211
+ kind: asset.kind,
212
+ error: String(error?.message || error || '')
213
+ });
214
+ }
215
+ }
216
+ }
217
+ if (incomingAssetDeletes.size > 0) {
218
+ for (const [assetPath, deletedAt] of incomingAssetDeletes.entries()) {
219
+ const existing = getExistingBinaryAsset(assetPath, workspaceId);
220
+ const existingUpdatedAt = String(existing?.updatedAt || '').trim();
221
+ const existingHash = String(existing?.contentSha256 || '').trim();
222
+ const compare = resolveSyncConflictOrdering(deletedAt, existingUpdatedAt, `delete:${assetPath}`, existingHash);
223
+ if (existing && compare < 0)
224
+ continue;
225
+ deleteWorkspaceSyncBinaryAsset(basePath, assetPath, workspaceId);
226
+ }
227
+ }
228
+ if (incomingDocuments.length > 0 || incomingAssets.length > 0) {
229
+ core.notifyDocumentMetadataMutation(workspaceId, [
230
+ ...incomingDocuments.map((entry) => entry.path),
231
+ ...incomingAssets.map((entry) => entry.path)
232
+ ], 'upsert');
233
+ }
234
+ if (incomingDocumentDeletes.size > 0 || incomingAssetDeletes.size > 0) {
235
+ core.notifyDocumentMetadataMutation(workspaceId, [
236
+ ...Array.from(incomingDocumentDeletes.keys()),
237
+ ...Array.from(incomingAssetDeletes.keys())
238
+ ], 'delete');
239
+ }
240
+ }
241
+ export function applyCollaborationSyncChanges(input) {
242
+ const { core, workspaceId, incomingDocumentReviewSessions, incomingDocumentReviewSessionDeletes, incomingDocumentReviewComments, incomingDocumentReviewCommentDeletes, incomingAnnotatedAttachmentSessions, incomingAnnotatedAttachmentSessionDeletes, recordSyncDiagnostic, createSyncRouteError, } = input;
243
+ if (incomingDocumentReviewSessions.size > 0) {
244
+ for (const session of incomingDocumentReviewSessions.values()) {
245
+ try {
246
+ const existing = core.getDocumentReviewSession({
247
+ workspaceId,
248
+ sessionId: session.id,
249
+ includeDeleted: true
250
+ });
251
+ const incomingTieBreak = createHash('sha256').update(JSON.stringify(session.comments || [])).digest('hex');
252
+ const existingTieBreak = existing
253
+ ? createHash('sha256').update(JSON.stringify(existing.comments || [])).digest('hex')
254
+ : '';
255
+ const compare = resolveSyncConflictOrdering(session.updatedAt, String(existing?.updatedAt || '').trim(), incomingTieBreak, existingTieBreak);
256
+ if (compare < 0)
257
+ continue;
258
+ core.upsertDocumentReviewSessionForSync({
259
+ id: session.id,
260
+ tenantId: core.getTenantId(),
261
+ workspaceId,
262
+ assetId: session.assetId,
263
+ documentId: session.documentId,
264
+ documentVersion: session.documentVersion,
265
+ title: session.title,
266
+ status: session.status,
267
+ comments: [],
268
+ createdByActorId: session.createdByActorId,
269
+ updatedByActorId: session.updatedByActorId,
270
+ createdAt: session.createdAt,
271
+ updatedAt: session.updatedAt,
272
+ deletedAt: session.deletedAt,
273
+ });
274
+ }
275
+ catch (error) {
276
+ const details = {
277
+ source: 'apply.documentReviewUpsert',
278
+ sessionId: session.id,
279
+ error: String(error?.message || error || ''),
280
+ errorCode: String(error?.code || '').trim() || null
281
+ };
282
+ recordSyncDiagnostic(workspaceId, 'apply', 'Failed to upsert document review session during sync apply.', details);
283
+ throw createSyncRouteError(`Failed to persist document review session ${session.id} during sync apply: ${String(error?.message || error || 'Unknown error')}`, details, { code: 'DOCUMENT_REVIEW_SESSION_SYNC_APPLY_FAILED', statusCode: 500 });
284
+ }
285
+ }
286
+ }
287
+ if (incomingDocumentReviewComments.size > 0) {
288
+ for (const comment of incomingDocumentReviewComments.values()) {
289
+ try {
290
+ const existingComments = core.listDocumentReviewCommentsForSync({
291
+ workspaceId,
292
+ includeDeleted: true
293
+ });
294
+ const existing = existingComments.find((entry) => entry.id === comment.id) || null;
295
+ const incomingTieBreak = createHash('sha256')
296
+ .update(JSON.stringify({ body: comment.body, anchor: comment.anchor, deletedAt: comment.deletedAt }))
297
+ .digest('hex');
298
+ const existingTieBreak = existing
299
+ ? createHash('sha256')
300
+ .update(JSON.stringify({ body: existing.body, anchor: existing.anchor, deletedAt: existing.deletedAt }))
301
+ .digest('hex')
302
+ : '';
303
+ const compare = resolveSyncConflictOrdering(comment.updatedAt, String(existing?.updatedAt || '').trim(), incomingTieBreak, existingTieBreak);
304
+ if (compare < 0)
305
+ continue;
306
+ core.upsertDocumentReviewCommentForSync({
307
+ id: comment.id,
308
+ tenantId: core.getTenantId(),
309
+ workspaceId,
310
+ sessionId: comment.sessionId,
311
+ body: comment.body,
312
+ anchor: comment.anchor,
313
+ order: comment.order,
314
+ authorActorId: comment.authorActorId,
315
+ createdAt: comment.createdAt,
316
+ updatedAt: comment.updatedAt,
317
+ deletedAt: comment.deletedAt,
318
+ });
319
+ }
320
+ catch (error) {
321
+ recordSyncDiagnostic(workspaceId, 'apply', 'Failed to upsert document review comment during sync apply.', {
322
+ source: 'apply.documentReviewCommentUpsert',
323
+ commentId: comment.id,
324
+ error: String(error?.message || error || '')
325
+ });
326
+ throw error;
327
+ }
328
+ }
329
+ }
330
+ if (incomingDocumentReviewSessionDeletes.size > 0) {
331
+ for (const [sessionId, deletedAt] of incomingDocumentReviewSessionDeletes.entries()) {
332
+ try {
333
+ const existing = core.getDocumentReviewSession({
334
+ workspaceId,
335
+ sessionId,
336
+ includeDeleted: true
337
+ });
338
+ const compare = resolveSyncConflictOrdering(deletedAt, String(existing?.updatedAt || '').trim(), `delete:${sessionId}`, String(existing?.id || '').trim());
339
+ if (existing && compare < 0)
340
+ continue;
341
+ core.deleteDocumentReviewSessionForSync({ workspaceId, sessionId, deletedAt });
342
+ }
343
+ catch (error) {
344
+ const details = {
345
+ source: 'apply.documentReviewDelete',
346
+ sessionId,
347
+ error: String(error?.message || error || ''),
348
+ errorCode: String(error?.code || '').trim() || null
349
+ };
350
+ recordSyncDiagnostic(workspaceId, 'apply', 'Failed to delete document review session during sync apply.', details);
351
+ throw createSyncRouteError(`Failed to delete document review session ${sessionId} during sync apply: ${String(error?.message || error || 'Unknown error')}`, details, { code: 'DOCUMENT_REVIEW_SESSION_SYNC_DELETE_FAILED', statusCode: 500 });
352
+ }
353
+ }
354
+ }
355
+ if (incomingDocumentReviewCommentDeletes.size > 0) {
356
+ for (const [commentId, deletedAt] of incomingDocumentReviewCommentDeletes.entries()) {
357
+ try {
358
+ const existing = core.listDocumentReviewCommentsForSync({
359
+ workspaceId,
360
+ includeDeleted: true
361
+ }).find((entry) => entry.id === commentId) || null;
362
+ const compare = resolveSyncConflictOrdering(deletedAt, String(existing?.updatedAt || '').trim(), `delete:${commentId}`, String(existing?.id || '').trim());
363
+ if (existing && compare < 0)
364
+ continue;
365
+ core.deleteDocumentReviewCommentForSync({ workspaceId, commentId, deletedAt });
366
+ }
367
+ catch (error) {
368
+ recordSyncDiagnostic(workspaceId, 'apply', 'Failed to delete document review comment during sync apply.', {
369
+ source: 'apply.documentReviewCommentDelete',
370
+ commentId,
371
+ error: String(error?.message || error || '')
372
+ });
373
+ throw error;
374
+ }
375
+ }
376
+ }
377
+ if (incomingAnnotatedAttachmentSessions.size > 0) {
378
+ for (const session of incomingAnnotatedAttachmentSessions.values()) {
379
+ try {
380
+ const existing = core.getAnnotatedAttachmentSession({
381
+ workspaceId,
382
+ sessionId: session.id,
383
+ includeDeleted: true
384
+ });
385
+ const incomingTieBreak = createHash('sha256').update(JSON.stringify(session.annotations || [])).digest('hex');
386
+ const existingTieBreak = existing
387
+ ? createHash('sha256').update(JSON.stringify(existing.annotations || [])).digest('hex')
388
+ : '';
389
+ const compare = resolveSyncConflictOrdering(session.updatedAt, String(existing?.updatedAt || '').trim(), incomingTieBreak, existingTieBreak);
390
+ if (compare < 0)
391
+ continue;
392
+ core.upsertAnnotatedAttachmentSessionForSync({
393
+ id: session.id,
394
+ tenantId: core.getTenantId(),
395
+ workspaceId,
396
+ taskId: session.taskId,
397
+ baseImageAssetId: session.baseImageAssetId,
398
+ title: session.title,
399
+ globalInstruction: session.globalInstruction,
400
+ annotations: session.annotations,
401
+ createdByActorId: session.createdByActorId,
402
+ updatedByActorId: session.updatedByActorId,
403
+ createdAt: session.createdAt,
404
+ updatedAt: session.updatedAt,
405
+ deletedAt: session.deletedAt,
406
+ });
407
+ }
408
+ catch (error) {
409
+ const details = {
410
+ source: 'apply.annotatedAttachmentUpsert',
411
+ sessionId: session.id,
412
+ error: String(error?.message || error || ''),
413
+ errorCode: String(error?.code || '').trim() || null
414
+ };
415
+ recordSyncDiagnostic(workspaceId, 'apply', 'Failed to upsert annotated attachment session during sync apply.', details);
416
+ throw createSyncRouteError(`Failed to persist annotated attachment session ${session.id} during sync apply: ${String(error?.message || error || 'Unknown error')}`, details, { code: 'ANNOTATED_ATTACHMENT_SESSION_SYNC_APPLY_FAILED', statusCode: 500 });
417
+ }
418
+ }
419
+ }
420
+ if (incomingAnnotatedAttachmentSessionDeletes.size > 0) {
421
+ for (const [sessionId, deletedAt] of incomingAnnotatedAttachmentSessionDeletes.entries()) {
422
+ try {
423
+ const existing = core.getAnnotatedAttachmentSession({
424
+ workspaceId,
425
+ sessionId,
426
+ includeDeleted: true
427
+ });
428
+ const compare = resolveSyncConflictOrdering(deletedAt, String(existing?.updatedAt || '').trim(), `delete:${sessionId}`, String(existing?.id || '').trim());
429
+ if (existing && compare < 0)
430
+ continue;
431
+ core.deleteAnnotatedAttachmentSessionForSync({ workspaceId, sessionId, deletedAt });
432
+ }
433
+ catch (error) {
434
+ const details = {
435
+ source: 'apply.annotatedAttachmentDelete',
436
+ sessionId,
437
+ error: String(error?.message || error || ''),
438
+ errorCode: String(error?.code || '').trim() || null
439
+ };
440
+ recordSyncDiagnostic(workspaceId, 'apply', 'Failed to delete annotated attachment session during sync apply.', details);
441
+ throw createSyncRouteError(`Failed to delete annotated attachment session ${sessionId} during sync apply: ${String(error?.message || error || 'Unknown error')}`, details, { code: 'ANNOTATED_ATTACHMENT_SESSION_SYNC_DELETE_FAILED', statusCode: 500 });
442
+ }
443
+ }
444
+ }
445
+ }
@@ -115,6 +115,7 @@ export interface WorkspacePullSyncResult {
115
115
  pulledArchivedUpserts: boolean;
116
116
  documentDecryptFailures?: string[];
117
117
  emittedEventIds?: string[];
118
+ serverDiagnostics?: Record<string, unknown> | null;
118
119
  }
119
120
  export declare function runWorkspacePullSyncService(input: WorkspacePullSyncInput): Promise<WorkspacePullSyncResult>;
120
121
  export declare function syncUserGlobalSettingsService(input: UserSettingsSyncInput): Promise<UserSettingsSyncResult>;
@@ -243,6 +243,7 @@ export async function runWorkspacePullSyncService(input) {
243
243
  const documentDecryptFailures = new Set();
244
244
  const emittedEventIds = new Set();
245
245
  const pulledAnnotatedAttachmentSessionIds = new Set();
246
+ let lastServerDiagnostics = null;
246
247
  const maxPages = Number.isFinite(Number(input.maxPages)) ? Math.max(1, Math.floor(Number(input.maxPages))) : 20;
247
248
  try {
248
249
  while (hasMore && pages < maxPages) {
@@ -279,11 +280,16 @@ export async function runWorkspacePullSyncService(input) {
279
280
  pulledDeleteTaskIds,
280
281
  pulledActiveUpserts,
281
282
  pulledArchivedUpserts,
282
- documentDecryptFailures: Array.from(documentDecryptFailures)
283
+ documentDecryptFailures: Array.from(documentDecryptFailures),
284
+ serverDiagnostics: null
283
285
  };
284
286
  }
285
287
  const pullData = pullRes.data || {};
286
288
  const changes = Array.isArray(pullData?.changes) ? pullData.changes : [];
289
+ const serverDiagnostics = pullData?.diagnostics && typeof pullData.diagnostics === 'object'
290
+ ? pullData.diagnostics
291
+ : null;
292
+ lastServerDiagnostics = serverDiagnostics;
287
293
  const hasWorkspaceMembersSnapshot = Array.isArray(pullData?.workspaceMembers);
288
294
  const workspaceMembers = hasWorkspaceMembersSnapshot
289
295
  ? pullData.workspaceMembers
@@ -365,7 +371,8 @@ export async function runWorkspacePullSyncService(input) {
365
371
  pulledDeleteTaskIds,
366
372
  pulledActiveUpserts,
367
373
  pulledArchivedUpserts,
368
- documentDecryptFailures: Array.from(documentDecryptFailures)
374
+ documentDecryptFailures: Array.from(documentDecryptFailures),
375
+ serverDiagnostics
369
376
  };
370
377
  }
371
378
  const hasTransientApplyFailure = applyFailures.some((entry) => {
@@ -392,7 +399,8 @@ export async function runWorkspacePullSyncService(input) {
392
399
  pulledActiveUpserts,
393
400
  pulledArchivedUpserts,
394
401
  documentDecryptFailures: Array.from(documentDecryptFailures),
395
- emittedEventIds: Array.from(emittedEventIds)
402
+ emittedEventIds: Array.from(emittedEventIds),
403
+ serverDiagnostics: lastServerDiagnostics
396
404
  };
397
405
  }
398
406
  const applyPayloadData = asObjectRecord(applyResult.data);
@@ -412,7 +420,8 @@ export async function runWorkspacePullSyncService(input) {
412
420
  pulledActiveUpserts,
413
421
  pulledArchivedUpserts,
414
422
  documentDecryptFailures: Array.from(documentDecryptFailures),
415
- emittedEventIds: Array.from(emittedEventIds)
423
+ emittedEventIds: Array.from(emittedEventIds),
424
+ serverDiagnostics: lastServerDiagnostics
416
425
  };
417
426
  }
418
427
  applyMs += (Date.now() - applyStartedAtMs);
@@ -461,7 +470,8 @@ export async function runWorkspacePullSyncService(input) {
461
470
  pulledActiveUpserts,
462
471
  pulledArchivedUpserts,
463
472
  documentDecryptFailures: Array.from(documentDecryptFailures),
464
- emittedEventIds: Array.from(emittedEventIds)
473
+ emittedEventIds: Array.from(emittedEventIds),
474
+ serverDiagnostics: lastServerDiagnostics
465
475
  };
466
476
  }
467
477
  const hasTransientApplyFailure = bootstrapApplyFailures.some((entry) => {
@@ -488,7 +498,8 @@ export async function runWorkspacePullSyncService(input) {
488
498
  pulledActiveUpserts,
489
499
  pulledArchivedUpserts,
490
500
  documentDecryptFailures: Array.from(documentDecryptFailures),
491
- emittedEventIds: Array.from(emittedEventIds)
501
+ emittedEventIds: Array.from(emittedEventIds),
502
+ serverDiagnostics: lastServerDiagnostics
492
503
  };
493
504
  }
494
505
  applyMs += (Date.now() - bootstrapApplyStartedAtMs);
@@ -505,7 +516,8 @@ export async function runWorkspacePullSyncService(input) {
505
516
  pulledActiveUpserts,
506
517
  pulledArchivedUpserts,
507
518
  documentDecryptFailures: Array.from(documentDecryptFailures),
508
- emittedEventIds: Array.from(emittedEventIds)
519
+ emittedEventIds: Array.from(emittedEventIds),
520
+ serverDiagnostics: lastServerDiagnostics
509
521
  };
510
522
  }
511
523
  catch (error) {
@@ -524,7 +536,8 @@ export async function runWorkspacePullSyncService(input) {
524
536
  pulledActiveUpserts,
525
537
  pulledArchivedUpserts,
526
538
  documentDecryptFailures: Array.from(documentDecryptFailures),
527
- emittedEventIds: Array.from(emittedEventIds)
539
+ emittedEventIds: Array.from(emittedEventIds),
540
+ serverDiagnostics: null
528
541
  };
529
542
  }
530
543
  }
@@ -0,0 +1,33 @@
1
+ import type { AttachmentItem, ChecklistItem, Comment, TaskItem, TaskStatus } from '../types.js';
2
+ import type { TaskItem as CoreTaskItem } from '../core/Taskforce.js';
3
+ export interface TaskSyncPayload {
4
+ id: string;
5
+ title: string;
6
+ description?: string | null;
7
+ status: TaskStatus;
8
+ priority?: number;
9
+ complexity?: number | null;
10
+ type: string;
11
+ category: string;
12
+ approach?: string;
13
+ canceledReason?: string | null;
14
+ createdAt: string;
15
+ updatedAt?: string;
16
+ completedAt?: string | null;
17
+ isArchived?: boolean;
18
+ taxonomies?: Record<string, string | string[]>;
19
+ createdBy?: string;
20
+ assignee?: string;
21
+ scheduledDate?: string | null;
22
+ dueDate?: string | null;
23
+ scheduledWeekKey?: string | null;
24
+ orderInDay?: number | null;
25
+ workstreamId?: string | null;
26
+ referenceNumber?: number | null;
27
+ comments?: Comment[];
28
+ attachments?: Array<string | AttachmentItem>;
29
+ checklistItems?: ChecklistItem[];
30
+ }
31
+ export declare function serializeTaskForSync(task: Partial<TaskItem> | null | undefined): TaskSyncPayload;
32
+ export declare function normalizeTaskSyncPayload(raw: unknown): TaskSyncPayload | null;
33
+ export declare function taskSyncPayloadToCoreTask(task: TaskSyncPayload): Partial<CoreTaskItem>;