@taskforcehq/taskforce 0.3.300 → 0.3.302
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/README.md +19 -0
- package/dist/Taskforce.module.css +26 -2
- package/dist/TaskforceCore.filter-persistence.test.js +1 -1
- package/dist/TaskforceCore.js +13 -24
- package/dist/TaskforceCore.test.js +167 -152
- package/dist/components/features/TaskSettings.js +14 -82
- package/dist/components/features/TaskSettings.test.js +33 -70
- package/dist/components/features/settings/TaxonomyLibraryManager.js +8 -2
- package/dist/components/features/settings/TaxonomyLibraryManager.test.js +1 -1
- package/dist/components/task/TaskActionHeader.js +6 -3
- package/dist/components/task/TaskCard.js +6 -3
- package/dist/components/task/TaskForm.js +2 -2
- package/dist/components/task/TaskForm.test.js +41 -2
- package/dist/components/ui/ReferenceBadgeButton.js +1 -1
- package/dist/components/views/PlansPage.js +9 -8
- package/dist/components/views/StandaloneLayout.js +490 -207
- package/dist/components/views/panels/FilterToolbar.d.ts +2 -0
- package/dist/components/views/panels/FilterToolbarControls.d.ts +1 -0
- package/dist/components/views/panels/FilterToolbarControls.js +1 -1
- package/dist/components/views/panels/PlanningDrawer.js +19 -2
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +43 -0
- package/dist/components/views/standalone/modals/AccountHubModal.js +22 -0
- package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.d.ts +9 -0
- package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.js +8 -0
- package/dist/components/views/standalone/modals/EditProfileModal.d.ts +26 -0
- package/dist/components/views/standalone/modals/EditProfileModal.js +10 -0
- package/dist/components/views/standalone/modals/HelpModal.d.ts +9 -0
- package/dist/components/views/standalone/modals/HelpModal.js +8 -0
- package/dist/components/views/standalone/modals/ScheduleWarningModal.d.ts +13 -0
- package/dist/components/views/standalone/modals/ScheduleWarningModal.js +6 -0
- package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +61 -0
- package/dist/components/views/standalone/modals/SyncStatusModal.js +38 -0
- package/dist/components/views/standalone/modals/TeamManagementModal.d.ts +64 -0
- package/dist/components/views/standalone/modals/TeamManagementModal.js +10 -0
- package/dist/components/views/standalone/modals/types.d.ts +2 -0
- package/dist/components/views/standalone/modals/types.js +1 -0
- package/dist/components/views/teamManagementAccess.d.ts +1 -1
- package/dist/components/views/teamManagementAccess.js +2 -2
- package/dist/components/views/teamManagementAccess.test.js +3 -3
- package/dist/config/deployment.js +2 -2
- package/dist/config/deployment.test.js +5 -3
- package/dist/core/AiProfileService.js +7 -2
- package/dist/core/AiProfiles.test.js +19 -0
- package/dist/core/SyncReconciliationService.d.ts +2 -0
- package/dist/core/SyncReconciliationService.js +3 -0
- package/dist/core/TaskAuditTimeline.test.js +40 -0
- package/dist/core/Taskforce.d.ts +49 -0
- package/dist/core/Taskforce.ids.test.js +133 -0
- package/dist/core/Taskforce.js +241 -78
- package/dist/core/UserProfileAvatarDrafts.test.d.ts +1 -0
- package/dist/core/UserProfileAvatarDrafts.test.js +102 -0
- package/dist/core/taskReferencePolicy.d.ts +28 -0
- package/dist/core/taskReferencePolicy.js +189 -0
- package/dist/core/types.d.ts +2 -0
- package/dist/hooks/tasks/useTaskFormActions.js +3 -0
- package/dist/hooks/useTaskData.js +16 -11
- package/dist/hooks/useTaskData.test.js +127 -0
- package/dist/hooks/useTaskforce.d.ts +5 -0
- package/dist/hooks/useTaskforce.js +63 -15
- package/dist/hooks/useTaskforce.sync-behavior.test.js +185 -6
- package/dist/mcp/aiProfileBootstrap.d.ts +29 -0
- package/dist/mcp/aiProfileBootstrap.js +80 -0
- package/dist/mcp/aiProfileBootstrap.test.d.ts +1 -0
- package/dist/mcp/aiProfileBootstrap.test.js +68 -0
- package/dist/mcp/clientIntegrations.d.ts +2 -8
- package/dist/mcp/clientIntegrations.js +21 -40
- package/dist/mcp/clientIntegrations.test.js +28 -2
- package/dist/mcp/clientProfileDefaults.d.ts +12 -0
- package/dist/mcp/clientProfileDefaults.js +37 -0
- package/dist/mcp/runtime.d.ts +10 -7
- package/dist/mcp/runtime.js +45 -23
- package/dist/mcp/runtime.test.js +56 -3
- package/dist/migrations/taskSchemaMigrations.js +53 -1
- package/dist/qaOpen.test.d.ts +1 -0
- package/dist/qaOpen.test.js +56 -0
- package/dist/server/index.cookieProxy.test.js +1 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +15 -1
- package/dist/server/index.test.js +14 -1
- package/dist/server/routes/admin.js +25 -22
- package/dist/server/routes/auth.d.ts +5 -0
- package/dist/server/routes/auth.js +350 -16
- package/dist/server/routes/billing.js +11 -1
- package/dist/server/routes/billing.test.js +32 -2
- package/dist/server/routes/documents.js +6 -1
- package/dist/server/routes/shared.d.ts +1 -1
- package/dist/server/routes/shared.js +11 -3
- package/dist/server/routes/sync.integration.test.js +130 -0
- package/dist/server/routes/sync.js +226 -1924
- package/dist/server/routes/tasks.js +18 -4
- package/dist/server/routes.js +26 -19
- package/dist/server/routes.test.js +352 -6
- package/dist/shared/runtimeContract.d.ts +1 -1
- package/dist/sync/collaborationSyncPayload.d.ts +17 -0
- package/dist/sync/collaborationSyncPayload.js +131 -0
- package/dist/sync/collaborationSyncPayload.test.d.ts +1 -0
- package/dist/sync/collaborationSyncPayload.test.js +104 -0
- package/dist/sync/collaborationSyncState.d.ts +15 -0
- package/dist/sync/collaborationSyncState.js +60 -0
- package/dist/sync/contentSyncPayload.d.ts +45 -0
- package/dist/sync/contentSyncPayload.js +104 -0
- package/dist/sync/contentSyncPayload.test.d.ts +1 -0
- package/dist/sync/contentSyncPayload.test.js +104 -0
- package/dist/sync/contentSyncState.d.ts +88 -0
- package/dist/sync/contentSyncState.js +743 -0
- package/dist/sync/planningSyncPayload.d.ts +9 -0
- package/dist/sync/planningSyncPayload.js +82 -0
- package/dist/sync/planningSyncPayload.test.d.ts +1 -0
- package/dist/sync/planningSyncPayload.test.js +68 -0
- package/dist/sync/syncApplyHandlers.d.ts +83 -0
- package/dist/sync/syncApplyHandlers.js +445 -0
- package/dist/sync/taskSyncPayload.d.ts +33 -0
- package/dist/sync/taskSyncPayload.js +126 -0
- package/dist/sync/taskSyncPayload.test.d.ts +1 -0
- package/dist/sync/taskSyncPayload.test.js +54 -0
- package/dist/sync/workspacePullFeed.d.ts +28 -0
- package/dist/sync/workspacePullFeed.js +301 -0
- package/dist/sync/workspacePullFeed.test.d.ts +1 -0
- package/dist/sync/workspacePullFeed.test.js +166 -0
- package/dist/sync/workspaceSyncModel.d.ts +4 -1
- package/dist/sync/workspaceSyncModel.js +9 -2
- package/dist/sync/workspaceSyncModel.test.js +29 -0
- package/dist/test/setup.js +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/ui/assets/{AgentsModule-TVmCgizS.js → AgentsModule-JG5jc3he.js} +1 -1
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-CAyhnPeR.js → AnnotatedAttachmentWorkspace-Dvi02Y7q.js} +1 -1
- package/dist/ui/assets/{DocumentWorkspace-BOCw5e4D.js → DocumentWorkspace-iys3zfca.js} +2 -2
- package/dist/ui/assets/{InitiativesModule-CN0r02Oi.js → InitiativesModule-DJvLJI3A.js} +1 -1
- package/dist/ui/assets/PlansPage-BMPlpFpw.js +1 -0
- package/dist/ui/assets/TaskSettings-Bqd4cCzW.js +8 -0
- package/dist/ui/assets/{TaskSettings-BwB9NtmJ.css → TaskSettings-CnIBL_Eb.css} +1 -1
- package/dist/ui/assets/{WorkflowsModule-CY9R2zPB.js → WorkflowsModule-BguHeQaV.js} +1 -1
- package/dist/ui/assets/index-MXeWoebC.css +1 -0
- package/dist/ui/assets/index-YJ4k5QNa.js +6 -0
- package/dist/ui/assets/{vendor-icons-QZyhEwgT.js → vendor-icons-C8EED3Oh.js} +1 -1
- package/dist/ui/index.html +3 -3
- package/dist/utils/billingStatusCache.d.ts +21 -0
- package/dist/utils/billingStatusCache.js +70 -0
- package/dist/utils/taskActivity.js +17 -4
- package/dist/utils/taskActivity.test.js +27 -0
- package/dist/utils/taskNormalization.js +10 -2
- package/dist/utils/taskReferences.d.ts +11 -1
- package/dist/utils/taskReferences.js +33 -1
- package/package.json +4 -1
- package/scripts/deploy-prod.sh +41 -0
- package/scripts/deploy-staging.sh +23 -0
- package/scripts/qa-open-lib.mjs +175 -0
- package/scripts/qa-open-lib.mjs.d.ts +49 -0
- package/scripts/qa-open.mjs +352 -0
- package/dist/ui/assets/PlansPage-FcQSNLZd.js +0 -1
- package/dist/ui/assets/TaskSettings-Cw5zYD1L.js +0 -8
- package/dist/ui/assets/index-Bzqy5QGm.js +0 -6
- package/dist/ui/assets/index-DxRiI3tY.css +0 -1
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
1
|
import * as path from 'path';
|
|
3
|
-
import { createHash, randomUUID } from 'crypto';
|
|
4
2
|
import { parseJsonBody } from '../routes.js';
|
|
5
3
|
import { normalizeTaskDataRelativePath, ObjectStorageClient } from '../../storage/objectStorageClient.js';
|
|
6
|
-
import { WorkspaceAssetStore } from '../../storage/workspaceAssetStore.js';
|
|
4
|
+
import { WorkspaceAssetStore, } from '../../storage/workspaceAssetStore.js';
|
|
7
5
|
import { resolveSessionCookieAttributesForRequest } from '../auth.js';
|
|
8
6
|
import { decryptDocument, ENCRYPTION_PREFIX } from '../../sync/encryptionService.js';
|
|
9
7
|
import { isAiProfileSurfaceType } from '../../shared/aiProfileSurfaceType.js';
|
|
10
|
-
import {
|
|
8
|
+
import { getTaskMutationTieBreak, getTaskMutationWatermark } from '../../sync/workspaceSyncSurface.js';
|
|
9
|
+
import { normalizeTaskSyncPayload, taskSyncPayloadToCoreTask } from '../../sync/taskSyncPayload.js';
|
|
10
|
+
import { normalizeBinaryAssetDeleteSyncPayload, normalizeBinaryAssetSyncPayload, normalizeDocumentDeleteSyncPayload, normalizeDocumentSyncPayload, serializeBinaryAssetSyncPayload, serializeDocumentSyncPayload } from '../../sync/contentSyncPayload.js';
|
|
11
|
+
import { createContentSyncState } from '../../sync/contentSyncState.js';
|
|
12
|
+
import { normalizeAiProfileSyncPayload, normalizeAnnotatedAttachmentSessionSyncPayload, normalizeDeleteSyncPayload, normalizeDocumentReviewCommentSyncPayload, normalizeDocumentReviewSessionSyncPayload, } from '../../sync/collaborationSyncPayload.js';
|
|
13
|
+
import { createCollaborationSyncState } from '../../sync/collaborationSyncState.js';
|
|
14
|
+
import { buildWorkspacePullFeed } from '../../sync/workspacePullFeed.js';
|
|
15
|
+
import { normalizeInitiativeSyncPayload, normalizeTaxonomySyncPayload, normalizeWorkstreamSyncPayload, } from '../../sync/planningSyncPayload.js';
|
|
16
|
+
import { applyAiProfileAndMemberSyncChanges, applyCollaborationSyncChanges, applyContentSyncChanges, applyPlanningSyncChanges, resolveSyncConflictOrdering, } from '../../sync/syncApplyHandlers.js';
|
|
11
17
|
import { applyWorkspaceRepair } from '../../sync/workspaceRepair.js';
|
|
12
18
|
function decodeWorkspaceSyncCursor(raw) {
|
|
13
19
|
const value = String(raw || '').trim();
|
|
@@ -174,790 +180,38 @@ export function registerSyncRoutes(deps) {
|
|
|
174
180
|
return normalized;
|
|
175
181
|
return null;
|
|
176
182
|
};
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
effectiveUpdatedAt = linkUpdatedAt;
|
|
192
|
-
}
|
|
193
|
-
return { primaryLink, effectiveUpdatedAt };
|
|
194
|
-
};
|
|
195
|
-
const getCanonicalSyncDocumentMetadata = (relativePath, workspaceId) => {
|
|
196
|
-
if (!workspaceAssetStore)
|
|
197
|
-
return {};
|
|
198
|
-
const asset = workspaceAssetStore.getByStorageKey(relativePath, workspaceId);
|
|
199
|
-
if (!asset || asset.kind !== 'document')
|
|
200
|
-
return {};
|
|
201
|
-
const { primaryLink } = resolveCanonicalAssetSyncLinkState(asset, workspaceId);
|
|
202
|
-
return {
|
|
203
|
-
assetId: asset.assetId,
|
|
204
|
-
documentId: asset.documentId,
|
|
205
|
-
referenceNumber: asset.referenceNumber,
|
|
206
|
-
version: asset.version,
|
|
207
|
-
taskId: primaryLink?.taskId || null,
|
|
208
|
-
logicalName: asset.logicalName || null,
|
|
209
|
-
caption: primaryLink?.displayName || null,
|
|
210
|
-
originalFilename: asset.originalFilename || path.basename(relativePath),
|
|
211
|
-
linkRole: primaryLink?.role === 'reference' ? 'reference' : 'attachment'
|
|
212
|
-
};
|
|
213
|
-
};
|
|
214
|
-
const hasDocumentSyncMetadataDrift = (workspaceId, doc, options) => {
|
|
215
|
-
if (!workspaceAssetStore)
|
|
216
|
-
return false;
|
|
217
|
-
const existingAsset = workspaceAssetStore.getByStorageKey(doc.path, workspaceId);
|
|
218
|
-
if (!existingAsset || existingAsset.kind !== 'document')
|
|
219
|
-
return false;
|
|
220
|
-
const { primaryLink } = resolveCanonicalAssetSyncLinkState(existingAsset, workspaceId);
|
|
221
|
-
const normalizedIncomingReference = Number.isFinite(Number(doc.referenceNumber))
|
|
222
|
-
? Math.max(1, Math.floor(Number(doc.referenceNumber)))
|
|
223
|
-
: null;
|
|
224
|
-
const normalizedIncomingVersion = Number.isFinite(Number(doc.version))
|
|
225
|
-
? Math.max(1, Math.floor(Number(doc.version)))
|
|
226
|
-
: null;
|
|
227
|
-
const normalizedIncomingDocumentId = typeof doc.documentId === 'string' && doc.documentId.trim().length > 0
|
|
228
|
-
? doc.documentId.trim()
|
|
229
|
-
: null;
|
|
230
|
-
const normalizedIncomingLogicalName = typeof doc.logicalName === 'string' && doc.logicalName.trim().length > 0
|
|
231
|
-
? doc.logicalName.trim()
|
|
232
|
-
: null;
|
|
233
|
-
const normalizedIncomingCaption = typeof doc.caption === 'string' && doc.caption.trim().length > 0
|
|
234
|
-
? doc.caption.trim()
|
|
235
|
-
: null;
|
|
236
|
-
const normalizedIncomingFilename = typeof doc.originalFilename === 'string' && doc.originalFilename.trim().length > 0
|
|
237
|
-
? doc.originalFilename.trim()
|
|
238
|
-
: null;
|
|
239
|
-
const normalizedIncomingTaskId = typeof doc.taskId === 'string' && doc.taskId.trim().length > 0
|
|
240
|
-
? doc.taskId.trim()
|
|
183
|
+
const contentSyncState = createContentSyncState({
|
|
184
|
+
core,
|
|
185
|
+
workspaceAssetStore,
|
|
186
|
+
getObjectStorageClient,
|
|
187
|
+
normalizeWorkspaceSyncDocumentPath,
|
|
188
|
+
normalizeTaskDataRelativePath: (rawPath) => normalizeTaskDataRelativePath(String(rawPath || '').trim()),
|
|
189
|
+
recordSyncDiagnostic,
|
|
190
|
+
workspaceDocumentSnapshotCache,
|
|
191
|
+
});
|
|
192
|
+
const collaborationSyncState = createCollaborationSyncState({ core });
|
|
193
|
+
const hasTaskReferenceNumberDrift = (workspaceId, task) => {
|
|
194
|
+
const taskId = String(task?.id || '').trim();
|
|
195
|
+
const incomingReferenceNumber = Number.isFinite(Number(task?.referenceNumber))
|
|
196
|
+
? Math.max(1, Math.floor(Number(task?.referenceNumber)))
|
|
241
197
|
: null;
|
|
242
|
-
|
|
243
|
-
const allowReferenceNumberReassignment = options?.allowReferenceNumberReassignment === true;
|
|
244
|
-
if (!allowReferenceNumberReassignment
|
|
245
|
-
&& existingAsset.referenceNumber
|
|
246
|
-
&& normalizedIncomingReference
|
|
247
|
-
&& existingAsset.referenceNumber !== normalizedIncomingReference) {
|
|
248
|
-
recordSyncDiagnostic(workspaceId, 'apply', 'Detected document reference number mismatch during normal sync.', {
|
|
249
|
-
source: 'documents.referenceNumber.mismatch',
|
|
250
|
-
path: doc.path,
|
|
251
|
-
assetId: existingAsset.assetId,
|
|
252
|
-
existingReferenceNumber: existingAsset.referenceNumber,
|
|
253
|
-
incomingReferenceNumber: normalizedIncomingReference
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
return (allowReferenceNumberReassignment && existingAsset.referenceNumber !== normalizedIncomingReference)
|
|
257
|
-
|| existingAsset.version !== normalizedIncomingVersion
|
|
258
|
-
|| (existingAsset.documentId || null) !== normalizedIncomingDocumentId
|
|
259
|
-
|| (existingAsset.logicalName || null) !== normalizedIncomingLogicalName
|
|
260
|
-
|| (existingAsset.originalFilename || null) !== normalizedIncomingFilename
|
|
261
|
-
|| (primaryLink?.taskId || null) !== normalizedIncomingTaskId
|
|
262
|
-
|| (primaryLink?.displayName || null) !== normalizedIncomingCaption
|
|
263
|
-
|| ((primaryLink?.role === 'reference' ? 'reference' : 'attachment')) !== normalizedIncomingLinkRole;
|
|
264
|
-
};
|
|
265
|
-
const hasBinaryAssetSyncMetadataDrift = (workspaceId, asset, options) => {
|
|
266
|
-
if (!workspaceAssetStore)
|
|
198
|
+
if (!incomingReferenceNumber)
|
|
267
199
|
return false;
|
|
268
|
-
const
|
|
269
|
-
if (!
|
|
200
|
+
const existingTask = core.resolveTaskIdentifier(`T-${incomingReferenceNumber}`, workspaceId);
|
|
201
|
+
if (!existingTask || existingTask.id === taskId)
|
|
270
202
|
return false;
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
: null
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
:
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
: null;
|
|
281
|
-
const normalizedIncomingFilename = typeof asset.originalFilename === 'string' && asset.originalFilename.trim().length > 0
|
|
282
|
-
? asset.originalFilename.trim()
|
|
283
|
-
: null;
|
|
284
|
-
const normalizedIncomingTaskId = typeof asset.taskId === 'string' && asset.taskId.trim().length > 0
|
|
285
|
-
? asset.taskId.trim()
|
|
286
|
-
: null;
|
|
287
|
-
const normalizedIncomingLinkRole = asset.linkRole === 'reference'
|
|
288
|
-
? 'reference'
|
|
289
|
-
: asset.kind === 'image'
|
|
290
|
-
? 'image'
|
|
291
|
-
: 'attachment';
|
|
292
|
-
const allowReferenceNumberReassignment = options?.allowReferenceNumberReassignment === true;
|
|
293
|
-
if (!allowReferenceNumberReassignment
|
|
294
|
-
&& existingAsset.referenceNumber
|
|
295
|
-
&& normalizedIncomingReference
|
|
296
|
-
&& existingAsset.referenceNumber !== normalizedIncomingReference) {
|
|
297
|
-
recordSyncDiagnostic(workspaceId, 'apply', 'Detected asset reference number mismatch during normal sync.', {
|
|
298
|
-
source: 'assets.referenceNumber.mismatch',
|
|
299
|
-
path: asset.path,
|
|
300
|
-
assetId: existingAsset.assetId,
|
|
301
|
-
existingReferenceNumber: existingAsset.referenceNumber,
|
|
302
|
-
incomingReferenceNumber: normalizedIncomingReference
|
|
303
|
-
});
|
|
304
|
-
}
|
|
305
|
-
return (allowReferenceNumberReassignment && existingAsset.referenceNumber !== normalizedIncomingReference)
|
|
306
|
-
|| (existingAsset.logicalName || null) !== normalizedIncomingLogicalName
|
|
307
|
-
|| (existingAsset.originalFilename || null) !== normalizedIncomingFilename
|
|
308
|
-
|| (primaryLink?.taskId || null) !== normalizedIncomingTaskId
|
|
309
|
-
|| (primaryLink?.displayName || null) !== normalizedIncomingCaption
|
|
310
|
-
|| ((primaryLink?.role === 'reference'
|
|
311
|
-
? 'reference'
|
|
312
|
-
: existingAsset.kind === 'image'
|
|
313
|
-
? 'image'
|
|
314
|
-
: 'attachment')) !== normalizedIncomingLinkRole;
|
|
315
|
-
};
|
|
316
|
-
const reconcileWorkspaceSyncLinks = (input) => {
|
|
317
|
-
if (!workspaceAssetStore)
|
|
318
|
-
return;
|
|
319
|
-
const workspaceId = String(input.workspaceId || '').trim() || 'default';
|
|
320
|
-
const assetId = String(input.assetId || '').trim();
|
|
321
|
-
if (!assetId)
|
|
322
|
-
return;
|
|
323
|
-
const desiredTaskId = String(input.taskId || '').trim();
|
|
324
|
-
const desiredRole = input.role === 'reference'
|
|
325
|
-
? 'reference'
|
|
326
|
-
: input.role === 'image'
|
|
327
|
-
? 'image'
|
|
328
|
-
: 'attachment';
|
|
329
|
-
const linkUpdatedAt = String(input.updatedAt || '').trim() || new Date().toISOString();
|
|
330
|
-
const existingLinks = workspaceAssetStore.listLinksForAsset(assetId, workspaceId, { includeDeleted: true });
|
|
331
|
-
if (desiredTaskId) {
|
|
332
|
-
workspaceAssetStore.upsertLink({
|
|
333
|
-
workspaceId,
|
|
334
|
-
assetId,
|
|
335
|
-
taskId: desiredTaskId,
|
|
336
|
-
role: desiredRole,
|
|
337
|
-
displayName: typeof input.displayName === 'string' && input.displayName.trim().length > 0 ? input.displayName.trim() : null,
|
|
338
|
-
updatedAt: linkUpdatedAt,
|
|
339
|
-
deletedAt: null
|
|
340
|
-
});
|
|
341
|
-
}
|
|
342
|
-
// Only clean up stale links when we have an explicit incoming taskId. If the incoming
|
|
343
|
-
// asset-upsert carries no taskId, there is no authoritative association to enforce, so
|
|
344
|
-
// preserve all existing links rather than wiping them.
|
|
345
|
-
if (desiredTaskId) {
|
|
346
|
-
for (const link of existingLinks) {
|
|
347
|
-
if (link.taskId === desiredTaskId && link.role === desiredRole)
|
|
348
|
-
continue;
|
|
349
|
-
if (link.deletedAt)
|
|
350
|
-
continue;
|
|
351
|
-
workspaceAssetStore.deleteLink({
|
|
352
|
-
workspaceId,
|
|
353
|
-
assetId,
|
|
354
|
-
taskId: link.taskId,
|
|
355
|
-
role: link.role
|
|
356
|
-
});
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
};
|
|
360
|
-
const listWorkspaceSyncDocumentDeletesSnapshot = (workspaceId = 'default') => {
|
|
361
|
-
if (!workspaceAssetStore)
|
|
362
|
-
return [];
|
|
363
|
-
const entries = workspaceAssetStore.listAllByWorkspace(workspaceId, {
|
|
364
|
-
includeDeleted: true,
|
|
365
|
-
kind: 'document'
|
|
366
|
-
})
|
|
367
|
-
.filter((asset) => typeof asset.deletedAt === 'string' && asset.deletedAt.trim().length > 0)
|
|
368
|
-
.map((asset) => {
|
|
369
|
-
const normalizedPath = normalizeWorkspaceSyncDocumentPath(asset.storageKey);
|
|
370
|
-
if (!normalizedPath)
|
|
371
|
-
return null;
|
|
372
|
-
return {
|
|
373
|
-
path: normalizedPath,
|
|
374
|
-
deletedAt: String(asset.deletedAt || '').trim(),
|
|
375
|
-
assetId: asset.assetId
|
|
376
|
-
};
|
|
377
|
-
});
|
|
378
|
-
return entries.filter((entry) => Boolean(entry && entry.path && entry.deletedAt));
|
|
379
|
-
};
|
|
380
|
-
const listWorkspaceSyncDocumentReviewSessionsSnapshot = (workspaceId = 'default') => {
|
|
381
|
-
return core.listDocumentReviewSessionsForSync({ workspaceId, includeDeleted: true })
|
|
382
|
-
.map((session) => ({
|
|
383
|
-
id: session.id,
|
|
384
|
-
assetId: session.assetId,
|
|
385
|
-
documentId: session.documentId,
|
|
386
|
-
documentVersion: session.documentVersion,
|
|
387
|
-
title: session.title,
|
|
388
|
-
status: session.status,
|
|
389
|
-
comments: session.comments,
|
|
390
|
-
createdByActorId: session.createdByActorId,
|
|
391
|
-
updatedByActorId: session.updatedByActorId,
|
|
392
|
-
createdAt: session.createdAt,
|
|
393
|
-
updatedAt: session.updatedAt,
|
|
394
|
-
deletedAt: session.deletedAt,
|
|
395
|
-
}))
|
|
396
|
-
.filter((session) => session.id.length > 0 && session.assetId.length > 0);
|
|
397
|
-
};
|
|
398
|
-
const listWorkspaceSyncDocumentReviewCommentsSnapshot = (workspaceId = 'default') => {
|
|
399
|
-
return core.listDocumentReviewCommentsForSync({ workspaceId, includeDeleted: true })
|
|
400
|
-
.map((comment) => ({
|
|
401
|
-
id: comment.id,
|
|
402
|
-
sessionId: comment.sessionId,
|
|
403
|
-
body: comment.body,
|
|
404
|
-
anchor: comment.anchor,
|
|
405
|
-
order: comment.order,
|
|
406
|
-
authorActorId: comment.authorActorId,
|
|
407
|
-
createdAt: comment.createdAt,
|
|
408
|
-
updatedAt: comment.updatedAt,
|
|
409
|
-
deletedAt: comment.deletedAt,
|
|
410
|
-
}))
|
|
411
|
-
.filter((comment) => comment.id.length > 0 && comment.sessionId.length > 0);
|
|
412
|
-
};
|
|
413
|
-
const listWorkspaceSyncAnnotatedAttachmentSessionsSnapshot = (workspaceId = 'default') => {
|
|
414
|
-
return core.listAnnotatedAttachmentSessionsForSync({ workspaceId, includeDeleted: true })
|
|
415
|
-
.map((session) => ({
|
|
416
|
-
id: session.id,
|
|
417
|
-
workspaceId: session.workspaceId,
|
|
418
|
-
taskId: session.taskId,
|
|
419
|
-
baseImageAssetId: session.baseImageAssetId,
|
|
420
|
-
title: session.title,
|
|
421
|
-
globalInstruction: session.globalInstruction,
|
|
422
|
-
annotations: session.annotations,
|
|
423
|
-
createdByActorId: session.createdByActorId,
|
|
424
|
-
updatedByActorId: session.updatedByActorId,
|
|
425
|
-
createdAt: session.createdAt,
|
|
426
|
-
updatedAt: session.updatedAt,
|
|
427
|
-
deletedAt: session.deletedAt,
|
|
428
|
-
}))
|
|
429
|
-
.filter((session) => (session.id.length > 0
|
|
430
|
-
&& session.workspaceId.length > 0
|
|
431
|
-
&& session.baseImageAssetId.length > 0));
|
|
432
|
-
};
|
|
433
|
-
const listWorkspaceSyncDocumentsSnapshot = async (basePath, workspaceId = 'default') => {
|
|
434
|
-
const root = path.resolve(basePath);
|
|
435
|
-
const objectStorageClient = getObjectStorageClient();
|
|
436
|
-
const candidatePaths = new Set();
|
|
437
|
-
const docsRoot = path.join(root, 'docs');
|
|
438
|
-
const collectMarkdownPaths = (directory) => {
|
|
439
|
-
if (!fs.existsSync(directory))
|
|
440
|
-
return;
|
|
441
|
-
const entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
442
|
-
for (const entry of entries) {
|
|
443
|
-
const absolutePath = path.join(directory, entry.name);
|
|
444
|
-
if (entry.isDirectory()) {
|
|
445
|
-
collectMarkdownPaths(absolutePath);
|
|
446
|
-
continue;
|
|
447
|
-
}
|
|
448
|
-
if (!entry.isFile())
|
|
449
|
-
continue;
|
|
450
|
-
const relativeFromRoot = path.relative(root, absolutePath).replace(/\\/g, '/');
|
|
451
|
-
const normalized = normalizeWorkspaceSyncDocumentPath(relativeFromRoot);
|
|
452
|
-
if (normalized)
|
|
453
|
-
candidatePaths.add(normalized);
|
|
454
|
-
}
|
|
455
|
-
};
|
|
456
|
-
collectMarkdownPaths(docsRoot);
|
|
457
|
-
if (workspaceAssetStore) {
|
|
458
|
-
const canonicalAssets = workspaceAssetStore.listAllByWorkspace(workspaceId, {
|
|
459
|
-
includeDeleted: false,
|
|
460
|
-
kind: 'document'
|
|
461
|
-
});
|
|
462
|
-
for (const asset of canonicalAssets) {
|
|
463
|
-
const normalized = normalizeWorkspaceSyncDocumentPath(asset.storageKey);
|
|
464
|
-
if (normalized)
|
|
465
|
-
candidatePaths.add(normalized);
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
const uniquePaths = Array.from(candidatePaths).sort((a, b) => a.localeCompare(b));
|
|
469
|
-
const workspaceCache = workspaceDocumentSnapshotCache.get(workspaceId) || new Map();
|
|
470
|
-
workspaceDocumentSnapshotCache.set(workspaceId, workspaceCache);
|
|
471
|
-
const remainingPaths = new Set(uniquePaths);
|
|
472
|
-
const docs = [];
|
|
473
|
-
for (const relativePath of uniquePaths) {
|
|
474
|
-
try {
|
|
475
|
-
const canonicalAsset = workspaceAssetStore?.getByStorageKey(relativePath, workspaceId);
|
|
476
|
-
let stat = null;
|
|
477
|
-
let content = null;
|
|
478
|
-
let updatedAt = '';
|
|
479
|
-
if (canonicalAsset?.kind === 'document') {
|
|
480
|
-
updatedAt = resolveCanonicalAssetSyncLinkState(canonicalAsset, workspaceId).effectiveUpdatedAt;
|
|
481
|
-
if (canonicalAsset.storageProvider === 'r2' && objectStorageClient) {
|
|
482
|
-
const object = await objectStorageClient.getObject(relativePath);
|
|
483
|
-
if (!object?.body)
|
|
484
|
-
continue;
|
|
485
|
-
content = object.body.toString('utf8');
|
|
486
|
-
}
|
|
487
|
-
else {
|
|
488
|
-
const absPath = path.resolve(path.join(root, relativePath));
|
|
489
|
-
if (absPath !== root && !absPath.startsWith(`${root}${path.sep}`))
|
|
490
|
-
continue;
|
|
491
|
-
if (!fs.existsSync(absPath))
|
|
492
|
-
continue;
|
|
493
|
-
stat = fs.statSync(absPath);
|
|
494
|
-
if (!stat.isFile())
|
|
495
|
-
continue;
|
|
496
|
-
content = fs.readFileSync(absPath, 'utf8');
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
else {
|
|
500
|
-
const absPath = path.resolve(path.join(basePath, relativePath));
|
|
501
|
-
const safeRoot = path.resolve(basePath);
|
|
502
|
-
if (absPath !== safeRoot && !absPath.startsWith(`${safeRoot}${path.sep}`))
|
|
503
|
-
continue;
|
|
504
|
-
stat = fs.statSync(absPath);
|
|
505
|
-
if (!stat.isFile())
|
|
506
|
-
continue;
|
|
507
|
-
content = fs.readFileSync(absPath, 'utf8');
|
|
508
|
-
}
|
|
509
|
-
if (content == null)
|
|
510
|
-
continue;
|
|
511
|
-
const cached = workspaceCache.get(relativePath);
|
|
512
|
-
if (cached
|
|
513
|
-
&& cached.size === Buffer.byteLength(content, 'utf8')
|
|
514
|
-
&& (canonicalAsset
|
|
515
|
-
? cached.updatedAt === updatedAt
|
|
516
|
-
: (stat != null && cached.mtimeMs === stat.mtimeMs && cached.ctimeMs === stat.ctimeMs))) {
|
|
517
|
-
docs.push({
|
|
518
|
-
path: relativePath,
|
|
519
|
-
updatedAt: cached.updatedAt,
|
|
520
|
-
content: cached.content,
|
|
521
|
-
...getCanonicalSyncDocumentMetadata(relativePath, workspaceId)
|
|
522
|
-
});
|
|
523
|
-
continue;
|
|
524
|
-
}
|
|
525
|
-
if (!canonicalAsset) {
|
|
526
|
-
const contentHash = createHash('sha256').update(content).digest('hex');
|
|
527
|
-
const existing = core.getDocumentWatermark(workspaceId, relativePath);
|
|
528
|
-
if (existing && existing.contentHash === contentHash) {
|
|
529
|
-
updatedAt = existing.updatedAt;
|
|
530
|
-
}
|
|
531
|
-
else {
|
|
532
|
-
updatedAt = stat && Number.isFinite(stat.mtimeMs)
|
|
533
|
-
? new Date(stat.mtimeMs).toISOString()
|
|
534
|
-
: new Date().toISOString();
|
|
535
|
-
try {
|
|
536
|
-
core.upsertDocumentWatermark(workspaceId, relativePath, contentHash, updatedAt);
|
|
537
|
-
}
|
|
538
|
-
catch (error) {
|
|
539
|
-
recordSyncDiagnostic(workspaceId, 'pull', 'Failed to persist document watermark during snapshot.', {
|
|
540
|
-
source: 'documents.snapshot.upsertWatermark',
|
|
541
|
-
path: relativePath,
|
|
542
|
-
error: String(error?.message || error || '')
|
|
543
|
-
});
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
docs.push({
|
|
548
|
-
path: relativePath,
|
|
549
|
-
updatedAt,
|
|
550
|
-
content,
|
|
551
|
-
...getCanonicalSyncDocumentMetadata(relativePath, workspaceId)
|
|
552
|
-
});
|
|
553
|
-
workspaceCache.set(relativePath, {
|
|
554
|
-
path: relativePath,
|
|
555
|
-
mtimeMs: stat?.mtimeMs ?? 0,
|
|
556
|
-
ctimeMs: stat?.ctimeMs ?? 0,
|
|
557
|
-
size: Buffer.byteLength(content, 'utf8'),
|
|
558
|
-
updatedAt,
|
|
559
|
-
content
|
|
560
|
-
});
|
|
561
|
-
}
|
|
562
|
-
catch (error) {
|
|
563
|
-
recordSyncDiagnostic(workspaceId, 'pull', 'Failed to read sync document snapshot entry.', {
|
|
564
|
-
source: 'documents.snapshot.readEntry',
|
|
565
|
-
path: relativePath,
|
|
566
|
-
error: String(error?.message || error || '')
|
|
567
|
-
});
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
for (const cachedPath of Array.from(workspaceCache.keys())) {
|
|
571
|
-
if (!remainingPaths.has(cachedPath))
|
|
572
|
-
workspaceCache.delete(cachedPath);
|
|
573
|
-
}
|
|
574
|
-
return docs;
|
|
575
|
-
};
|
|
576
|
-
const listWorkspaceSyncBinaryAssetsSnapshot = async (basePath, workspaceId = 'default') => {
|
|
577
|
-
if (!workspaceAssetStore)
|
|
578
|
-
return [];
|
|
579
|
-
const root = path.resolve(basePath);
|
|
580
|
-
const objectStorageClient = getObjectStorageClient();
|
|
581
|
-
const assets = workspaceAssetStore.listAllByWorkspace(workspaceId, {
|
|
582
|
-
includeDeleted: false
|
|
583
|
-
});
|
|
584
|
-
const snapshots = [];
|
|
585
|
-
for (const asset of assets) {
|
|
586
|
-
if (asset.kind !== 'image' && asset.kind !== 'file')
|
|
587
|
-
continue;
|
|
588
|
-
// External references are not syncable binary payloads.
|
|
589
|
-
if (asset.storageProvider === 'external')
|
|
590
|
-
continue;
|
|
591
|
-
const storageKey = String(asset.storageKey || '').trim();
|
|
592
|
-
if (!storageKey)
|
|
593
|
-
continue;
|
|
594
|
-
// Skip legacy task-data assets — they cannot be received on the other end
|
|
595
|
-
if (!normalizeTaskDataRelativePath(storageKey))
|
|
596
|
-
continue;
|
|
597
|
-
try {
|
|
598
|
-
let buffer = null;
|
|
599
|
-
if (asset.storageProvider === 'r2' && objectStorageClient) {
|
|
600
|
-
const object = await objectStorageClient.getObject(storageKey);
|
|
601
|
-
buffer = object?.body || null;
|
|
602
|
-
}
|
|
603
|
-
else {
|
|
604
|
-
const absolutePath = path.resolve(root, storageKey);
|
|
605
|
-
if (absolutePath !== root && !absolutePath.startsWith(`${root}${path.sep}`))
|
|
606
|
-
continue;
|
|
607
|
-
if (!fs.existsSync(absolutePath)) {
|
|
608
|
-
recordSyncDiagnostic(workspaceId, 'push', 'Asset registered in DB but file missing from disk — skipped from sync payload.', {
|
|
609
|
-
source: 'assets.snapshot.missingFile',
|
|
610
|
-
assetId: asset.assetId,
|
|
611
|
-
path: storageKey
|
|
612
|
-
});
|
|
613
|
-
continue;
|
|
614
|
-
}
|
|
615
|
-
buffer = fs.readFileSync(absolutePath);
|
|
616
|
-
}
|
|
617
|
-
if (!buffer) {
|
|
618
|
-
recordSyncDiagnostic(workspaceId, 'push', 'Asset registered in DB but content could not be read — skipped from sync payload.', {
|
|
619
|
-
source: 'assets.snapshot.emptyBuffer',
|
|
620
|
-
assetId: asset.assetId,
|
|
621
|
-
path: storageKey,
|
|
622
|
-
storageProvider: asset.storageProvider
|
|
623
|
-
});
|
|
624
|
-
continue;
|
|
625
|
-
}
|
|
626
|
-
const { primaryLink, effectiveUpdatedAt } = resolveCanonicalAssetSyncLinkState(asset, workspaceId);
|
|
627
|
-
snapshots.push({
|
|
628
|
-
path: storageKey,
|
|
629
|
-
updatedAt: effectiveUpdatedAt,
|
|
630
|
-
contentBase64: buffer.toString('base64'),
|
|
631
|
-
assetId: asset.assetId,
|
|
632
|
-
kind: asset.kind,
|
|
633
|
-
mimeType: asset.mimeType || 'application/octet-stream',
|
|
634
|
-
referenceNumber: asset.referenceNumber,
|
|
635
|
-
taskId: primaryLink?.taskId || null,
|
|
636
|
-
logicalName: asset.logicalName || null,
|
|
637
|
-
caption: primaryLink?.displayName || null,
|
|
638
|
-
originalFilename: asset.originalFilename || path.basename(storageKey),
|
|
639
|
-
linkRole: primaryLink?.role || (asset.kind === 'image' ? 'image' : 'attachment')
|
|
640
|
-
});
|
|
641
|
-
}
|
|
642
|
-
catch (error) {
|
|
643
|
-
recordSyncDiagnostic(workspaceId, 'pull', 'Failed to read binary asset snapshot entry.', {
|
|
644
|
-
source: 'assets.snapshot.readEntry',
|
|
645
|
-
path: storageKey,
|
|
646
|
-
error: String(error?.message || error || '')
|
|
647
|
-
});
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
return snapshots;
|
|
651
|
-
};
|
|
652
|
-
const listWorkspaceSyncBinaryAssetDeletesSnapshot = (workspaceId = 'default') => {
|
|
653
|
-
if (!workspaceAssetStore)
|
|
654
|
-
return [];
|
|
655
|
-
const entries = workspaceAssetStore.listAllByWorkspace(workspaceId, {
|
|
656
|
-
includeDeleted: true
|
|
657
|
-
})
|
|
658
|
-
.filter((asset) => ((asset.kind === 'image' || asset.kind === 'file')
|
|
659
|
-
&& asset.storageProvider !== 'external'
|
|
660
|
-
&& typeof asset.deletedAt === 'string'
|
|
661
|
-
&& asset.deletedAt.trim().length > 0))
|
|
662
|
-
.map((asset) => {
|
|
663
|
-
const normalizedPath = normalizeTaskDataRelativePath(asset.storageKey);
|
|
664
|
-
if (!normalizedPath)
|
|
665
|
-
return null;
|
|
666
|
-
return {
|
|
667
|
-
path: normalizedPath,
|
|
668
|
-
deletedAt: String(asset.deletedAt || '').trim(),
|
|
669
|
-
assetId: asset.assetId
|
|
670
|
-
};
|
|
203
|
+
recordSyncDiagnostic(workspaceId, 'apply', 'Detected task reference number mismatch during normal sync.', {
|
|
204
|
+
source: 'tasks.referenceNumber.mismatch',
|
|
205
|
+
taskId: taskId || null,
|
|
206
|
+
taskTitle: typeof task?.title === 'string' ? task.title.trim() || null : null,
|
|
207
|
+
existingTaskId: existingTask.id,
|
|
208
|
+
existingTaskTitle: existingTask.title || null,
|
|
209
|
+
referenceLabel: `T-${incomingReferenceNumber}`,
|
|
210
|
+
existingReferenceNumber: existingTask.referenceNumber ?? incomingReferenceNumber,
|
|
211
|
+
incomingReferenceNumber
|
|
671
212
|
});
|
|
672
|
-
return
|
|
673
|
-
};
|
|
674
|
-
const writeWorkspaceSyncDocument = async (basePath, doc, workspaceId = 'default', options) => {
|
|
675
|
-
const normalizedPath = normalizeWorkspaceSyncDocumentPath(doc.path);
|
|
676
|
-
if (!normalizedPath)
|
|
677
|
-
return;
|
|
678
|
-
const root = path.resolve(basePath);
|
|
679
|
-
const targetPath = path.resolve(root, normalizedPath);
|
|
680
|
-
if (targetPath !== root && !targetPath.startsWith(`${root}${path.sep}`))
|
|
681
|
-
return;
|
|
682
|
-
const objectStorageClient = getObjectStorageClient();
|
|
683
|
-
const content = String(doc.content || '');
|
|
684
|
-
if (objectStorageClient) {
|
|
685
|
-
try {
|
|
686
|
-
await objectStorageClient.putObject(normalizedPath, Buffer.from(content, 'utf8'), 'text/markdown');
|
|
687
|
-
}
|
|
688
|
-
catch (error) {
|
|
689
|
-
recordSyncDiagnostic(workspaceId, 'apply', 'Failed to write document to object storage.', {
|
|
690
|
-
source: 'documents.write.putObject',
|
|
691
|
-
path: normalizedPath,
|
|
692
|
-
error: String(error?.message || error || '')
|
|
693
|
-
});
|
|
694
|
-
return;
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
else {
|
|
698
|
-
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
699
|
-
fs.writeFileSync(targetPath, content, 'utf8');
|
|
700
|
-
}
|
|
701
|
-
const updatedAt = String(doc.updatedAt || '').trim();
|
|
702
|
-
const watermarkMs = Date.parse(updatedAt);
|
|
703
|
-
if (!objectStorageClient && Number.isFinite(watermarkMs) && watermarkMs > 0) {
|
|
704
|
-
const when = new Date(watermarkMs);
|
|
705
|
-
fs.utimesSync(targetPath, when, when);
|
|
706
|
-
}
|
|
707
|
-
// Persist content hash + timestamp to SQLite so the snapshot function uses a
|
|
708
|
-
// stable watermark instead of relying on non-deterministic filesystem mtime.
|
|
709
|
-
try {
|
|
710
|
-
const contentHash = createHash('sha256').update(content).digest('hex');
|
|
711
|
-
core.upsertDocumentWatermark(workspaceId, normalizedPath, contentHash, updatedAt || new Date().toISOString());
|
|
712
|
-
if (workspaceAssetStore) {
|
|
713
|
-
const existing = workspaceAssetStore.getByStorageKey(normalizedPath, workspaceId);
|
|
714
|
-
const effectiveUpdatedAt = updatedAt || new Date().toISOString();
|
|
715
|
-
const originalFilename = String(doc.originalFilename || existing?.originalFilename || path.basename(normalizedPath) || 'document.md').trim() || 'document.md';
|
|
716
|
-
const asset = workspaceAssetStore.upsertAsset({
|
|
717
|
-
assetId: String(doc.assetId || existing?.assetId || `asset-${randomUUID().replace(/-/g, '')}`),
|
|
718
|
-
workspaceId,
|
|
719
|
-
kind: 'document',
|
|
720
|
-
logicalName: typeof doc.logicalName === 'string' && doc.logicalName.trim().length > 0
|
|
721
|
-
? doc.logicalName.trim()
|
|
722
|
-
: existing?.logicalName || null,
|
|
723
|
-
originalFilename,
|
|
724
|
-
mimeType: existing?.mimeType || 'text/markdown',
|
|
725
|
-
storageProvider: objectStorageClient ? 'r2' : 'local',
|
|
726
|
-
storageKey: normalizedPath,
|
|
727
|
-
contentSha256: contentHash,
|
|
728
|
-
sizeBytes: Buffer.byteLength(content, 'utf8'),
|
|
729
|
-
documentId: String(doc.documentId || existing?.documentId || `doc-${randomUUID().replace(/-/g, '')}`),
|
|
730
|
-
referenceNumber: Number.isFinite(Number(doc.referenceNumber))
|
|
731
|
-
? Math.max(1, Math.floor(Number(doc.referenceNumber)))
|
|
732
|
-
: existing?.referenceNumber || null,
|
|
733
|
-
version: Number.isFinite(Number(doc.version))
|
|
734
|
-
? Math.max(1, Math.floor(Number(doc.version)))
|
|
735
|
-
: Math.max(1, Number(existing?.version || 0) + 1),
|
|
736
|
-
isLatest: true,
|
|
737
|
-
scanStatus: existing?.scanStatus || 'clean',
|
|
738
|
-
scanEngine: existing?.scanEngine || null,
|
|
739
|
-
scanDetails: existing?.scanDetails || null,
|
|
740
|
-
createdByUserId: existing?.createdByUserId || null,
|
|
741
|
-
createdAt: existing?.createdAt || effectiveUpdatedAt,
|
|
742
|
-
updatedAt: effectiveUpdatedAt,
|
|
743
|
-
deletedAt: null,
|
|
744
|
-
purgeAfter: null,
|
|
745
|
-
allowReferenceNumberReassignment: options?.allowReferenceNumberReassignment === true
|
|
746
|
-
});
|
|
747
|
-
const taskId = String(doc.taskId || '').trim();
|
|
748
|
-
reconcileWorkspaceSyncLinks({
|
|
749
|
-
workspaceId,
|
|
750
|
-
assetId: asset.assetId,
|
|
751
|
-
taskId,
|
|
752
|
-
displayName: typeof doc.caption === 'string' && doc.caption.trim().length > 0 ? doc.caption.trim() : null,
|
|
753
|
-
role: doc.linkRole === 'reference' ? 'reference' : 'attachment',
|
|
754
|
-
updatedAt: effectiveUpdatedAt
|
|
755
|
-
});
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
catch (error) {
|
|
759
|
-
recordSyncDiagnostic(workspaceId, 'apply', 'Failed to persist document watermark after write.', {
|
|
760
|
-
source: 'documents.write.upsertWatermark',
|
|
761
|
-
path: normalizedPath,
|
|
762
|
-
error: String(error?.message || error || '')
|
|
763
|
-
});
|
|
764
|
-
}
|
|
765
|
-
workspaceDocumentSnapshotCache.get(workspaceId)?.delete(normalizedPath);
|
|
766
|
-
};
|
|
767
|
-
const writeWorkspaceSyncBinaryAsset = async (basePath, asset, workspaceId = 'default', options) => {
|
|
768
|
-
const normalizedPath = normalizeTaskDataRelativePath(asset.path);
|
|
769
|
-
if (!normalizedPath)
|
|
770
|
-
return;
|
|
771
|
-
const root = path.resolve(basePath);
|
|
772
|
-
const targetPath = path.resolve(root, normalizedPath);
|
|
773
|
-
if (targetPath !== root && !targetPath.startsWith(`${root}${path.sep}`))
|
|
774
|
-
return;
|
|
775
|
-
const objectStorageClient = getObjectStorageClient();
|
|
776
|
-
const buffer = Buffer.from(String(asset.contentBase64 || ''), 'base64');
|
|
777
|
-
if (objectStorageClient) {
|
|
778
|
-
try {
|
|
779
|
-
await objectStorageClient.putObject(normalizedPath, buffer, String(asset.mimeType || 'application/octet-stream').trim() || 'application/octet-stream');
|
|
780
|
-
}
|
|
781
|
-
catch (error) {
|
|
782
|
-
recordSyncDiagnostic(workspaceId, 'apply', 'Failed to write binary asset to object storage.', {
|
|
783
|
-
source: 'assets.write.putObject',
|
|
784
|
-
path: normalizedPath,
|
|
785
|
-
error: String(error?.message || error || '')
|
|
786
|
-
});
|
|
787
|
-
return;
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
else {
|
|
791
|
-
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
792
|
-
fs.writeFileSync(targetPath, buffer);
|
|
793
|
-
}
|
|
794
|
-
const updatedAt = String(asset.updatedAt || '').trim();
|
|
795
|
-
const watermarkMs = Date.parse(updatedAt);
|
|
796
|
-
if (!objectStorageClient && Number.isFinite(watermarkMs) && watermarkMs > 0) {
|
|
797
|
-
const when = new Date(watermarkMs);
|
|
798
|
-
fs.utimesSync(targetPath, when, when);
|
|
799
|
-
}
|
|
800
|
-
if (!workspaceAssetStore)
|
|
801
|
-
return;
|
|
802
|
-
try {
|
|
803
|
-
const existing = workspaceAssetStore.getByStorageKey(normalizedPath, workspaceId);
|
|
804
|
-
const effectiveUpdatedAt = updatedAt || new Date().toISOString();
|
|
805
|
-
const assetRecord = workspaceAssetStore.upsertAsset({
|
|
806
|
-
assetId: String(asset.assetId || existing?.assetId || `asset-${randomUUID().replace(/-/g, '')}`),
|
|
807
|
-
workspaceId,
|
|
808
|
-
kind: asset.kind === 'image' ? 'image' : 'file',
|
|
809
|
-
logicalName: typeof asset.logicalName === 'string' && asset.logicalName.trim().length > 0
|
|
810
|
-
? asset.logicalName.trim()
|
|
811
|
-
: existing?.logicalName || null,
|
|
812
|
-
originalFilename: String(asset.originalFilename || existing?.originalFilename || path.basename(normalizedPath) || 'asset').trim() || 'asset',
|
|
813
|
-
mimeType: String(asset.mimeType || existing?.mimeType || 'application/octet-stream').trim() || 'application/octet-stream',
|
|
814
|
-
storageProvider: objectStorageClient ? 'r2' : 'local',
|
|
815
|
-
storageKey: normalizedPath,
|
|
816
|
-
contentSha256: createHash('sha256').update(buffer).digest('hex'),
|
|
817
|
-
sizeBytes: buffer.length,
|
|
818
|
-
documentId: null,
|
|
819
|
-
referenceNumber: Number.isFinite(Number(asset.referenceNumber))
|
|
820
|
-
? Math.max(1, Math.floor(Number(asset.referenceNumber)))
|
|
821
|
-
: existing?.referenceNumber || null,
|
|
822
|
-
version: null,
|
|
823
|
-
isLatest: true,
|
|
824
|
-
scanStatus: existing?.scanStatus || 'clean',
|
|
825
|
-
scanEngine: existing?.scanEngine || null,
|
|
826
|
-
scanDetails: existing?.scanDetails || null,
|
|
827
|
-
createdByUserId: existing?.createdByUserId || null,
|
|
828
|
-
createdAt: existing?.createdAt || effectiveUpdatedAt,
|
|
829
|
-
updatedAt: effectiveUpdatedAt,
|
|
830
|
-
deletedAt: null,
|
|
831
|
-
purgeAfter: null,
|
|
832
|
-
allowReferenceNumberReassignment: options?.allowReferenceNumberReassignment === true
|
|
833
|
-
});
|
|
834
|
-
const taskId = String(asset.taskId || '').trim();
|
|
835
|
-
reconcileWorkspaceSyncLinks({
|
|
836
|
-
workspaceId,
|
|
837
|
-
assetId: assetRecord.assetId,
|
|
838
|
-
taskId,
|
|
839
|
-
displayName: typeof asset.caption === 'string' && asset.caption.trim().length > 0 ? asset.caption.trim() : null,
|
|
840
|
-
role: asset.linkRole === 'reference' ? 'reference' : asset.kind === 'image' ? 'image' : 'attachment',
|
|
841
|
-
updatedAt: effectiveUpdatedAt
|
|
842
|
-
});
|
|
843
|
-
}
|
|
844
|
-
catch (error) {
|
|
845
|
-
recordSyncDiagnostic(workspaceId, 'apply', 'Failed to persist binary asset metadata after write.', {
|
|
846
|
-
source: 'assets.write.upsertAsset',
|
|
847
|
-
path: normalizedPath,
|
|
848
|
-
assetId: typeof asset.assetId === 'string' ? asset.assetId : null,
|
|
849
|
-
kind: asset.kind,
|
|
850
|
-
error: String(error?.message || error || '')
|
|
851
|
-
});
|
|
852
|
-
}
|
|
213
|
+
return true;
|
|
853
214
|
};
|
|
854
|
-
const deleteWorkspaceSyncBinaryAsset = (basePath, assetPath, workspaceId = 'default') => {
|
|
855
|
-
const normalizedPath = normalizeTaskDataRelativePath(assetPath);
|
|
856
|
-
if (!normalizedPath)
|
|
857
|
-
return;
|
|
858
|
-
const root = path.resolve(basePath);
|
|
859
|
-
const targetPath = path.resolve(root, normalizedPath);
|
|
860
|
-
const objectStorageClient = getObjectStorageClient();
|
|
861
|
-
try {
|
|
862
|
-
if (objectStorageClient) {
|
|
863
|
-
void objectStorageClient.deleteObject(normalizedPath);
|
|
864
|
-
}
|
|
865
|
-
else if (targetPath === root || targetPath.startsWith(`${root}${path.sep}`)) {
|
|
866
|
-
if (fs.existsSync(targetPath))
|
|
867
|
-
fs.unlinkSync(targetPath);
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
|
-
catch (error) {
|
|
871
|
-
recordSyncDiagnostic(workspaceId, 'apply', 'Failed to delete workspace sync binary asset.', {
|
|
872
|
-
source: 'assets.delete.file',
|
|
873
|
-
path: normalizedPath,
|
|
874
|
-
error: String(error?.message || error || '')
|
|
875
|
-
});
|
|
876
|
-
}
|
|
877
|
-
if (workspaceAssetStore) {
|
|
878
|
-
const existing = workspaceAssetStore.getByStorageKey(normalizedPath, workspaceId);
|
|
879
|
-
if (existing) {
|
|
880
|
-
const deletedAt = new Date().toISOString();
|
|
881
|
-
workspaceAssetStore.markDeleted(existing.assetId, workspaceId, deletedAt);
|
|
882
|
-
if (existing.kind === 'image') {
|
|
883
|
-
core.deleteAnnotatedAttachmentSessionsForImage({
|
|
884
|
-
workspaceId,
|
|
885
|
-
imageAssetId: existing.assetId,
|
|
886
|
-
deletedAt,
|
|
887
|
-
});
|
|
888
|
-
}
|
|
889
|
-
const links = workspaceAssetStore.listLinksForAsset(existing.assetId, workspaceId, { includeDeleted: true });
|
|
890
|
-
for (const link of links) {
|
|
891
|
-
workspaceAssetStore.deleteLink({
|
|
892
|
-
workspaceId,
|
|
893
|
-
assetId: existing.assetId,
|
|
894
|
-
taskId: link.taskId,
|
|
895
|
-
role: link.role
|
|
896
|
-
});
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
};
|
|
901
|
-
const deleteWorkspaceSyncDocument = (basePath, docPath, workspaceId = 'default') => {
|
|
902
|
-
const normalizedPath = normalizeWorkspaceSyncDocumentPath(docPath);
|
|
903
|
-
if (!normalizedPath)
|
|
904
|
-
return;
|
|
905
|
-
const root = path.resolve(basePath);
|
|
906
|
-
const targetPath = path.resolve(root, normalizedPath);
|
|
907
|
-
if (targetPath !== root && !targetPath.startsWith(`${root}${path.sep}`))
|
|
908
|
-
return;
|
|
909
|
-
const objectStorageClient = getObjectStorageClient();
|
|
910
|
-
try {
|
|
911
|
-
if (objectStorageClient) {
|
|
912
|
-
void objectStorageClient.deleteObject(normalizedPath);
|
|
913
|
-
}
|
|
914
|
-
else if (fs.existsSync(targetPath)) {
|
|
915
|
-
fs.unlinkSync(targetPath);
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
catch (error) {
|
|
919
|
-
recordSyncDiagnostic(workspaceId, 'apply', 'Failed to delete workspace sync document.', {
|
|
920
|
-
source: 'documents.delete.file',
|
|
921
|
-
path: normalizedPath,
|
|
922
|
-
error: String(error?.message || error || '')
|
|
923
|
-
});
|
|
924
|
-
}
|
|
925
|
-
try {
|
|
926
|
-
core.deleteDocumentWatermark(workspaceId, normalizedPath);
|
|
927
|
-
if (workspaceAssetStore) {
|
|
928
|
-
const existing = workspaceAssetStore.getByStorageKey(normalizedPath, workspaceId);
|
|
929
|
-
if (existing) {
|
|
930
|
-
const deletedAt = new Date().toISOString();
|
|
931
|
-
workspaceAssetStore.markDeleted(existing.assetId, workspaceId, deletedAt);
|
|
932
|
-
if (existing.kind === 'image') {
|
|
933
|
-
core.deleteAnnotatedAttachmentSessionsForImage({
|
|
934
|
-
workspaceId,
|
|
935
|
-
imageAssetId: existing.assetId,
|
|
936
|
-
deletedAt,
|
|
937
|
-
});
|
|
938
|
-
}
|
|
939
|
-
const links = workspaceAssetStore.listLinksForAsset(existing.assetId, workspaceId, { includeDeleted: true });
|
|
940
|
-
for (const link of links) {
|
|
941
|
-
workspaceAssetStore.deleteLink({
|
|
942
|
-
workspaceId,
|
|
943
|
-
assetId: existing.assetId,
|
|
944
|
-
taskId: link.taskId,
|
|
945
|
-
role: link.role
|
|
946
|
-
});
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
catch (error) {
|
|
952
|
-
recordSyncDiagnostic(workspaceId, 'apply', 'Failed to delete document watermark.', {
|
|
953
|
-
source: 'documents.delete.watermark',
|
|
954
|
-
path: normalizedPath,
|
|
955
|
-
error: String(error?.message || error || '')
|
|
956
|
-
});
|
|
957
|
-
}
|
|
958
|
-
workspaceDocumentSnapshotCache.get(workspaceId)?.delete(normalizedPath);
|
|
959
|
-
};
|
|
960
|
-
const resolveConflictOrdering = (incomingWatermark, existingWatermark, incomingTieBreak, existingTieBreak) => (compareWorkspaceMutationOrdering(incomingWatermark, existingWatermark, incomingTieBreak, existingTieBreak));
|
|
961
215
|
const dispatchWorkspaceSyncChanges = async (workspaceId, incomingChanges, options) => {
|
|
962
216
|
const tasks = [];
|
|
963
217
|
const archived = [];
|
|
@@ -992,72 +246,32 @@ export function registerSyncRoutes(deps) {
|
|
|
992
246
|
const raw = change;
|
|
993
247
|
const op = String(raw.op || '').trim().toLowerCase();
|
|
994
248
|
if (op === 'taxonomy-upsert') {
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
incomingTaxonomiesUpdatedAt =
|
|
1002
|
-
}
|
|
249
|
+
const taxonomy = normalizeTaxonomySyncPayload(raw);
|
|
250
|
+
if (taxonomy?.taxonomyState)
|
|
251
|
+
incomingTaxonomyState = taxonomy.taxonomyState;
|
|
252
|
+
if (Array.isArray(taxonomy?.taxonomies))
|
|
253
|
+
incomingTaxonomies = taxonomy.taxonomies;
|
|
254
|
+
if (taxonomy?.updatedAt)
|
|
255
|
+
incomingTaxonomiesUpdatedAt = taxonomy.updatedAt;
|
|
1003
256
|
continue;
|
|
1004
257
|
}
|
|
1005
258
|
if (op === 'initiative-upsert') {
|
|
1006
|
-
const initiative = raw.initiative
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
const initiativeId = String(initiative?.id || '').trim();
|
|
1010
|
-
if (initiativeId) {
|
|
1011
|
-
incomingInitiatives.set(initiativeId, {
|
|
1012
|
-
id: initiativeId,
|
|
1013
|
-
referenceNumber: Number.isFinite(Number(initiative?.referenceNumber))
|
|
1014
|
-
? Math.max(1, Math.floor(Number(initiative?.referenceNumber)))
|
|
1015
|
-
: null,
|
|
1016
|
-
referenceLabel: typeof initiative?.referenceLabel === 'string' ? initiative.referenceLabel.trim() : undefined,
|
|
1017
|
-
title: String(initiative?.title || '').trim(),
|
|
1018
|
-
description: typeof initiative?.description === 'string' ? initiative.description : null,
|
|
1019
|
-
ownerId: typeof initiative?.ownerId === 'string' ? initiative.ownerId.trim() : null,
|
|
1020
|
-
createdAt: String(initiative?.createdAt || raw.updatedAt || raw.watermark || '').trim(),
|
|
1021
|
-
updatedAt: String(initiative?.updatedAt || raw.updatedAt || raw.watermark || initiative?.createdAt || '').trim(),
|
|
1022
|
-
order: Number.isFinite(Number(initiative?.order)) ? Math.floor(Number(initiative?.order)) : null,
|
|
1023
|
-
isArchived: Boolean(initiative?.isArchived)
|
|
1024
|
-
});
|
|
1025
|
-
}
|
|
259
|
+
const initiative = normalizeInitiativeSyncPayload(raw.initiative, String(raw.updatedAt || raw.watermark || '').trim());
|
|
260
|
+
if (initiative)
|
|
261
|
+
incomingInitiatives.set(initiative.id, initiative);
|
|
1026
262
|
continue;
|
|
1027
263
|
}
|
|
1028
264
|
if (op === 'workstream-upsert') {
|
|
1029
|
-
const workstream = raw.workstream
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
const workstreamId = String(workstream?.id || '').trim();
|
|
1033
|
-
if (workstreamId) {
|
|
1034
|
-
incomingWorkstreams.set(workstreamId, {
|
|
1035
|
-
id: workstreamId,
|
|
1036
|
-
referenceNumber: Number.isFinite(Number(workstream?.referenceNumber))
|
|
1037
|
-
? Math.max(1, Math.floor(Number(workstream?.referenceNumber)))
|
|
1038
|
-
: null,
|
|
1039
|
-
referenceLabel: typeof workstream?.referenceLabel === 'string' ? workstream.referenceLabel.trim() : undefined,
|
|
1040
|
-
initiativeId: typeof workstream?.initiativeId === 'string' && workstream.initiativeId.trim().length > 0
|
|
1041
|
-
? workstream.initiativeId.trim()
|
|
1042
|
-
: null,
|
|
1043
|
-
title: String(workstream?.title || '').trim(),
|
|
1044
|
-
description: typeof workstream?.description === 'string' ? workstream.description : null,
|
|
1045
|
-
ownerId: typeof workstream?.ownerId === 'string' ? workstream.ownerId.trim() : null,
|
|
1046
|
-
createdAt: String(workstream?.createdAt || raw.updatedAt || raw.watermark || '').trim(),
|
|
1047
|
-
updatedAt: String(workstream?.updatedAt || raw.updatedAt || raw.watermark || workstream?.createdAt || '').trim(),
|
|
1048
|
-
order: Number.isFinite(Number(workstream?.order)) ? Math.floor(Number(workstream?.order)) : null,
|
|
1049
|
-
isArchived: Boolean(workstream?.isArchived)
|
|
1050
|
-
});
|
|
1051
|
-
}
|
|
265
|
+
const workstream = normalizeWorkstreamSyncPayload(raw.workstream, String(raw.updatedAt || raw.watermark || '').trim());
|
|
266
|
+
if (workstream)
|
|
267
|
+
incomingWorkstreams.set(workstream.id, workstream);
|
|
1052
268
|
continue;
|
|
1053
269
|
}
|
|
1054
270
|
if (op === 'ai-profile-upsert') {
|
|
1055
|
-
const profile = raw.profile
|
|
1056
|
-
? raw.profile
|
|
1057
|
-
: null;
|
|
271
|
+
const profile = normalizeAiProfileSyncPayload(raw.profile, workspaceId, normalizeIncomingAiProfileSurfaceType, 'apply.aiProfileUpsert');
|
|
1058
272
|
const profileId = String(profile?.id || '').trim();
|
|
1059
|
-
if (profileId) {
|
|
1060
|
-
const profileWorkspaceId = String(profile
|
|
273
|
+
if (profileId && profile) {
|
|
274
|
+
const profileWorkspaceId = String(profile.workspaceId || '').trim();
|
|
1061
275
|
if (profileWorkspaceId && profileWorkspaceId !== workspaceId) {
|
|
1062
276
|
recordSyncDiagnostic(workspaceId, 'apply', `Rejected ai-profile-upsert for profile ${profileId}: workspace mismatch (expected ${workspaceId}, got ${profileWorkspaceId}).`, {
|
|
1063
277
|
source: 'apply.aiProfileWorkspaceMismatch',
|
|
@@ -1069,185 +283,77 @@ export function registerSyncRoutes(deps) {
|
|
|
1069
283
|
continue;
|
|
1070
284
|
}
|
|
1071
285
|
incomingAiProfiles.set(profileId, {
|
|
1072
|
-
|
|
286
|
+
...profile,
|
|
1073
287
|
workspaceId,
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
username: String(profile?.username || '').trim(),
|
|
1077
|
-
icon: String(profile?.icon || 'Bot').trim() || 'Bot',
|
|
1078
|
-
color: String(profile?.color || '#8b5cf6').trim() || '#8b5cf6',
|
|
1079
|
-
surfaceType: normalizeIncomingAiProfileSurfaceType(profile?.surfaceType, {
|
|
1080
|
-
source: 'apply.aiProfileUpsert',
|
|
1081
|
-
profileId,
|
|
1082
|
-
workspaceId
|
|
1083
|
-
}),
|
|
1084
|
-
providerMetadata: profile?.providerMetadata && typeof profile.providerMetadata === 'object' && !Array.isArray(profile.providerMetadata)
|
|
1085
|
-
? profile.providerMetadata
|
|
1086
|
-
: null,
|
|
1087
|
-
createdAt: String(profile?.createdAt || raw.updatedAt || raw.watermark || '').trim(),
|
|
1088
|
-
updatedAt: String(profile?.updatedAt || raw.updatedAt || raw.watermark || '').trim(),
|
|
1089
|
-
lastActiveAt: typeof profile?.lastActiveAt === 'string' && profile.lastActiveAt.trim().length > 0
|
|
1090
|
-
? profile.lastActiveAt.trim()
|
|
1091
|
-
: null
|
|
288
|
+
createdAt: profile.createdAt || String(raw.updatedAt || raw.watermark || '').trim(),
|
|
289
|
+
updatedAt: profile.updatedAt || String(raw.updatedAt || raw.watermark || '').trim(),
|
|
1092
290
|
});
|
|
1093
291
|
}
|
|
1094
292
|
continue;
|
|
1095
293
|
}
|
|
1096
294
|
if (op === 'document-upsert') {
|
|
1097
|
-
const
|
|
1098
|
-
if (
|
|
1099
|
-
incomingDocuments.push(
|
|
1100
|
-
path: normalizedPath,
|
|
1101
|
-
updatedAt: String(raw.updatedAt || raw.watermark || '').trim(),
|
|
1102
|
-
content: typeof raw.content === 'string' ? raw.content : '',
|
|
1103
|
-
assetId: typeof raw.assetId === 'string' ? raw.assetId.trim() : undefined,
|
|
1104
|
-
documentId: typeof raw.documentId === 'string' ? raw.documentId.trim() : null,
|
|
1105
|
-
referenceNumber: Number.isFinite(Number(raw.referenceNumber))
|
|
1106
|
-
? Math.max(1, Math.floor(Number(raw.referenceNumber)))
|
|
1107
|
-
: null,
|
|
1108
|
-
version: Number.isFinite(Number(raw.version)) ? Math.max(1, Math.floor(Number(raw.version))) : null,
|
|
1109
|
-
taskId: typeof raw.taskId === 'string' ? raw.taskId.trim() : null,
|
|
1110
|
-
logicalName: typeof raw.logicalName === 'string' ? raw.logicalName.trim() : null,
|
|
1111
|
-
caption: typeof raw.caption === 'string' ? raw.caption.trim() : null,
|
|
1112
|
-
originalFilename: typeof raw.originalFilename === 'string' ? raw.originalFilename.trim() : null,
|
|
1113
|
-
linkRole: raw.linkRole === 'reference' ? 'reference' : 'attachment'
|
|
1114
|
-
});
|
|
1115
|
-
}
|
|
295
|
+
const document = normalizeDocumentSyncPayload(raw, normalizeWorkspaceSyncDocumentPath);
|
|
296
|
+
if (document)
|
|
297
|
+
incomingDocuments.push(document);
|
|
1116
298
|
continue;
|
|
1117
299
|
}
|
|
1118
300
|
if (op === 'document-delete') {
|
|
1119
|
-
const
|
|
1120
|
-
if (
|
|
1121
|
-
incomingDocumentDeletes.set(
|
|
1122
|
-
}
|
|
301
|
+
const deletedDocument = normalizeDocumentDeleteSyncPayload(raw, normalizeWorkspaceSyncDocumentPath);
|
|
302
|
+
if (deletedDocument)
|
|
303
|
+
incomingDocumentDeletes.set(deletedDocument.path, deletedDocument.deletedAt);
|
|
1123
304
|
continue;
|
|
1124
305
|
}
|
|
1125
306
|
if (op === 'asset-upsert') {
|
|
1126
|
-
const
|
|
1127
|
-
if (
|
|
1128
|
-
incomingAssets.push(
|
|
1129
|
-
path: normalizedPath,
|
|
1130
|
-
updatedAt: String(raw.updatedAt || raw.watermark || '').trim(),
|
|
1131
|
-
contentBase64: typeof raw.contentBase64 === 'string' ? raw.contentBase64 : '',
|
|
1132
|
-
assetId: typeof raw.assetId === 'string' ? raw.assetId.trim() : undefined,
|
|
1133
|
-
kind: raw.kind === 'image' ? 'image' : 'file',
|
|
1134
|
-
mimeType: typeof raw.mimeType === 'string' ? raw.mimeType.trim() : 'application/octet-stream',
|
|
1135
|
-
referenceNumber: Number.isFinite(Number(raw.referenceNumber))
|
|
1136
|
-
? Math.max(1, Math.floor(Number(raw.referenceNumber)))
|
|
1137
|
-
: null,
|
|
1138
|
-
taskId: typeof raw.taskId === 'string' ? raw.taskId.trim() : null,
|
|
1139
|
-
logicalName: typeof raw.logicalName === 'string' ? raw.logicalName.trim() : null,
|
|
1140
|
-
caption: typeof raw.caption === 'string' ? raw.caption.trim() : null,
|
|
1141
|
-
originalFilename: typeof raw.originalFilename === 'string' ? raw.originalFilename.trim() : null,
|
|
1142
|
-
linkRole: raw.linkRole === 'reference' ? 'reference' : raw.linkRole === 'image' ? 'image' : 'attachment'
|
|
1143
|
-
});
|
|
1144
|
-
}
|
|
307
|
+
const asset = normalizeBinaryAssetSyncPayload(raw, (rawPath) => normalizeTaskDataRelativePath(String(rawPath || '').trim()));
|
|
308
|
+
if (asset)
|
|
309
|
+
incomingAssets.push(asset);
|
|
1145
310
|
continue;
|
|
1146
311
|
}
|
|
1147
312
|
if (op === 'asset-delete') {
|
|
1148
|
-
const
|
|
1149
|
-
if (
|
|
1150
|
-
incomingAssetDeletes.set(
|
|
1151
|
-
}
|
|
313
|
+
const deletedAsset = normalizeBinaryAssetDeleteSyncPayload(raw, (rawPath) => normalizeTaskDataRelativePath(String(rawPath || '').trim()));
|
|
314
|
+
if (deletedAsset)
|
|
315
|
+
incomingAssetDeletes.set(deletedAsset.path, deletedAsset.deletedAt);
|
|
1152
316
|
continue;
|
|
1153
317
|
}
|
|
1154
318
|
if (op === 'document-review-session-upsert') {
|
|
1155
|
-
const session = raw.session
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
const sessionId = String(session?.id || '').trim();
|
|
1159
|
-
const assetId = String(session?.assetId || '').trim();
|
|
1160
|
-
if (sessionId && assetId) {
|
|
1161
|
-
incomingDocumentReviewSessions.set(sessionId, {
|
|
1162
|
-
id: sessionId,
|
|
1163
|
-
assetId,
|
|
1164
|
-
documentId: typeof session?.documentId === 'string' ? session.documentId.trim() : null,
|
|
1165
|
-
documentVersion: Number.isFinite(Number(session?.documentVersion)) ? Math.max(1, Math.floor(Number(session?.documentVersion))) : null,
|
|
1166
|
-
title: typeof session?.title === 'string' ? session.title.trim() : null,
|
|
1167
|
-
status: session?.status === 'resolved' ? 'resolved' : 'open',
|
|
1168
|
-
comments: Array.isArray(session?.comments) ? session.comments : [],
|
|
1169
|
-
createdByActorId: typeof session?.createdByActorId === 'string' ? session.createdByActorId.trim() : null,
|
|
1170
|
-
updatedByActorId: typeof session?.updatedByActorId === 'string' ? session.updatedByActorId.trim() : null,
|
|
1171
|
-
createdAt: String(session?.createdAt || raw.updatedAt || raw.watermark || '').trim(),
|
|
1172
|
-
updatedAt: String(session?.updatedAt || raw.updatedAt || raw.watermark || session?.createdAt || '').trim(),
|
|
1173
|
-
deletedAt: typeof session?.deletedAt === 'string' && session.deletedAt.trim().length > 0 ? session.deletedAt.trim() : null,
|
|
1174
|
-
});
|
|
1175
|
-
}
|
|
319
|
+
const session = normalizeDocumentReviewSessionSyncPayload(raw.session, String(raw.updatedAt || raw.watermark || '').trim());
|
|
320
|
+
if (session)
|
|
321
|
+
incomingDocumentReviewSessions.set(session.id, session);
|
|
1176
322
|
continue;
|
|
1177
323
|
}
|
|
1178
324
|
if (op === 'document-review-session-delete') {
|
|
1179
|
-
const
|
|
1180
|
-
if (
|
|
1181
|
-
incomingDocumentReviewSessionDeletes.set(
|
|
1182
|
-
}
|
|
325
|
+
const deletedSession = normalizeDeleteSyncPayload(raw, 'sessionId');
|
|
326
|
+
if (deletedSession)
|
|
327
|
+
incomingDocumentReviewSessionDeletes.set(deletedSession.id, deletedSession.deletedAt);
|
|
1183
328
|
continue;
|
|
1184
329
|
}
|
|
1185
330
|
if (op === 'document-review-comment-upsert') {
|
|
1186
|
-
const comment = raw.comment
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
const commentId = String(comment?.id || '').trim();
|
|
1190
|
-
const sessionId = String(comment?.sessionId || '').trim();
|
|
1191
|
-
if (commentId && sessionId) {
|
|
1192
|
-
incomingDocumentReviewComments.set(commentId, {
|
|
1193
|
-
id: commentId,
|
|
1194
|
-
sessionId,
|
|
1195
|
-
body: typeof comment?.body === 'string' ? comment.body : '',
|
|
1196
|
-
anchor: comment?.anchor ?? null,
|
|
1197
|
-
order: Number.isFinite(Number(comment?.order)) ? Math.max(0, Math.floor(Number(comment?.order))) : 0,
|
|
1198
|
-
authorActorId: typeof comment?.authorActorId === 'string' ? comment.authorActorId.trim() : null,
|
|
1199
|
-
createdAt: String(comment?.createdAt || raw.updatedAt || raw.watermark || '').trim(),
|
|
1200
|
-
updatedAt: String(comment?.updatedAt || raw.updatedAt || raw.watermark || comment?.createdAt || '').trim(),
|
|
1201
|
-
deletedAt: typeof comment?.deletedAt === 'string' && comment.deletedAt.trim().length > 0 ? comment.deletedAt.trim() : null,
|
|
1202
|
-
});
|
|
1203
|
-
}
|
|
331
|
+
const comment = normalizeDocumentReviewCommentSyncPayload(raw.comment, { sourceWatermark: String(raw.updatedAt || raw.watermark || '').trim() });
|
|
332
|
+
if (comment)
|
|
333
|
+
incomingDocumentReviewComments.set(comment.id, comment);
|
|
1204
334
|
continue;
|
|
1205
335
|
}
|
|
1206
336
|
if (op === 'document-review-comment-delete') {
|
|
1207
|
-
const
|
|
1208
|
-
if (
|
|
1209
|
-
incomingDocumentReviewCommentDeletes.set(
|
|
1210
|
-
}
|
|
337
|
+
const deletedComment = normalizeDeleteSyncPayload(raw, 'commentId');
|
|
338
|
+
if (deletedComment)
|
|
339
|
+
incomingDocumentReviewCommentDeletes.set(deletedComment.id, deletedComment.deletedAt);
|
|
1211
340
|
continue;
|
|
1212
341
|
}
|
|
1213
342
|
if (op === 'annotated-attachment-session-upsert') {
|
|
1214
|
-
const session = raw.session
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
const sessionId = String(session?.id || '').trim();
|
|
1218
|
-
const sessionWorkspaceId = String(session?.workspaceId || '').trim();
|
|
1219
|
-
const taskId = String(session?.taskId || '').trim();
|
|
1220
|
-
const baseImageAssetId = String(session?.baseImageAssetId || '').trim();
|
|
1221
|
-
if (sessionId
|
|
1222
|
-
&& sessionWorkspaceId === workspaceId
|
|
1223
|
-
&& baseImageAssetId) {
|
|
1224
|
-
incomingAnnotatedAttachmentSessions.set(sessionId, {
|
|
1225
|
-
id: sessionId,
|
|
1226
|
-
workspaceId: sessionWorkspaceId,
|
|
1227
|
-
taskId,
|
|
1228
|
-
baseImageAssetId,
|
|
1229
|
-
title: typeof session?.title === 'string' ? session.title.trim() : null,
|
|
1230
|
-
globalInstruction: typeof session?.globalInstruction === 'string' ? session.globalInstruction.trim() : null,
|
|
1231
|
-
annotations: Array.isArray(session?.annotations) ? session.annotations : [],
|
|
1232
|
-
createdByActorId: typeof session?.createdByActorId === 'string' ? session.createdByActorId.trim() : null,
|
|
1233
|
-
updatedByActorId: typeof session?.updatedByActorId === 'string' ? session.updatedByActorId.trim() : null,
|
|
1234
|
-
createdAt: String(session?.createdAt || raw.updatedAt || raw.watermark || '').trim(),
|
|
1235
|
-
updatedAt: String(session?.updatedAt || raw.updatedAt || raw.watermark || session?.createdAt || '').trim(),
|
|
1236
|
-
deletedAt: typeof session?.deletedAt === 'string' && session.deletedAt.trim().length > 0 ? session.deletedAt.trim() : null,
|
|
1237
|
-
});
|
|
1238
|
-
}
|
|
343
|
+
const session = normalizeAnnotatedAttachmentSessionSyncPayload(raw.session, workspaceId, String(raw.updatedAt || raw.watermark || '').trim());
|
|
344
|
+
if (session)
|
|
345
|
+
incomingAnnotatedAttachmentSessions.set(session.id, session);
|
|
1239
346
|
continue;
|
|
1240
347
|
}
|
|
1241
348
|
if (op === 'annotated-attachment-session-delete') {
|
|
1242
|
-
const
|
|
1243
|
-
if (
|
|
1244
|
-
incomingAnnotatedAttachmentSessionDeletes.set(
|
|
1245
|
-
}
|
|
349
|
+
const deletedSession = normalizeDeleteSyncPayload(raw, 'sessionId');
|
|
350
|
+
if (deletedSession)
|
|
351
|
+
incomingAnnotatedAttachmentSessionDeletes.set(deletedSession.id, deletedSession.deletedAt);
|
|
1246
352
|
continue;
|
|
1247
353
|
}
|
|
1248
354
|
if (op !== 'delete')
|
|
1249
355
|
continue;
|
|
1250
|
-
const task = raw.task;
|
|
356
|
+
const task = normalizeTaskSyncPayload(raw.task);
|
|
1251
357
|
const deleteTaskId = String(raw.taskId || raw.id || task?.id || '').trim();
|
|
1252
358
|
if (deleteTaskId) {
|
|
1253
359
|
incomingTaskDeletes.set(deleteTaskId, String(raw.deletedAt || raw.watermark || new Date().toISOString()).trim());
|
|
@@ -1270,13 +376,14 @@ export function registerSyncRoutes(deps) {
|
|
|
1270
376
|
|| op === 'annotated-attachment-session-upsert'
|
|
1271
377
|
|| op === 'annotated-attachment-session-delete')
|
|
1272
378
|
continue;
|
|
1273
|
-
const task = raw.task
|
|
379
|
+
const task = normalizeTaskSyncPayload(raw.task);
|
|
1274
380
|
if (!task)
|
|
1275
381
|
continue;
|
|
1276
382
|
const taskId = String(task.id || '').trim();
|
|
383
|
+
hasTaskReferenceNumberDrift(workspaceId, task);
|
|
1277
384
|
const deleteWatermark = taskId ? String(incomingTaskDeletes.get(taskId) || '').trim() : '';
|
|
1278
385
|
if (taskId && deleteWatermark) {
|
|
1279
|
-
const compareAgainstDelete =
|
|
386
|
+
const compareAgainstDelete = resolveSyncConflictOrdering(deleteWatermark, String(task.updatedAt || task.createdAt || '').trim(), `delete:${taskId}`, getTaskMutationTieBreak(task, raw.archived === true || task?.isArchived === true));
|
|
1280
387
|
if (compareAgainstDelete >= 0)
|
|
1281
388
|
continue;
|
|
1282
389
|
}
|
|
@@ -1290,7 +397,7 @@ export function registerSyncRoutes(deps) {
|
|
|
1290
397
|
try {
|
|
1291
398
|
const existingTask = core.getTask(taskId, workspaceId);
|
|
1292
399
|
if (existingTask) {
|
|
1293
|
-
const compareAgainstExisting =
|
|
400
|
+
const compareAgainstExisting = resolveSyncConflictOrdering(deleteWatermark, getTaskMutationWatermark(existingTask), `delete:${taskId}`, getTaskMutationTieBreak(existingTask, Boolean(existingTask.isArchived)));
|
|
1294
401
|
if (compareAgainstExisting < 0)
|
|
1295
402
|
continue;
|
|
1296
403
|
}
|
|
@@ -1304,435 +411,62 @@ export function registerSyncRoutes(deps) {
|
|
|
1304
411
|
});
|
|
1305
412
|
}
|
|
1306
413
|
}
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
error: String(error?.message || error || '')
|
|
1328
|
-
});
|
|
1329
|
-
throw error;
|
|
1330
|
-
}
|
|
1331
|
-
}
|
|
1332
|
-
}
|
|
1333
|
-
if (incomingWorkstreams.size > 0) {
|
|
1334
|
-
for (const workstream of incomingWorkstreams.values()) {
|
|
1335
|
-
try {
|
|
1336
|
-
core.upsertWorkstreamForSync({
|
|
1337
|
-
id: workstream.id,
|
|
1338
|
-
referenceNumber: workstream.referenceNumber,
|
|
1339
|
-
referenceLabel: workstream.referenceLabel,
|
|
1340
|
-
initiativeId: workstream.initiativeId,
|
|
1341
|
-
title: workstream.title,
|
|
1342
|
-
description: workstream.description,
|
|
1343
|
-
ownerId: workstream.ownerId,
|
|
1344
|
-
createdAt: workstream.createdAt,
|
|
1345
|
-
updatedAt: workstream.updatedAt,
|
|
1346
|
-
order: workstream.order,
|
|
1347
|
-
isArchived: workstream.isArchived
|
|
1348
|
-
}, workspaceId);
|
|
1349
|
-
}
|
|
1350
|
-
catch (error) {
|
|
1351
|
-
recordSyncDiagnostic(workspaceId, 'apply', 'Failed to upsert workstream during sync apply.', {
|
|
1352
|
-
source: 'apply.workstreamUpsert',
|
|
1353
|
-
workstreamId: workstream.id,
|
|
1354
|
-
error: String(error?.message || error || '')
|
|
1355
|
-
});
|
|
1356
|
-
throw error;
|
|
1357
|
-
}
|
|
1358
|
-
}
|
|
1359
|
-
}
|
|
1360
|
-
if (incomingTaxonomyState || Array.isArray(incomingTaxonomies)) {
|
|
1361
|
-
const nextTaxonomyState = incomingTaxonomyState || { taxonomies: incomingTaxonomies || [] };
|
|
1362
|
-
if (!incomingTaxonomiesUpdatedAt) {
|
|
1363
|
-
if (typeof core.updateTaxonomyState === 'function') {
|
|
1364
|
-
core.updateTaxonomyState(nextTaxonomyState, workspaceId);
|
|
1365
|
-
}
|
|
1366
|
-
else {
|
|
1367
|
-
core.updateTaxonomies(Array.isArray(incomingTaxonomies) ? incomingTaxonomies : []);
|
|
1368
|
-
}
|
|
1369
|
-
const fallbackHash = createHash('sha256').update(JSON.stringify(nextTaxonomyState)).digest('hex');
|
|
1370
|
-
workspaceTaxonomySyncMeta.set(workspaceId, {
|
|
1371
|
-
updatedAt: new Date().toISOString(),
|
|
1372
|
-
hash: fallbackHash
|
|
1373
|
-
});
|
|
1374
|
-
}
|
|
1375
|
-
else {
|
|
1376
|
-
const incomingTaxonomyHash = createHash('sha256').update(JSON.stringify(nextTaxonomyState)).digest('hex');
|
|
1377
|
-
const currentMeta = workspaceTaxonomySyncMeta.get(workspaceId) || { updatedAt: '', hash: '' };
|
|
1378
|
-
const compare = resolveConflictOrdering(incomingTaxonomiesUpdatedAt, currentMeta.updatedAt, incomingTaxonomyHash, currentMeta.hash);
|
|
1379
|
-
if (compare >= 0) {
|
|
1380
|
-
if (typeof core.updateTaxonomyState === 'function') {
|
|
1381
|
-
core.updateTaxonomyState(nextTaxonomyState, workspaceId);
|
|
1382
|
-
}
|
|
1383
|
-
else {
|
|
1384
|
-
core.updateTaxonomies(Array.isArray(incomingTaxonomies) ? incomingTaxonomies : []);
|
|
1385
|
-
}
|
|
1386
|
-
workspaceTaxonomySyncMeta.set(workspaceId, {
|
|
1387
|
-
updatedAt: incomingTaxonomiesUpdatedAt,
|
|
1388
|
-
hash: incomingTaxonomyHash
|
|
1389
|
-
});
|
|
1390
|
-
}
|
|
1391
|
-
}
|
|
1392
|
-
}
|
|
1393
|
-
if (incomingAiProfiles.size > 0) {
|
|
1394
|
-
for (const profile of incomingAiProfiles.values()) {
|
|
1395
|
-
try {
|
|
1396
|
-
core.upsertAiProfileForSync({
|
|
1397
|
-
id: profile.id,
|
|
1398
|
-
workspaceId: workspaceId,
|
|
1399
|
-
profileToken: profile.profileToken,
|
|
1400
|
-
name: profile.name,
|
|
1401
|
-
username: profile.username,
|
|
1402
|
-
icon: profile.icon,
|
|
1403
|
-
color: profile.color,
|
|
1404
|
-
surfaceType: profile.surfaceType,
|
|
1405
|
-
providerMetadata: profile.providerMetadata,
|
|
1406
|
-
createdAt: profile.createdAt,
|
|
1407
|
-
updatedAt: profile.updatedAt,
|
|
1408
|
-
lastActiveAt: profile.lastActiveAt
|
|
1409
|
-
});
|
|
1410
|
-
}
|
|
1411
|
-
catch (error) {
|
|
1412
|
-
const details = {
|
|
1413
|
-
source: 'apply.aiProfileUpsert',
|
|
1414
|
-
workspaceId,
|
|
1415
|
-
profileId: profile.id,
|
|
1416
|
-
error: String(error?.message || error || ''),
|
|
1417
|
-
errorCode: String(error?.code || '').trim() || null
|
|
1418
|
-
};
|
|
1419
|
-
recordSyncDiagnostic(workspaceId, 'apply', 'Failed to upsert AI profile during sync apply.', details);
|
|
1420
|
-
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 });
|
|
1421
|
-
}
|
|
1422
|
-
}
|
|
1423
|
-
assertSyncedAiProfilesPersisted(workspaceId, incomingAiProfiles);
|
|
1424
|
-
}
|
|
1425
|
-
if (Array.isArray(options?.workspaceMembers)) {
|
|
1426
|
-
core.replaceWorkspaceUsersForSync({
|
|
1427
|
-
workspaceId,
|
|
1428
|
-
users: options.workspaceMembers
|
|
1429
|
-
.filter((member) => String(member?.workspaceId || workspaceId).trim() === workspaceId)
|
|
1430
|
-
.map((member) => ({
|
|
1431
|
-
userId: member.userId,
|
|
1432
|
-
email: member.email,
|
|
1433
|
-
displayName: member.displayName,
|
|
1434
|
-
role: member.role,
|
|
1435
|
-
permissionMode: member.permissionMode,
|
|
1436
|
-
betaAccess: member.betaAccess,
|
|
1437
|
-
status: member.status,
|
|
1438
|
-
disabled: member.disabled,
|
|
1439
|
-
createdAt: member.createdAt,
|
|
1440
|
-
updatedAt: member.updatedAt,
|
|
1441
|
-
emailVerifiedAt: member.emailVerifiedAt
|
|
1442
|
-
}))
|
|
1443
|
-
});
|
|
1444
|
-
}
|
|
414
|
+
applyPlanningSyncChanges({
|
|
415
|
+
core,
|
|
416
|
+
workspaceId,
|
|
417
|
+
incomingInitiatives,
|
|
418
|
+
incomingWorkstreams,
|
|
419
|
+
incomingTaxonomies,
|
|
420
|
+
incomingTaxonomyState,
|
|
421
|
+
incomingTaxonomiesUpdatedAt,
|
|
422
|
+
workspaceTaxonomySyncMeta,
|
|
423
|
+
recordSyncDiagnostic,
|
|
424
|
+
});
|
|
425
|
+
applyAiProfileAndMemberSyncChanges({
|
|
426
|
+
core,
|
|
427
|
+
workspaceId,
|
|
428
|
+
incomingAiProfiles,
|
|
429
|
+
workspaceMembers: options?.workspaceMembers,
|
|
430
|
+
recordSyncDiagnostic,
|
|
431
|
+
assertSyncedAiProfilesPersisted,
|
|
432
|
+
createSyncRouteError,
|
|
433
|
+
});
|
|
1445
434
|
validateTaskActorAssignments(workspaceId, tasks, archived);
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
}
|
|
1482
|
-
if (incomingAssets.length > 0) {
|
|
1483
|
-
for (const asset of incomingAssets) {
|
|
1484
|
-
try {
|
|
1485
|
-
const existing = workspaceAssetStore?.getByStorageKey(asset.path, workspaceId);
|
|
1486
|
-
const incomingHash = createHash('sha256').update(Buffer.from(String(asset.contentBase64 || ''), 'base64')).digest('hex');
|
|
1487
|
-
const existingUpdatedAt = String(existing?.updatedAt || '').trim();
|
|
1488
|
-
const existingHash = String(existing?.contentSha256 || '').trim();
|
|
1489
|
-
const assetPath = path.resolve(core.getPaths().basePath, asset.path);
|
|
1490
|
-
const localFileMissing = !getObjectStorageClient() && !fs.existsSync(assetPath);
|
|
1491
|
-
const compare = resolveConflictOrdering(asset.updatedAt, existingUpdatedAt, incomingHash, existingHash);
|
|
1492
|
-
const metadataDrift = hasBinaryAssetSyncMetadataDrift(workspaceId, asset, options);
|
|
1493
|
-
if (compare < 0)
|
|
1494
|
-
continue;
|
|
1495
|
-
if (compare === 0 && existingHash === incomingHash && !localFileMissing && !metadataDrift)
|
|
1496
|
-
continue;
|
|
1497
|
-
await writeWorkspaceSyncBinaryAsset(core.getPaths().basePath, asset, workspaceId, options);
|
|
1498
|
-
}
|
|
1499
|
-
catch (error) {
|
|
1500
|
-
recordSyncDiagnostic(workspaceId, 'apply', 'Failed to apply synced asset change.', {
|
|
1501
|
-
source: 'assets.apply.write',
|
|
1502
|
-
path: asset.path,
|
|
1503
|
-
assetId: typeof asset.assetId === 'string' ? asset.assetId : null,
|
|
1504
|
-
kind: asset.kind,
|
|
1505
|
-
error: String(error?.message || error || '')
|
|
1506
|
-
});
|
|
1507
|
-
}
|
|
1508
|
-
}
|
|
1509
|
-
}
|
|
1510
|
-
if (incomingAssetDeletes.size > 0) {
|
|
1511
|
-
for (const [assetPath, deletedAt] of incomingAssetDeletes.entries()) {
|
|
1512
|
-
const existing = workspaceAssetStore?.getByStorageKey(assetPath, workspaceId);
|
|
1513
|
-
const existingUpdatedAt = String(existing?.updatedAt || '').trim();
|
|
1514
|
-
const existingHash = String(existing?.contentSha256 || '').trim();
|
|
1515
|
-
const compare = resolveConflictOrdering(deletedAt, existingUpdatedAt, `delete:${assetPath}`, existingHash);
|
|
1516
|
-
if (existing && compare < 0)
|
|
1517
|
-
continue;
|
|
1518
|
-
deleteWorkspaceSyncBinaryAsset(core.getPaths().basePath, assetPath, workspaceId);
|
|
1519
|
-
}
|
|
1520
|
-
}
|
|
1521
|
-
if (incomingDocumentReviewSessions.size > 0) {
|
|
1522
|
-
for (const session of incomingDocumentReviewSessions.values()) {
|
|
1523
|
-
try {
|
|
1524
|
-
const existing = core.getDocumentReviewSession({
|
|
1525
|
-
workspaceId,
|
|
1526
|
-
sessionId: session.id,
|
|
1527
|
-
includeDeleted: true
|
|
1528
|
-
});
|
|
1529
|
-
const incomingTieBreak = createHash('sha256').update(JSON.stringify(session.comments || [])).digest('hex');
|
|
1530
|
-
const existingTieBreak = existing
|
|
1531
|
-
? createHash('sha256').update(JSON.stringify(existing.comments || [])).digest('hex')
|
|
1532
|
-
: '';
|
|
1533
|
-
const compare = resolveConflictOrdering(session.updatedAt, String(existing?.updatedAt || '').trim(), incomingTieBreak, existingTieBreak);
|
|
1534
|
-
if (compare < 0)
|
|
1535
|
-
continue;
|
|
1536
|
-
core.upsertDocumentReviewSessionForSync({
|
|
1537
|
-
id: session.id,
|
|
1538
|
-
tenantId: core.getTenantId(),
|
|
1539
|
-
workspaceId,
|
|
1540
|
-
assetId: session.assetId,
|
|
1541
|
-
documentId: session.documentId,
|
|
1542
|
-
documentVersion: session.documentVersion,
|
|
1543
|
-
title: session.title,
|
|
1544
|
-
status: session.status,
|
|
1545
|
-
comments: [],
|
|
1546
|
-
createdByActorId: session.createdByActorId,
|
|
1547
|
-
updatedByActorId: session.updatedByActorId,
|
|
1548
|
-
createdAt: session.createdAt,
|
|
1549
|
-
updatedAt: session.updatedAt,
|
|
1550
|
-
deletedAt: session.deletedAt,
|
|
1551
|
-
});
|
|
1552
|
-
}
|
|
1553
|
-
catch (error) {
|
|
1554
|
-
const details = {
|
|
1555
|
-
source: 'apply.documentReviewUpsert',
|
|
1556
|
-
sessionId: session.id,
|
|
1557
|
-
error: String(error?.message || error || ''),
|
|
1558
|
-
errorCode: String(error?.code || '').trim() || null
|
|
1559
|
-
};
|
|
1560
|
-
recordSyncDiagnostic(workspaceId, 'apply', 'Failed to upsert document review session during sync apply.', details);
|
|
1561
|
-
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 });
|
|
1562
|
-
}
|
|
1563
|
-
}
|
|
1564
|
-
}
|
|
1565
|
-
if (incomingDocumentReviewComments.size > 0) {
|
|
1566
|
-
for (const comment of incomingDocumentReviewComments.values()) {
|
|
1567
|
-
try {
|
|
1568
|
-
const existingComments = core.listDocumentReviewCommentsForSync({
|
|
1569
|
-
workspaceId,
|
|
1570
|
-
includeDeleted: true
|
|
1571
|
-
});
|
|
1572
|
-
const existing = existingComments.find((entry) => entry.id === comment.id) || null;
|
|
1573
|
-
const incomingTieBreak = createHash('sha256')
|
|
1574
|
-
.update(JSON.stringify({ body: comment.body, anchor: comment.anchor, deletedAt: comment.deletedAt }))
|
|
1575
|
-
.digest('hex');
|
|
1576
|
-
const existingTieBreak = existing
|
|
1577
|
-
? createHash('sha256')
|
|
1578
|
-
.update(JSON.stringify({ body: existing.body, anchor: existing.anchor, deletedAt: existing.deletedAt }))
|
|
1579
|
-
.digest('hex')
|
|
1580
|
-
: '';
|
|
1581
|
-
const compare = resolveConflictOrdering(comment.updatedAt, String(existing?.updatedAt || '').trim(), incomingTieBreak, existingTieBreak);
|
|
1582
|
-
if (compare < 0)
|
|
1583
|
-
continue;
|
|
1584
|
-
core.upsertDocumentReviewCommentForSync({
|
|
1585
|
-
id: comment.id,
|
|
1586
|
-
tenantId: core.getTenantId(),
|
|
1587
|
-
workspaceId,
|
|
1588
|
-
sessionId: comment.sessionId,
|
|
1589
|
-
body: comment.body,
|
|
1590
|
-
anchor: comment.anchor,
|
|
1591
|
-
order: comment.order,
|
|
1592
|
-
authorActorId: comment.authorActorId,
|
|
1593
|
-
createdAt: comment.createdAt,
|
|
1594
|
-
updatedAt: comment.updatedAt,
|
|
1595
|
-
deletedAt: comment.deletedAt,
|
|
1596
|
-
});
|
|
1597
|
-
}
|
|
1598
|
-
catch (error) {
|
|
1599
|
-
recordSyncDiagnostic(workspaceId, 'apply', 'Failed to upsert document review comment during sync apply.', {
|
|
1600
|
-
source: 'apply.documentReviewCommentUpsert',
|
|
1601
|
-
commentId: comment.id,
|
|
1602
|
-
error: String(error?.message || error || '')
|
|
1603
|
-
});
|
|
1604
|
-
throw error;
|
|
1605
|
-
}
|
|
1606
|
-
}
|
|
1607
|
-
}
|
|
1608
|
-
if (incomingDocumentReviewSessionDeletes.size > 0) {
|
|
1609
|
-
for (const [sessionId, deletedAt] of incomingDocumentReviewSessionDeletes.entries()) {
|
|
1610
|
-
try {
|
|
1611
|
-
const existing = core.getDocumentReviewSession({
|
|
1612
|
-
workspaceId,
|
|
1613
|
-
sessionId,
|
|
1614
|
-
includeDeleted: true
|
|
1615
|
-
});
|
|
1616
|
-
const compare = resolveConflictOrdering(deletedAt, String(existing?.updatedAt || '').trim(), `delete:${sessionId}`, String(existing?.id || '').trim());
|
|
1617
|
-
if (existing && compare < 0)
|
|
1618
|
-
continue;
|
|
1619
|
-
core.deleteDocumentReviewSessionForSync({ workspaceId, sessionId, deletedAt });
|
|
1620
|
-
}
|
|
1621
|
-
catch (error) {
|
|
1622
|
-
const details = {
|
|
1623
|
-
source: 'apply.documentReviewDelete',
|
|
1624
|
-
sessionId,
|
|
1625
|
-
error: String(error?.message || error || ''),
|
|
1626
|
-
errorCode: String(error?.code || '').trim() || null
|
|
1627
|
-
};
|
|
1628
|
-
recordSyncDiagnostic(workspaceId, 'apply', 'Failed to delete document review session during sync apply.', details);
|
|
1629
|
-
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 });
|
|
1630
|
-
}
|
|
1631
|
-
}
|
|
1632
|
-
}
|
|
1633
|
-
if (incomingDocumentReviewCommentDeletes.size > 0) {
|
|
1634
|
-
for (const [commentId, deletedAt] of incomingDocumentReviewCommentDeletes.entries()) {
|
|
1635
|
-
try {
|
|
1636
|
-
const existing = core.listDocumentReviewCommentsForSync({
|
|
1637
|
-
workspaceId,
|
|
1638
|
-
includeDeleted: true
|
|
1639
|
-
}).find((entry) => entry.id === commentId) || null;
|
|
1640
|
-
const compare = resolveConflictOrdering(deletedAt, String(existing?.updatedAt || '').trim(), `delete:${commentId}`, String(existing?.id || '').trim());
|
|
1641
|
-
if (existing && compare < 0)
|
|
1642
|
-
continue;
|
|
1643
|
-
core.deleteDocumentReviewCommentForSync({ workspaceId, commentId, deletedAt });
|
|
1644
|
-
}
|
|
1645
|
-
catch (error) {
|
|
1646
|
-
recordSyncDiagnostic(workspaceId, 'apply', 'Failed to delete document review comment during sync apply.', {
|
|
1647
|
-
source: 'apply.documentReviewCommentDelete',
|
|
1648
|
-
commentId,
|
|
1649
|
-
error: String(error?.message || error || '')
|
|
1650
|
-
});
|
|
1651
|
-
throw error;
|
|
1652
|
-
}
|
|
1653
|
-
}
|
|
1654
|
-
}
|
|
1655
|
-
if (incomingAnnotatedAttachmentSessions.size > 0) {
|
|
1656
|
-
for (const session of incomingAnnotatedAttachmentSessions.values()) {
|
|
1657
|
-
try {
|
|
1658
|
-
const existing = core.getAnnotatedAttachmentSession({
|
|
1659
|
-
workspaceId,
|
|
1660
|
-
sessionId: session.id,
|
|
1661
|
-
includeDeleted: true
|
|
1662
|
-
});
|
|
1663
|
-
const incomingTieBreak = createHash('sha256').update(JSON.stringify(session.annotations || [])).digest('hex');
|
|
1664
|
-
const existingTieBreak = existing
|
|
1665
|
-
? createHash('sha256').update(JSON.stringify(existing.annotations || [])).digest('hex')
|
|
1666
|
-
: '';
|
|
1667
|
-
const compare = resolveConflictOrdering(session.updatedAt, String(existing?.updatedAt || '').trim(), incomingTieBreak, existingTieBreak);
|
|
1668
|
-
if (compare < 0)
|
|
1669
|
-
continue;
|
|
1670
|
-
core.upsertAnnotatedAttachmentSessionForSync({
|
|
1671
|
-
id: session.id,
|
|
1672
|
-
tenantId: core.getTenantId(),
|
|
1673
|
-
workspaceId,
|
|
1674
|
-
taskId: session.taskId,
|
|
1675
|
-
baseImageAssetId: session.baseImageAssetId,
|
|
1676
|
-
title: session.title,
|
|
1677
|
-
globalInstruction: session.globalInstruction,
|
|
1678
|
-
annotations: session.annotations,
|
|
1679
|
-
createdByActorId: session.createdByActorId,
|
|
1680
|
-
updatedByActorId: session.updatedByActorId,
|
|
1681
|
-
createdAt: session.createdAt,
|
|
1682
|
-
updatedAt: session.updatedAt,
|
|
1683
|
-
deletedAt: session.deletedAt,
|
|
1684
|
-
});
|
|
1685
|
-
}
|
|
1686
|
-
catch (error) {
|
|
1687
|
-
const details = {
|
|
1688
|
-
source: 'apply.annotatedAttachmentUpsert',
|
|
1689
|
-
sessionId: session.id,
|
|
1690
|
-
error: String(error?.message || error || ''),
|
|
1691
|
-
errorCode: String(error?.code || '').trim() || null
|
|
1692
|
-
};
|
|
1693
|
-
recordSyncDiagnostic(workspaceId, 'apply', 'Failed to upsert annotated attachment session during sync apply.', details);
|
|
1694
|
-
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 });
|
|
1695
|
-
}
|
|
1696
|
-
}
|
|
1697
|
-
}
|
|
1698
|
-
if (incomingAnnotatedAttachmentSessionDeletes.size > 0) {
|
|
1699
|
-
for (const [sessionId, deletedAt] of incomingAnnotatedAttachmentSessionDeletes.entries()) {
|
|
1700
|
-
try {
|
|
1701
|
-
const existing = core.getAnnotatedAttachmentSession({
|
|
1702
|
-
workspaceId,
|
|
1703
|
-
sessionId,
|
|
1704
|
-
includeDeleted: true
|
|
1705
|
-
});
|
|
1706
|
-
const compare = resolveConflictOrdering(deletedAt, String(existing?.updatedAt || '').trim(), `delete:${sessionId}`, String(existing?.id || '').trim());
|
|
1707
|
-
if (existing && compare < 0)
|
|
1708
|
-
continue;
|
|
1709
|
-
core.deleteAnnotatedAttachmentSessionForSync({ workspaceId, sessionId, deletedAt });
|
|
1710
|
-
}
|
|
1711
|
-
catch (error) {
|
|
1712
|
-
const details = {
|
|
1713
|
-
source: 'apply.annotatedAttachmentDelete',
|
|
1714
|
-
sessionId,
|
|
1715
|
-
error: String(error?.message || error || ''),
|
|
1716
|
-
errorCode: String(error?.code || '').trim() || null
|
|
1717
|
-
};
|
|
1718
|
-
recordSyncDiagnostic(workspaceId, 'apply', 'Failed to delete annotated attachment session during sync apply.', details);
|
|
1719
|
-
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 });
|
|
1720
|
-
}
|
|
1721
|
-
}
|
|
1722
|
-
}
|
|
1723
|
-
if (incomingDocuments.length > 0 || incomingAssets.length > 0) {
|
|
1724
|
-
core.notifyDocumentMetadataMutation(workspaceId, [
|
|
1725
|
-
...incomingDocuments.map((entry) => entry.path),
|
|
1726
|
-
...incomingAssets.map((entry) => entry.path)
|
|
1727
|
-
], 'upsert');
|
|
1728
|
-
}
|
|
1729
|
-
if (incomingDocumentDeletes.size > 0 || incomingAssetDeletes.size > 0) {
|
|
1730
|
-
core.notifyDocumentMetadataMutation(workspaceId, [
|
|
1731
|
-
...Array.from(incomingDocumentDeletes.keys()),
|
|
1732
|
-
...Array.from(incomingAssetDeletes.keys())
|
|
1733
|
-
], 'delete');
|
|
1734
|
-
}
|
|
1735
|
-
const result = core.applyWorkspaceSyncSnapshot({ tasks, archived }, workspaceId);
|
|
435
|
+
await applyContentSyncChanges({
|
|
436
|
+
core,
|
|
437
|
+
workspaceId,
|
|
438
|
+
incomingDocuments,
|
|
439
|
+
incomingDocumentDeletes,
|
|
440
|
+
incomingAssets,
|
|
441
|
+
incomingAssetDeletes,
|
|
442
|
+
basePath: core.getPaths().basePath,
|
|
443
|
+
hasObjectStorageClient: () => Boolean(getObjectStorageClient()),
|
|
444
|
+
hasDocumentSyncMetadataDrift: contentSyncState.hasDocumentSyncMetadataDrift,
|
|
445
|
+
hasBinaryAssetSyncMetadataDrift: contentSyncState.hasBinaryAssetSyncMetadataDrift,
|
|
446
|
+
writeWorkspaceSyncDocument: contentSyncState.writeWorkspaceSyncDocument,
|
|
447
|
+
deleteWorkspaceSyncDocument: contentSyncState.deleteWorkspaceSyncDocument,
|
|
448
|
+
writeWorkspaceSyncBinaryAsset: contentSyncState.writeWorkspaceSyncBinaryAsset,
|
|
449
|
+
deleteWorkspaceSyncBinaryAsset: contentSyncState.deleteWorkspaceSyncBinaryAsset,
|
|
450
|
+
getExistingBinaryAsset: (assetPath, targetWorkspaceId) => workspaceAssetStore?.getByStorageKey(assetPath, targetWorkspaceId) || null,
|
|
451
|
+
recordSyncDiagnostic,
|
|
452
|
+
options,
|
|
453
|
+
});
|
|
454
|
+
applyCollaborationSyncChanges({
|
|
455
|
+
core,
|
|
456
|
+
workspaceId,
|
|
457
|
+
incomingDocumentReviewSessions,
|
|
458
|
+
incomingDocumentReviewSessionDeletes,
|
|
459
|
+
incomingDocumentReviewComments,
|
|
460
|
+
incomingDocumentReviewCommentDeletes,
|
|
461
|
+
incomingAnnotatedAttachmentSessions,
|
|
462
|
+
incomingAnnotatedAttachmentSessionDeletes,
|
|
463
|
+
recordSyncDiagnostic,
|
|
464
|
+
createSyncRouteError,
|
|
465
|
+
});
|
|
466
|
+
const result = core.applyWorkspaceSyncSnapshot({
|
|
467
|
+
tasks: tasks.map(taskSyncPayloadToCoreTask),
|
|
468
|
+
archived: archived.map(taskSyncPayloadToCoreTask)
|
|
469
|
+
}, workspaceId);
|
|
1736
470
|
return { result, deleteCount: incomingTaskDeletes.size, emittedEventIds: Array.from(emittedEventIds) };
|
|
1737
471
|
}
|
|
1738
472
|
finally {
|
|
@@ -2000,135 +734,54 @@ export function registerSyncRoutes(deps) {
|
|
|
2000
734
|
const incomingAssets = Array.isArray(body?.assets) ? body.assets : [];
|
|
2001
735
|
try {
|
|
2002
736
|
const bootstrapChanges = [
|
|
2003
|
-
...((incomingTaxonomyState || incomingTaxonomies)
|
|
737
|
+
...((incomingTaxonomyState || incomingTaxonomies)
|
|
738
|
+
? [normalizeTaxonomySyncPayload({
|
|
739
|
+
taxonomies: incomingTaxonomies,
|
|
740
|
+
taxonomyState: incomingTaxonomyState || undefined,
|
|
741
|
+
updatedAt: new Date().toISOString()
|
|
742
|
+
})]
|
|
743
|
+
.filter((entry) => Boolean(entry))
|
|
744
|
+
.map((entry) => ({
|
|
2004
745
|
op: 'taxonomy-upsert',
|
|
2005
|
-
taxonomies:
|
|
2006
|
-
taxonomyState:
|
|
2007
|
-
updatedAt:
|
|
2008
|
-
}
|
|
746
|
+
taxonomies: entry.taxonomies,
|
|
747
|
+
taxonomyState: entry.taxonomyState,
|
|
748
|
+
updatedAt: entry.updatedAt
|
|
749
|
+
}))
|
|
750
|
+
: []),
|
|
2009
751
|
...incomingInitiatives
|
|
752
|
+
.map((initiative) => normalizeInitiativeSyncPayload(initiative))
|
|
753
|
+
.filter((initiative) => Boolean(initiative && initiative.id.length > 0))
|
|
2010
754
|
.map((initiative) => ({
|
|
2011
755
|
op: 'initiative-upsert',
|
|
2012
|
-
initiative
|
|
2013
|
-
|
|
2014
|
-
referenceNumber: Number.isFinite(Number(initiative?.referenceNumber))
|
|
2015
|
-
? Math.max(1, Math.floor(Number(initiative.referenceNumber)))
|
|
2016
|
-
: null,
|
|
2017
|
-
referenceLabel: typeof initiative?.referenceLabel === 'string' ? initiative.referenceLabel.trim() : undefined,
|
|
2018
|
-
title: String(initiative?.title || '').trim(),
|
|
2019
|
-
description: typeof initiative?.description === 'string' ? initiative.description : null,
|
|
2020
|
-
ownerId: typeof initiative?.ownerId === 'string' ? initiative.ownerId.trim() : null,
|
|
2021
|
-
createdAt: String(initiative?.createdAt || '').trim(),
|
|
2022
|
-
updatedAt: String(initiative?.updatedAt || initiative?.createdAt || '').trim(),
|
|
2023
|
-
order: Number.isFinite(Number(initiative?.order)) ? Math.floor(Number(initiative.order)) : null,
|
|
2024
|
-
isArchived: Boolean(initiative?.isArchived)
|
|
2025
|
-
}
|
|
2026
|
-
}))
|
|
2027
|
-
.filter((change) => change.initiative.id.length > 0),
|
|
756
|
+
initiative
|
|
757
|
+
})),
|
|
2028
758
|
...incomingWorkstreams
|
|
759
|
+
.map((workstream) => normalizeWorkstreamSyncPayload(workstream))
|
|
760
|
+
.filter((workstream) => Boolean(workstream && workstream.id.length > 0))
|
|
2029
761
|
.map((workstream) => ({
|
|
2030
762
|
op: 'workstream-upsert',
|
|
2031
|
-
workstream
|
|
2032
|
-
|
|
2033
|
-
referenceNumber: Number.isFinite(Number(workstream?.referenceNumber))
|
|
2034
|
-
? Math.max(1, Math.floor(Number(workstream.referenceNumber)))
|
|
2035
|
-
: null,
|
|
2036
|
-
referenceLabel: typeof workstream?.referenceLabel === 'string' ? workstream.referenceLabel.trim() : undefined,
|
|
2037
|
-
initiativeId: typeof workstream?.initiativeId === 'string' ? workstream.initiativeId.trim() : null,
|
|
2038
|
-
title: String(workstream?.title || '').trim(),
|
|
2039
|
-
description: typeof workstream?.description === 'string' ? workstream.description : null,
|
|
2040
|
-
ownerId: typeof workstream?.ownerId === 'string' ? workstream.ownerId.trim() : null,
|
|
2041
|
-
createdAt: String(workstream?.createdAt || '').trim(),
|
|
2042
|
-
updatedAt: String(workstream?.updatedAt || workstream?.createdAt || '').trim(),
|
|
2043
|
-
order: Number.isFinite(Number(workstream?.order)) ? Math.floor(Number(workstream.order)) : null,
|
|
2044
|
-
isArchived: Boolean(workstream?.isArchived)
|
|
2045
|
-
}
|
|
2046
|
-
}))
|
|
2047
|
-
.filter((change) => change.workstream.id.length > 0),
|
|
763
|
+
workstream
|
|
764
|
+
})),
|
|
2048
765
|
...incomingAiProfiles
|
|
766
|
+
.map((profile) => normalizeAiProfileSyncPayload(profile, cloudWorkspaceId, normalizeIncomingAiProfileSurfaceType, 'push.aiProfileUpsert'))
|
|
767
|
+
.filter((profile) => Boolean(profile && profile.id.length > 0))
|
|
2049
768
|
.map((profile) => ({
|
|
2050
769
|
op: 'ai-profile-upsert',
|
|
2051
|
-
profile: {
|
|
2052
|
-
|
|
2053
|
-
workspaceId: cloudWorkspaceId,
|
|
2054
|
-
profileToken: String(profile?.profileToken || '').trim(),
|
|
2055
|
-
name: String(profile?.name || '').trim(),
|
|
2056
|
-
username: String(profile?.username || '').trim(),
|
|
2057
|
-
icon: String(profile?.icon || 'Bot').trim() || 'Bot',
|
|
2058
|
-
color: String(profile?.color || '#8b5cf6').trim() || '#8b5cf6',
|
|
2059
|
-
surfaceType: normalizeIncomingAiProfileSurfaceType(profile?.surfaceType, {
|
|
2060
|
-
source: 'push.aiProfileUpsert',
|
|
2061
|
-
profileId: String(profile?.id || '').trim(),
|
|
2062
|
-
workspaceId: cloudWorkspaceId
|
|
2063
|
-
}),
|
|
2064
|
-
providerMetadata: profile?.providerMetadata && typeof profile.providerMetadata === 'object' && !Array.isArray(profile.providerMetadata)
|
|
2065
|
-
? profile.providerMetadata
|
|
2066
|
-
: null,
|
|
2067
|
-
createdAt: String(profile?.createdAt || '').trim(),
|
|
2068
|
-
updatedAt: String(profile?.updatedAt || profile?.createdAt || '').trim(),
|
|
2069
|
-
lastActiveAt: typeof profile?.lastActiveAt === 'string' && profile.lastActiveAt.trim().length > 0
|
|
2070
|
-
? profile.lastActiveAt.trim()
|
|
2071
|
-
: null
|
|
2072
|
-
}
|
|
2073
|
-
}))
|
|
2074
|
-
.filter((change) => change.profile.id.length > 0),
|
|
770
|
+
profile: { ...profile, workspaceId: cloudWorkspaceId }
|
|
771
|
+
})),
|
|
2075
772
|
...incomingDocuments
|
|
2076
|
-
.map((doc) => (
|
|
2077
|
-
|
|
2078
|
-
path: String(doc?.path || '').trim(),
|
|
2079
|
-
updatedAt: String(doc?.updatedAt || '').trim(),
|
|
2080
|
-
content: typeof doc?.content === 'string' ? doc.content : '',
|
|
2081
|
-
assetId: typeof doc?.assetId === 'string' ? doc.assetId.trim() : undefined,
|
|
2082
|
-
documentId: typeof doc?.documentId === 'string' ? doc.documentId.trim() : null,
|
|
2083
|
-
referenceNumber: Number.isFinite(Number(doc?.referenceNumber)) ? Math.max(1, Math.floor(Number(doc.referenceNumber))) : null,
|
|
2084
|
-
version: Number.isFinite(Number(doc?.version)) ? Math.max(1, Math.floor(Number(doc.version))) : null,
|
|
2085
|
-
taskId: typeof doc?.taskId === 'string' ? doc.taskId.trim() : null,
|
|
2086
|
-
logicalName: typeof doc?.logicalName === 'string' ? doc.logicalName.trim() : null,
|
|
2087
|
-
caption: typeof doc?.caption === 'string' ? doc.caption.trim() : null,
|
|
2088
|
-
originalFilename: typeof doc?.originalFilename === 'string' ? doc.originalFilename.trim() : null,
|
|
2089
|
-
linkRole: (doc?.linkRole === 'reference' ? 'reference' : 'attachment')
|
|
2090
|
-
}))
|
|
2091
|
-
.filter((doc) => doc.path.length > 0),
|
|
773
|
+
.map((doc) => serializeDocumentSyncPayload(doc))
|
|
774
|
+
.filter((doc) => Boolean(doc && doc.path.length > 0)),
|
|
2092
775
|
...incomingAssets
|
|
2093
|
-
.map((asset) => (
|
|
2094
|
-
|
|
2095
|
-
path: String(asset?.path || '').trim(),
|
|
2096
|
-
updatedAt: String(asset?.updatedAt || '').trim(),
|
|
2097
|
-
contentBase64: typeof asset?.contentBase64 === 'string' ? asset.contentBase64 : '',
|
|
2098
|
-
assetId: typeof asset?.assetId === 'string' ? asset.assetId.trim() : undefined,
|
|
2099
|
-
kind: asset?.kind === 'image' ? 'image' : 'file',
|
|
2100
|
-
mimeType: typeof asset?.mimeType === 'string' ? asset.mimeType.trim() : 'application/octet-stream',
|
|
2101
|
-
referenceNumber: Number.isFinite(Number(asset?.referenceNumber)) ? Math.max(1, Math.floor(Number(asset.referenceNumber))) : null,
|
|
2102
|
-
taskId: typeof asset?.taskId === 'string' ? asset.taskId.trim() : null,
|
|
2103
|
-
logicalName: typeof asset?.logicalName === 'string' ? asset.logicalName.trim() : null,
|
|
2104
|
-
caption: typeof asset?.caption === 'string' ? asset.caption.trim() : null,
|
|
2105
|
-
originalFilename: typeof asset?.originalFilename === 'string' ? asset.originalFilename.trim() : null,
|
|
2106
|
-
linkRole: asset?.linkRole === 'reference'
|
|
2107
|
-
? 'reference'
|
|
2108
|
-
: asset?.kind === 'image'
|
|
2109
|
-
? 'image'
|
|
2110
|
-
: 'attachment'
|
|
2111
|
-
}))
|
|
2112
|
-
.filter((asset) => asset.path.length > 0 && asset.contentBase64.length > 0),
|
|
776
|
+
.map((asset) => serializeBinaryAssetSyncPayload(asset))
|
|
777
|
+
.filter((asset) => Boolean(asset && asset.path.length > 0 && asset.contentBase64.length > 0)),
|
|
2113
778
|
...incomingDocumentReviewSessions
|
|
779
|
+
.map((session) => normalizeDocumentReviewSessionSyncPayload(session))
|
|
780
|
+
.filter((session) => Boolean(session && session.id.length > 0 && session.assetId.length > 0))
|
|
2114
781
|
.map((session) => ({
|
|
2115
782
|
op: 'document-review-session-upsert',
|
|
2116
|
-
session: {
|
|
2117
|
-
|
|
2118
|
-
assetId: String(session?.assetId || '').trim(),
|
|
2119
|
-
documentId: typeof session?.documentId === 'string' ? session.documentId.trim() : null,
|
|
2120
|
-
documentVersion: Number.isFinite(Number(session?.documentVersion)) ? Math.max(1, Math.floor(Number(session.documentVersion))) : null,
|
|
2121
|
-
title: typeof session?.title === 'string' ? session.title.trim() : null,
|
|
2122
|
-
status: session?.status === 'resolved' ? 'resolved' : 'open',
|
|
2123
|
-
comments: [],
|
|
2124
|
-
createdByActorId: typeof session?.createdByActorId === 'string' ? session.createdByActorId.trim() : null,
|
|
2125
|
-
updatedByActorId: typeof session?.updatedByActorId === 'string' ? session.updatedByActorId.trim() : null,
|
|
2126
|
-
createdAt: String(session?.createdAt || '').trim(),
|
|
2127
|
-
updatedAt: String(session?.updatedAt || session?.createdAt || '').trim(),
|
|
2128
|
-
deletedAt: typeof session?.deletedAt === 'string' && session.deletedAt.trim().length > 0 ? session.deletedAt.trim() : null
|
|
2129
|
-
}
|
|
2130
|
-
}))
|
|
2131
|
-
.filter((change) => change.session.id.length > 0 && change.session.assetId.length > 0),
|
|
783
|
+
session: { ...session, comments: [] }
|
|
784
|
+
})),
|
|
2132
785
|
...(incomingDocumentReviewComments.length > 0
|
|
2133
786
|
? incomingDocumentReviewComments
|
|
2134
787
|
: incomingDocumentReviewSessions.flatMap((session) => {
|
|
@@ -2144,43 +797,22 @@ export function registerSyncRoutes(deps) {
|
|
|
2144
797
|
const sessionId = isLegacyNested
|
|
2145
798
|
? String(entry.sessionId || '').trim()
|
|
2146
799
|
: String(comment?.sessionId || '').trim();
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
body: typeof comment?.body === 'string' ? comment.body : '',
|
|
2153
|
-
anchor: comment?.anchor ?? null,
|
|
2154
|
-
order: Number.isFinite(Number(comment?.order)) ? Math.max(0, Math.floor(Number(comment?.order))) : 0,
|
|
2155
|
-
authorActorId: typeof comment?.authorActorId === 'string' ? comment.authorActorId.trim() : null,
|
|
2156
|
-
createdAt: String(comment?.createdAt || '').trim(),
|
|
2157
|
-
updatedAt: String(comment?.updatedAt || comment?.createdAt || '').trim(),
|
|
2158
|
-
deletedAt: typeof comment?.deletedAt === 'string' && comment.deletedAt.trim().length > 0 ? comment.deletedAt.trim() : null,
|
|
800
|
+
const normalized = normalizeDocumentReviewCommentSyncPayload(comment, { sessionId });
|
|
801
|
+
return normalized
|
|
802
|
+
? {
|
|
803
|
+
op: 'document-review-comment-upsert',
|
|
804
|
+
comment: normalized
|
|
2159
805
|
}
|
|
2160
|
-
|
|
806
|
+
: null;
|
|
2161
807
|
})
|
|
2162
|
-
.filter((change) => change.comment.id.length > 0 && change.comment.sessionId.length > 0),
|
|
808
|
+
.filter((change) => Boolean(change && change.comment.id.length > 0 && change.comment.sessionId.length > 0)),
|
|
2163
809
|
...incomingAnnotatedAttachmentSessions
|
|
810
|
+
.map((session) => normalizeAnnotatedAttachmentSessionSyncPayload(session, cloudWorkspaceId))
|
|
811
|
+
.filter((session) => Boolean(session && session.id.length > 0 && session.workspaceId.length > 0 && session.baseImageAssetId.length > 0))
|
|
2164
812
|
.map((session) => ({
|
|
2165
813
|
op: 'annotated-attachment-session-upsert',
|
|
2166
|
-
session
|
|
2167
|
-
|
|
2168
|
-
workspaceId: cloudWorkspaceId,
|
|
2169
|
-
taskId: typeof session?.taskId === 'string' ? session.taskId.trim() : '',
|
|
2170
|
-
baseImageAssetId: typeof session?.baseImageAssetId === 'string' ? session.baseImageAssetId.trim() : '',
|
|
2171
|
-
title: typeof session?.title === 'string' ? session.title.trim() : null,
|
|
2172
|
-
globalInstruction: typeof session?.globalInstruction === 'string' ? session.globalInstruction.trim() : null,
|
|
2173
|
-
annotations: Array.isArray(session?.annotations) ? session.annotations : [],
|
|
2174
|
-
createdByActorId: typeof session?.createdByActorId === 'string' ? session.createdByActorId.trim() : null,
|
|
2175
|
-
updatedByActorId: typeof session?.updatedByActorId === 'string' ? session.updatedByActorId.trim() : null,
|
|
2176
|
-
createdAt: String(session?.createdAt || '').trim(),
|
|
2177
|
-
updatedAt: String(session?.updatedAt || session?.createdAt || '').trim(),
|
|
2178
|
-
deletedAt: typeof session?.deletedAt === 'string' && session.deletedAt.trim().length > 0 ? session.deletedAt.trim() : null
|
|
2179
|
-
}
|
|
2180
|
-
}))
|
|
2181
|
-
.filter((change) => (change.session.id.length > 0
|
|
2182
|
-
&& change.session.workspaceId.length > 0
|
|
2183
|
-
&& change.session.baseImageAssetId.length > 0)),
|
|
814
|
+
session
|
|
815
|
+
})),
|
|
2184
816
|
...tasks.map((task) => ({ op: 'upsert', archived: false, task })),
|
|
2185
817
|
...archived.map((task) => ({ op: 'upsert', archived: true, task }))
|
|
2186
818
|
];
|
|
@@ -2235,7 +867,7 @@ export function registerSyncRoutes(deps) {
|
|
|
2235
867
|
}
|
|
2236
868
|
const workspaceId = requestWorkspaceId(req) || 'default';
|
|
2237
869
|
const basePath = core.getPaths().basePath;
|
|
2238
|
-
const documents = await listWorkspaceSyncDocumentsSnapshot(basePath, workspaceId);
|
|
870
|
+
const documents = await contentSyncState.listWorkspaceSyncDocumentsSnapshot(basePath, workspaceId);
|
|
2239
871
|
const fingerprint = JSON.stringify(documents.map((doc) => [
|
|
2240
872
|
doc.path,
|
|
2241
873
|
doc.updatedAt,
|
|
@@ -2293,8 +925,8 @@ export function registerSyncRoutes(deps) {
|
|
|
2293
925
|
}
|
|
2294
926
|
}
|
|
2295
927
|
const workspaceId = requestWorkspaceId(req) || 'default';
|
|
2296
|
-
const sessions = listWorkspaceSyncDocumentReviewSessionsSnapshot(workspaceId);
|
|
2297
|
-
const comments = listWorkspaceSyncDocumentReviewCommentsSnapshot(workspaceId);
|
|
928
|
+
const sessions = collaborationSyncState.listWorkspaceSyncDocumentReviewSessionsSnapshot(workspaceId);
|
|
929
|
+
const comments = collaborationSyncState.listWorkspaceSyncDocumentReviewCommentsSnapshot(workspaceId);
|
|
2298
930
|
const fingerprint = JSON.stringify([
|
|
2299
931
|
...sessions.map((session) => [
|
|
2300
932
|
'session',
|
|
@@ -2331,7 +963,7 @@ export function registerSyncRoutes(deps) {
|
|
|
2331
963
|
}
|
|
2332
964
|
}
|
|
2333
965
|
const workspaceId = requestWorkspaceId(req) || 'default';
|
|
2334
|
-
const sessions = listWorkspaceSyncAnnotatedAttachmentSessionsSnapshot(workspaceId);
|
|
966
|
+
const sessions = collaborationSyncState.listWorkspaceSyncAnnotatedAttachmentSessionsSnapshot(workspaceId);
|
|
2335
967
|
const fingerprint = JSON.stringify(sessions.map((session) => [
|
|
2336
968
|
session.id,
|
|
2337
969
|
session.updatedAt,
|
|
@@ -2361,7 +993,7 @@ export function registerSyncRoutes(deps) {
|
|
|
2361
993
|
}
|
|
2362
994
|
const workspaceId = requestWorkspaceId(req) || 'default';
|
|
2363
995
|
const basePath = core.getPaths().basePath;
|
|
2364
|
-
const assets = await listWorkspaceSyncBinaryAssetsSnapshot(basePath, workspaceId);
|
|
996
|
+
const assets = await contentSyncState.listWorkspaceSyncBinaryAssetsSnapshot(basePath, workspaceId);
|
|
2365
997
|
const fingerprint = JSON.stringify(assets.map((asset) => [
|
|
2366
998
|
asset.path,
|
|
2367
999
|
asset.updatedAt,
|
|
@@ -2428,378 +1060,27 @@ export function registerSyncRoutes(deps) {
|
|
|
2428
1060
|
const pageSize = Number.isFinite(requestedLimit) && requestedLimit > 0
|
|
2429
1061
|
? Math.max(1, Math.min(500, Math.floor(requestedLimit)))
|
|
2430
1062
|
: 200;
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
// small in-memory collections (taxonomy, documents) before slicing to pageSize.
|
|
2434
|
-
const syncCursor = cursor ? { watermark: cursor.u, id: cursor.id } : null;
|
|
2435
|
-
const dbFetchLimit = pageSize * 2 + 10;
|
|
2436
|
-
const taskRows = core.listTasksForSync(targetWorkspaceId, syncCursor, dbFetchLimit);
|
|
2437
|
-
const upsertEntries = taskRows
|
|
2438
|
-
.map((entry) => {
|
|
2439
|
-
const watermark = String(entry.task?.updatedAt || entry.task?.createdAt || '').trim();
|
|
2440
|
-
const taskId = String(entry.task?.id || '').trim();
|
|
2441
|
-
return {
|
|
2442
|
-
kind: 'upsert',
|
|
2443
|
-
watermark,
|
|
2444
|
-
sortId: taskId,
|
|
2445
|
-
task: entry.task,
|
|
2446
|
-
archived: Boolean(entry.archived)
|
|
2447
|
-
};
|
|
2448
|
-
})
|
|
2449
|
-
.filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.sortId.length > 0);
|
|
2450
|
-
const rawDeleteEntries = core.listWorkspaceSyncDeletesSince(targetWorkspaceId, syncCursor, dbFetchLimit);
|
|
2451
|
-
const deleteEntries = rawDeleteEntries
|
|
2452
|
-
.map((entry) => ({
|
|
2453
|
-
kind: 'delete',
|
|
2454
|
-
watermark: String(entry.deletedAt || '').trim(),
|
|
2455
|
-
sortId: String(entry.taskId || '').trim(),
|
|
2456
|
-
taskId: String(entry.taskId || '').trim()
|
|
2457
|
-
}))
|
|
2458
|
-
.filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.sortId.length > 0);
|
|
2459
|
-
const taxonomySnapshot = core.getTaxonomiesSyncSnapshot(targetWorkspaceId);
|
|
2460
|
-
const taxonomyEntries = (taxonomySnapshot
|
|
2461
|
-
&& Array.isArray(taxonomySnapshot.taxonomies)
|
|
2462
|
-
&& Number.isFinite(Date.parse(String(taxonomySnapshot.updatedAt || '').trim())))
|
|
2463
|
-
? [{
|
|
2464
|
-
kind: 'taxonomy',
|
|
2465
|
-
watermark: String(taxonomySnapshot.updatedAt || '').trim(),
|
|
2466
|
-
sortId: '__taxonomy_config__',
|
|
2467
|
-
taxonomies: taxonomySnapshot.taxonomies,
|
|
2468
|
-
taxonomyState: taxonomySnapshot.taxonomyState || null
|
|
2469
|
-
}]
|
|
2470
|
-
: [];
|
|
2471
|
-
const initiativeEntries = core.listInitiatives(targetWorkspaceId)
|
|
2472
|
-
.map((initiative) => ({
|
|
2473
|
-
kind: 'initiative',
|
|
2474
|
-
watermark: String(initiative.updatedAt || initiative.createdAt || '').trim(),
|
|
2475
|
-
sortId: `initiative:${String(initiative.id || '').trim()}`,
|
|
2476
|
-
initiative
|
|
2477
|
-
}))
|
|
2478
|
-
.filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.initiative.id.length > 0);
|
|
2479
|
-
const workstreamEntries = core.listWorkstreams(targetWorkspaceId)
|
|
2480
|
-
.map((workstream) => ({
|
|
2481
|
-
kind: 'workstream',
|
|
2482
|
-
watermark: String(workstream.updatedAt || workstream.createdAt || '').trim(),
|
|
2483
|
-
sortId: `workstream:${String(workstream.id || '').trim()}`,
|
|
2484
|
-
workstream
|
|
2485
|
-
}))
|
|
2486
|
-
.filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.workstream.id.length > 0);
|
|
2487
|
-
const aiProfileEntries = core.listAiProfiles(targetWorkspaceId)
|
|
2488
|
-
.map((profile) => ({
|
|
2489
|
-
kind: 'ai-profile',
|
|
2490
|
-
watermark: String(profile.updatedAt || profile.createdAt || '').trim(),
|
|
2491
|
-
sortId: `ai-profile:${String(profile.id || '').trim()}`,
|
|
2492
|
-
profile
|
|
2493
|
-
}))
|
|
2494
|
-
.filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.sortId.length > 0);
|
|
2495
|
-
const documentEntries = (await listWorkspaceSyncDocumentsSnapshot(core.getPaths().basePath, targetWorkspaceId))
|
|
2496
|
-
.map((doc) => ({
|
|
2497
|
-
kind: 'document',
|
|
2498
|
-
watermark: String(doc.updatedAt || '').trim(),
|
|
2499
|
-
sortId: `doc:${String(doc.path || '').trim()}`,
|
|
2500
|
-
path: String(doc.path || '').trim(),
|
|
2501
|
-
content: String(doc.content || ''),
|
|
2502
|
-
assetId: doc.assetId,
|
|
2503
|
-
documentId: doc.documentId,
|
|
2504
|
-
referenceNumber: doc.referenceNumber,
|
|
2505
|
-
version: doc.version,
|
|
2506
|
-
taskId: doc.taskId,
|
|
2507
|
-
logicalName: doc.logicalName,
|
|
2508
|
-
caption: doc.caption,
|
|
2509
|
-
originalFilename: doc.originalFilename,
|
|
2510
|
-
linkRole: doc.linkRole
|
|
2511
|
-
}))
|
|
2512
|
-
.filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.path.length > 0);
|
|
2513
|
-
const documentDeleteEntries = listWorkspaceSyncDocumentDeletesSnapshot(targetWorkspaceId)
|
|
2514
|
-
.map((doc) => ({
|
|
2515
|
-
kind: 'document-delete',
|
|
2516
|
-
watermark: String(doc.deletedAt || '').trim(),
|
|
2517
|
-
sortId: `doc-delete:${String(doc.path || '').trim()}:${String(doc.assetId || '').trim()}`,
|
|
2518
|
-
path: String(doc.path || '').trim(),
|
|
2519
|
-
assetId: typeof doc.assetId === 'string' && doc.assetId.trim().length > 0 ? doc.assetId.trim() : undefined
|
|
2520
|
-
}))
|
|
2521
|
-
.filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.path.length > 0);
|
|
2522
|
-
const documentReviewSessionEntries = listWorkspaceSyncDocumentReviewSessionsSnapshot(targetWorkspaceId)
|
|
2523
|
-
.filter((session) => !session.deletedAt)
|
|
2524
|
-
.map((session) => ({
|
|
2525
|
-
kind: 'document-review-session',
|
|
2526
|
-
watermark: String(session.updatedAt || session.createdAt || '').trim(),
|
|
2527
|
-
sortId: `document-review-session:${String(session.id || '').trim()}`,
|
|
2528
|
-
session
|
|
2529
|
-
}))
|
|
2530
|
-
.filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.session.id.length > 0 && entry.session.assetId.length > 0);
|
|
2531
|
-
const documentReviewSessionDeleteEntries = listWorkspaceSyncDocumentReviewSessionsSnapshot(targetWorkspaceId)
|
|
2532
|
-
.filter((session) => typeof session.deletedAt === 'string' && session.deletedAt.trim().length > 0)
|
|
2533
|
-
.map((session) => ({
|
|
2534
|
-
kind: 'document-review-session-delete',
|
|
2535
|
-
watermark: String(session.deletedAt || session.updatedAt || '').trim(),
|
|
2536
|
-
sortId: `document-review-session-delete:${String(session.id || '').trim()}`,
|
|
2537
|
-
sessionId: String(session.id || '').trim()
|
|
2538
|
-
}))
|
|
2539
|
-
.filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.sessionId.length > 0);
|
|
2540
|
-
const documentReviewCommentEntries = listWorkspaceSyncDocumentReviewCommentsSnapshot(targetWorkspaceId)
|
|
2541
|
-
.filter((comment) => !comment.deletedAt)
|
|
2542
|
-
.map((comment) => ({
|
|
2543
|
-
kind: 'document-review-comment',
|
|
2544
|
-
watermark: String(comment.updatedAt || comment.createdAt || '').trim(),
|
|
2545
|
-
sortId: `document-review-comment:${String(comment.id || '').trim()}`,
|
|
2546
|
-
comment
|
|
2547
|
-
}))
|
|
2548
|
-
.filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.comment.id.length > 0 && entry.comment.sessionId.length > 0);
|
|
2549
|
-
const documentReviewCommentDeleteEntries = listWorkspaceSyncDocumentReviewCommentsSnapshot(targetWorkspaceId)
|
|
2550
|
-
.filter((comment) => typeof comment.deletedAt === 'string' && comment.deletedAt.trim().length > 0)
|
|
2551
|
-
.map((comment) => ({
|
|
2552
|
-
kind: 'document-review-comment-delete',
|
|
2553
|
-
watermark: String(comment.deletedAt || comment.updatedAt || '').trim(),
|
|
2554
|
-
sortId: `document-review-comment-delete:${String(comment.id || '').trim()}`,
|
|
2555
|
-
commentId: String(comment.id || '').trim()
|
|
2556
|
-
}))
|
|
2557
|
-
.filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.commentId.length > 0);
|
|
2558
|
-
const annotatedAttachmentSessionEntries = listWorkspaceSyncAnnotatedAttachmentSessionsSnapshot(targetWorkspaceId)
|
|
2559
|
-
.filter((session) => !session.deletedAt)
|
|
2560
|
-
.map((session) => ({
|
|
2561
|
-
kind: 'annotated-attachment-session',
|
|
2562
|
-
watermark: String(session.updatedAt || session.createdAt || '').trim(),
|
|
2563
|
-
sortId: `annotated-attachment-session:${String(session.id || '').trim()}`,
|
|
2564
|
-
session
|
|
2565
|
-
}))
|
|
2566
|
-
.filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.session.id.length > 0 && entry.session.baseImageAssetId.length > 0);
|
|
2567
|
-
const annotatedAttachmentSessionDeleteEntries = listWorkspaceSyncAnnotatedAttachmentSessionsSnapshot(targetWorkspaceId)
|
|
2568
|
-
.filter((session) => typeof session.deletedAt === 'string' && session.deletedAt.trim().length > 0)
|
|
2569
|
-
.map((session) => ({
|
|
2570
|
-
kind: 'annotated-attachment-session-delete',
|
|
2571
|
-
watermark: String(session.deletedAt || session.updatedAt || '').trim(),
|
|
2572
|
-
sortId: `annotated-attachment-session-delete:${String(session.id || '').trim()}`,
|
|
2573
|
-
sessionId: String(session.id || '').trim()
|
|
2574
|
-
}))
|
|
2575
|
-
.filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.sessionId.length > 0);
|
|
2576
|
-
const assetEntries = (await listWorkspaceSyncBinaryAssetsSnapshot(core.getPaths().basePath, targetWorkspaceId))
|
|
2577
|
-
.map((asset) => ({
|
|
2578
|
-
kind: 'asset',
|
|
2579
|
-
watermark: String(asset.updatedAt || '').trim(),
|
|
2580
|
-
sortId: `asset:${String(asset.path || '').trim()}`,
|
|
2581
|
-
path: String(asset.path || '').trim(),
|
|
2582
|
-
contentBase64: String(asset.contentBase64 || ''),
|
|
2583
|
-
assetId: asset.assetId,
|
|
2584
|
-
kindValue: asset.kind,
|
|
2585
|
-
mimeType: asset.mimeType,
|
|
2586
|
-
referenceNumber: asset.referenceNumber,
|
|
2587
|
-
taskId: asset.taskId,
|
|
2588
|
-
logicalName: asset.logicalName,
|
|
2589
|
-
caption: asset.caption,
|
|
2590
|
-
originalFilename: asset.originalFilename,
|
|
2591
|
-
linkRole: asset.linkRole
|
|
2592
|
-
}))
|
|
2593
|
-
.filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.path.length > 0 && entry.contentBase64.length > 0);
|
|
2594
|
-
const assetDeleteEntries = listWorkspaceSyncBinaryAssetDeletesSnapshot(targetWorkspaceId)
|
|
2595
|
-
.map((asset) => ({
|
|
2596
|
-
kind: 'asset-delete',
|
|
2597
|
-
watermark: String(asset.deletedAt || '').trim(),
|
|
2598
|
-
sortId: `asset-delete:${String(asset.path || '').trim()}:${String(asset.assetId || '').trim()}`,
|
|
2599
|
-
path: String(asset.path || '').trim(),
|
|
2600
|
-
assetId: typeof asset.assetId === 'string' && asset.assetId.trim().length > 0 ? asset.assetId.trim() : undefined
|
|
2601
|
-
}))
|
|
2602
|
-
.filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.path.length > 0);
|
|
2603
|
-
const all = [
|
|
2604
|
-
...upsertEntries,
|
|
2605
|
-
...deleteEntries,
|
|
2606
|
-
...taxonomyEntries,
|
|
2607
|
-
...initiativeEntries,
|
|
2608
|
-
...workstreamEntries,
|
|
2609
|
-
...aiProfileEntries,
|
|
2610
|
-
...documentEntries,
|
|
2611
|
-
...documentDeleteEntries,
|
|
2612
|
-
...documentReviewSessionEntries,
|
|
2613
|
-
...documentReviewSessionDeleteEntries,
|
|
2614
|
-
...documentReviewCommentEntries,
|
|
2615
|
-
...documentReviewCommentDeleteEntries,
|
|
2616
|
-
...annotatedAttachmentSessionEntries,
|
|
2617
|
-
...annotatedAttachmentSessionDeleteEntries,
|
|
2618
|
-
...assetEntries,
|
|
2619
|
-
...assetDeleteEntries
|
|
2620
|
-
];
|
|
2621
|
-
const afterCursor = all.filter((entry) => {
|
|
2622
|
-
if (!cursor)
|
|
2623
|
-
return true;
|
|
2624
|
-
const watermark = entry.watermark;
|
|
2625
|
-
const sortId = entry.sortId;
|
|
2626
|
-
if (watermark > cursor.u)
|
|
2627
|
-
return true;
|
|
2628
|
-
if (watermark < cursor.u)
|
|
2629
|
-
return false;
|
|
2630
|
-
return sortId > cursor.id;
|
|
2631
|
-
});
|
|
2632
|
-
afterCursor.sort((a, b) => {
|
|
2633
|
-
if (a.watermark !== b.watermark)
|
|
2634
|
-
return a.watermark.localeCompare(b.watermark);
|
|
2635
|
-
return String(a.sortId || '').localeCompare(String(b.sortId || ''));
|
|
2636
|
-
});
|
|
2637
|
-
const page = afterCursor.slice(0, pageSize);
|
|
2638
|
-
const hasMore = afterCursor.length > pageSize;
|
|
2639
|
-
const last = page.length > 0 ? page[page.length - 1] : null;
|
|
2640
|
-
const nextCursor = encodeWorkspaceSyncCursor(last
|
|
2641
|
-
? { u: last.watermark, id: String(last.sortId || '') }
|
|
2642
|
-
: cursor);
|
|
2643
|
-
const workspaceMembers = core.listWorkspaceUsers(targetWorkspaceId).map((member) => ({
|
|
2644
|
-
userId: member.userId,
|
|
1063
|
+
const { workspaceMembers, changes, nextCursor, hasMore } = await buildWorkspacePullFeed({
|
|
1064
|
+
core,
|
|
2645
1065
|
workspaceId: targetWorkspaceId,
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
disabled: member.disabled,
|
|
2653
|
-
createdAt: member.createdAt || null,
|
|
2654
|
-
updatedAt: member.updatedAt || null,
|
|
2655
|
-
emailVerifiedAt: member.emailVerifiedAt || null
|
|
2656
|
-
}));
|
|
1066
|
+
cursor,
|
|
1067
|
+
pageSize,
|
|
1068
|
+
encodeWorkspaceSyncCursor,
|
|
1069
|
+
contentSyncState,
|
|
1070
|
+
collaborationSyncState,
|
|
1071
|
+
});
|
|
2657
1072
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2658
1073
|
res.end(JSON.stringify({
|
|
2659
1074
|
success: true,
|
|
2660
1075
|
workspaceId: targetWorkspaceId,
|
|
2661
1076
|
workspaceMembers,
|
|
2662
|
-
changes
|
|
2663
|
-
? {
|
|
2664
|
-
op: 'delete',
|
|
2665
|
-
taskId: entry.taskId,
|
|
2666
|
-
deletedAt: entry.watermark,
|
|
2667
|
-
watermark: entry.watermark
|
|
2668
|
-
}
|
|
2669
|
-
: entry.kind === 'taxonomy'
|
|
2670
|
-
? {
|
|
2671
|
-
op: 'taxonomy-upsert',
|
|
2672
|
-
taxonomies: entry.taxonomies,
|
|
2673
|
-
taxonomyState: entry.taxonomyState,
|
|
2674
|
-
watermark: entry.watermark
|
|
2675
|
-
}
|
|
2676
|
-
: entry.kind === 'initiative'
|
|
2677
|
-
? {
|
|
2678
|
-
op: 'initiative-upsert',
|
|
2679
|
-
initiative: entry.initiative,
|
|
2680
|
-
updatedAt: entry.watermark,
|
|
2681
|
-
watermark: entry.watermark
|
|
2682
|
-
}
|
|
2683
|
-
: entry.kind === 'workstream'
|
|
2684
|
-
? {
|
|
2685
|
-
op: 'workstream-upsert',
|
|
2686
|
-
workstream: entry.workstream,
|
|
2687
|
-
updatedAt: entry.watermark,
|
|
2688
|
-
watermark: entry.watermark
|
|
2689
|
-
}
|
|
2690
|
-
: entry.kind === 'ai-profile'
|
|
2691
|
-
? {
|
|
2692
|
-
op: 'ai-profile-upsert',
|
|
2693
|
-
profile: entry.profile,
|
|
2694
|
-
updatedAt: entry.watermark,
|
|
2695
|
-
watermark: entry.watermark
|
|
2696
|
-
}
|
|
2697
|
-
: entry.kind === 'document-review-session'
|
|
2698
|
-
? {
|
|
2699
|
-
op: 'document-review-session-upsert',
|
|
2700
|
-
session: entry.session,
|
|
2701
|
-
updatedAt: entry.watermark,
|
|
2702
|
-
watermark: entry.watermark
|
|
2703
|
-
}
|
|
2704
|
-
: entry.kind === 'document-review-session-delete'
|
|
2705
|
-
? {
|
|
2706
|
-
op: 'document-review-session-delete',
|
|
2707
|
-
sessionId: entry.sessionId,
|
|
2708
|
-
deletedAt: entry.watermark,
|
|
2709
|
-
watermark: entry.watermark
|
|
2710
|
-
}
|
|
2711
|
-
: entry.kind === 'document-review-comment'
|
|
2712
|
-
? {
|
|
2713
|
-
op: 'document-review-comment-upsert',
|
|
2714
|
-
comment: entry.comment,
|
|
2715
|
-
updatedAt: entry.watermark,
|
|
2716
|
-
watermark: entry.watermark
|
|
2717
|
-
}
|
|
2718
|
-
: entry.kind === 'document-review-comment-delete'
|
|
2719
|
-
? {
|
|
2720
|
-
op: 'document-review-comment-delete',
|
|
2721
|
-
commentId: entry.commentId,
|
|
2722
|
-
deletedAt: entry.watermark,
|
|
2723
|
-
watermark: entry.watermark
|
|
2724
|
-
}
|
|
2725
|
-
: entry.kind === 'annotated-attachment-session'
|
|
2726
|
-
? {
|
|
2727
|
-
op: 'annotated-attachment-session-upsert',
|
|
2728
|
-
session: entry.session,
|
|
2729
|
-
updatedAt: entry.watermark,
|
|
2730
|
-
watermark: entry.watermark
|
|
2731
|
-
}
|
|
2732
|
-
: entry.kind === 'annotated-attachment-session-delete'
|
|
2733
|
-
? {
|
|
2734
|
-
op: 'annotated-attachment-session-delete',
|
|
2735
|
-
sessionId: entry.sessionId,
|
|
2736
|
-
deletedAt: entry.watermark,
|
|
2737
|
-
watermark: entry.watermark
|
|
2738
|
-
}
|
|
2739
|
-
// Keep markdown and binary assets on separate ops so the protocol stays explicit:
|
|
2740
|
-
// document-* carries markdown content + document identity, asset-* carries image/file bytes.
|
|
2741
|
-
: entry.kind === 'document'
|
|
2742
|
-
? {
|
|
2743
|
-
op: 'document-upsert',
|
|
2744
|
-
path: entry.path,
|
|
2745
|
-
updatedAt: entry.watermark,
|
|
2746
|
-
content: entry.content,
|
|
2747
|
-
assetId: entry.assetId,
|
|
2748
|
-
documentId: entry.documentId,
|
|
2749
|
-
referenceNumber: entry.referenceNumber,
|
|
2750
|
-
version: entry.version,
|
|
2751
|
-
taskId: entry.taskId,
|
|
2752
|
-
logicalName: entry.logicalName,
|
|
2753
|
-
caption: entry.caption,
|
|
2754
|
-
originalFilename: entry.originalFilename,
|
|
2755
|
-
linkRole: entry.linkRole,
|
|
2756
|
-
watermark: entry.watermark
|
|
2757
|
-
}
|
|
2758
|
-
: entry.kind === 'document-delete'
|
|
2759
|
-
? {
|
|
2760
|
-
op: 'document-delete',
|
|
2761
|
-
path: entry.path,
|
|
2762
|
-
assetId: entry.assetId,
|
|
2763
|
-
deletedAt: entry.watermark,
|
|
2764
|
-
watermark: entry.watermark
|
|
2765
|
-
}
|
|
2766
|
-
: entry.kind === 'asset'
|
|
2767
|
-
? {
|
|
2768
|
-
op: 'asset-upsert',
|
|
2769
|
-
path: entry.path,
|
|
2770
|
-
updatedAt: entry.watermark,
|
|
2771
|
-
contentBase64: entry.contentBase64,
|
|
2772
|
-
assetId: entry.assetId,
|
|
2773
|
-
kind: entry.kindValue,
|
|
2774
|
-
mimeType: entry.mimeType,
|
|
2775
|
-
referenceNumber: entry.referenceNumber,
|
|
2776
|
-
taskId: entry.taskId,
|
|
2777
|
-
logicalName: entry.logicalName,
|
|
2778
|
-
caption: entry.caption,
|
|
2779
|
-
originalFilename: entry.originalFilename,
|
|
2780
|
-
linkRole: entry.linkRole,
|
|
2781
|
-
watermark: entry.watermark
|
|
2782
|
-
}
|
|
2783
|
-
: entry.kind === 'asset-delete'
|
|
2784
|
-
? {
|
|
2785
|
-
op: 'asset-delete',
|
|
2786
|
-
path: entry.path,
|
|
2787
|
-
assetId: entry.assetId,
|
|
2788
|
-
deletedAt: entry.watermark,
|
|
2789
|
-
watermark: entry.watermark
|
|
2790
|
-
}
|
|
2791
|
-
: {
|
|
2792
|
-
op: 'upsert',
|
|
2793
|
-
task: entry.task,
|
|
2794
|
-
archived: Boolean(entry.archived),
|
|
2795
|
-
watermark: entry.watermark
|
|
2796
|
-
})),
|
|
1077
|
+
changes,
|
|
2797
1078
|
nextCursor,
|
|
2798
1079
|
hasMore
|
|
2799
1080
|
}));
|
|
2800
1081
|
auditAuthAction(req, 'sync-workspace-pull-success', {
|
|
2801
1082
|
workspaceId: targetWorkspaceId,
|
|
2802
|
-
returnedChanges:
|
|
1083
|
+
returnedChanges: changes.length,
|
|
2803
1084
|
hasMore
|
|
2804
1085
|
});
|
|
2805
1086
|
});
|
|
@@ -2995,6 +1276,27 @@ export function registerSyncRoutes(deps) {
|
|
|
2995
1276
|
}
|
|
2996
1277
|
incomingChanges = decryptedChanges;
|
|
2997
1278
|
}
|
|
1279
|
+
const receivedAtBaseMs = Date.now();
|
|
1280
|
+
incomingChanges = incomingChanges.map((change, index) => {
|
|
1281
|
+
if (change.op !== 'upsert')
|
|
1282
|
+
return change;
|
|
1283
|
+
const task = change.task;
|
|
1284
|
+
const rawReferenceNumber = task.referenceNumber;
|
|
1285
|
+
const incomingReferenceNumber = rawReferenceNumber === undefined || rawReferenceNumber === null
|
|
1286
|
+
? null
|
|
1287
|
+
: (Number.isFinite(Number(rawReferenceNumber))
|
|
1288
|
+
? Math.max(1, Math.floor(Number(rawReferenceNumber)))
|
|
1289
|
+
: null);
|
|
1290
|
+
if (incomingReferenceNumber)
|
|
1291
|
+
return change;
|
|
1292
|
+
return {
|
|
1293
|
+
...change,
|
|
1294
|
+
task: {
|
|
1295
|
+
...task,
|
|
1296
|
+
updatedAt: new Date(receivedAtBaseMs + index).toISOString()
|
|
1297
|
+
}
|
|
1298
|
+
};
|
|
1299
|
+
});
|
|
2998
1300
|
try {
|
|
2999
1301
|
const { result, deleteCount, emittedEventIds } = await dispatchWorkspaceSyncChanges(targetWorkspaceId, incomingChanges, {
|
|
3000
1302
|
allowReferenceNumberReassignment
|