@taskforcehq/taskforce 0.3.301 → 0.3.303
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Taskforce.module.css +37 -2
- package/dist/TaskforceCore.filter-persistence.test.js +1 -1
- package/dist/TaskforceCore.js +72 -33
- package/dist/TaskforceCore.test.js +848 -130
- 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/task/TaskKanban.test.js +17 -0
- package/dist/components/ui/ReferenceBadgeButton.js +1 -1
- package/dist/components/views/PlansPage.d.ts +3 -1
- package/dist/components/views/PlansPage.js +109 -49
- package/dist/components/views/PlansPage.test.d.ts +1 -0
- package/dist/components/views/PlansPage.test.js +100 -0
- package/dist/components/views/StandaloneLayout.d.ts +1 -0
- package/dist/components/views/StandaloneLayout.js +473 -256
- package/dist/components/views/panels/PlanningDrawer.js +18 -1
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +48 -0
- package/dist/components/views/standalone/modals/AccountHubModal.js +36 -0
- package/dist/components/views/standalone/modals/AccountHubModal.test.d.ts +1 -0
- package/dist/components/views/standalone/modals/AccountHubModal.test.js +26 -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/EntitlementsPolicy.test.js +2 -2
- package/dist/core/GlobalSettingsService.js +78 -18
- package/dist/core/PlanEntitlementService.d.ts +29 -5
- package/dist/core/PlanEntitlementService.js +297 -219
- package/dist/core/SignupMonetizationSettings.test.js +173 -49
- package/dist/core/SyncReconciliationService.d.ts +2 -0
- package/dist/core/SyncReconciliationService.js +3 -0
- package/dist/core/SystemAdmin.test.js +186 -96
- package/dist/core/TaskAuditTimeline.test.js +40 -0
- package/dist/core/Taskforce.d.ts +100 -10
- package/dist/core/Taskforce.ids.test.js +133 -0
- package/dist/core/Taskforce.js +459 -127
- package/dist/core/Taskforce.mcpAuth.test.js +0 -2
- package/dist/core/UserProfileAvatarDrafts.test.d.ts +1 -0
- package/dist/core/UserProfileAvatarDrafts.test.js +111 -0
- package/dist/core/taskReferencePolicy.d.ts +28 -0
- package/dist/core/taskReferencePolicy.js +189 -0
- package/dist/core/types.d.ts +13 -12
- package/dist/hooks/tasks/useTaskFormActions.js +3 -0
- package/dist/hooks/useSyncOrchestrator.js +17 -3
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +97 -5
- package/dist/hooks/useTaskData.js +3 -2
- package/dist/hooks/useTaskData.test.js +66 -0
- package/dist/hooks/useTaskforce.d.ts +8 -0
- package/dist/hooks/useTaskforce.js +72 -19
- 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 +131 -48
- package/dist/mcp/runtime.test.js +321 -83
- package/dist/migrations/billingSchemaParity.test.js +3 -0
- package/dist/migrations/taskSchemaMigrations.js +68 -1
- package/dist/qaOpen.test.d.ts +1 -0
- package/dist/qaOpen.test.js +56 -0
- package/dist/server/index.cookieProxy.test.js +2 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +17 -1
- package/dist/server/index.test.js +15 -1
- package/dist/server/mockStripe.d.ts +1 -0
- package/dist/server/mockStripe.js +2 -0
- package/dist/server/routes/admin.js +105 -45
- package/dist/server/routes/auth.d.ts +6 -0
- package/dist/server/routes/auth.js +505 -30
- package/dist/server/routes/billing.js +517 -24
- package/dist/server/routes/billing.test.js +967 -45
- package/dist/server/routes/documents.js +6 -1
- package/dist/server/routes/shared.d.ts +1 -1
- package/dist/server/routes/shared.js +16 -6
- package/dist/server/routes/sync.integration.test.js +290 -0
- package/dist/server/routes/sync.js +234 -1922
- package/dist/server/routes/tasks.js +18 -4
- package/dist/server/routes.js +29 -22
- package/dist/server/routes.test.js +639 -82
- package/dist/shared/runtimeContract.d.ts +1 -1
- package/dist/sync/cloudSyncApi.d.ts +1 -0
- 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 +98 -0
- package/dist/sync/contentSyncState.js +1004 -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/syncService.d.ts +1 -0
- package/dist/sync/syncService.js +21 -8
- 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 +38 -0
- package/dist/sync/workspacePullFeed.js +349 -0
- package/dist/sync/workspacePullFeed.test.d.ts +1 -0
- package/dist/sync/workspacePullFeed.test.js +278 -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-Bdq3T1Ts.js → AgentsModule-CpsTmrIW.js} +1 -1
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-jKUVo-PJ.js → AnnotatedAttachmentWorkspace-C_TmXZwA.js} +1 -1
- package/dist/ui/assets/{DocumentWorkspace-DuQa-uIC.js → DocumentWorkspace-C3GyzrWm.js} +2 -2
- package/dist/ui/assets/{InitiativesModule-C8Pi2VTk.js → InitiativesModule-4-Rh2K2n.js} +1 -1
- package/dist/ui/assets/PlansPage-BP7AYOqr.js +1 -0
- package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +8 -0
- package/dist/ui/assets/{TaskSettings-BwB9NtmJ.css → TaskSettings-CnIBL_Eb.css} +1 -1
- package/dist/ui/assets/{WorkflowsModule-b6TLuHwJ.js → WorkflowsModule-CGk9s3NJ.js} +1 -1
- package/dist/ui/assets/index-CLIMgR6g.js +6 -0
- package/dist/ui/assets/index-DneETxcu.css +1 -0
- package/dist/ui/assets/{vendor-icons-QZyhEwgT.js → vendor-icons-pWocEipT.js} +1 -1
- package/dist/ui/index.html +3 -3
- package/dist/utils/accountProfileSummaryCache.d.ts +31 -0
- package/dist/utils/accountProfileSummaryCache.js +97 -0
- package/dist/utils/accountProfileSummaryCache.test.d.ts +1 -0
- package/dist/utils/accountProfileSummaryCache.test.js +63 -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 -3
- 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-B6BDECSi.js +0 -1
- package/dist/ui/assets/TaskSettings-DUJgRTPU.js +0 -8
- package/dist/ui/assets/index-BiiF1cLQ.css +0 -1
- package/dist/ui/assets/index-xBWLPOEb.js +0 -6
|
@@ -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,62 @@ 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
|
|
773
|
+
.map((doc) => serializeDocumentSyncPayload(doc))
|
|
774
|
+
.filter((doc) => Boolean(doc && doc.path.length > 0))
|
|
2076
775
|
.map((doc) => ({
|
|
2077
776
|
op: 'document-upsert',
|
|
2078
|
-
|
|
2079
|
-
|
|
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),
|
|
777
|
+
...doc
|
|
778
|
+
})),
|
|
2092
779
|
...incomingAssets
|
|
780
|
+
.map((asset) => serializeBinaryAssetSyncPayload(asset))
|
|
781
|
+
.filter((asset) => Boolean(asset && asset.path.length > 0 && asset.contentBase64.length > 0))
|
|
2093
782
|
.map((asset) => ({
|
|
2094
783
|
op: 'asset-upsert',
|
|
2095
|
-
|
|
2096
|
-
|
|
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),
|
|
784
|
+
...asset
|
|
785
|
+
})),
|
|
2113
786
|
...incomingDocumentReviewSessions
|
|
787
|
+
.map((session) => normalizeDocumentReviewSessionSyncPayload(session))
|
|
788
|
+
.filter((session) => Boolean(session && session.id.length > 0 && session.assetId.length > 0))
|
|
2114
789
|
.map((session) => ({
|
|
2115
790
|
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),
|
|
791
|
+
session: { ...session, comments: [] }
|
|
792
|
+
})),
|
|
2132
793
|
...(incomingDocumentReviewComments.length > 0
|
|
2133
794
|
? incomingDocumentReviewComments
|
|
2134
795
|
: incomingDocumentReviewSessions.flatMap((session) => {
|
|
@@ -2144,43 +805,22 @@ export function registerSyncRoutes(deps) {
|
|
|
2144
805
|
const sessionId = isLegacyNested
|
|
2145
806
|
? String(entry.sessionId || '').trim()
|
|
2146
807
|
: 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,
|
|
808
|
+
const normalized = normalizeDocumentReviewCommentSyncPayload(comment, { sessionId });
|
|
809
|
+
return normalized
|
|
810
|
+
? {
|
|
811
|
+
op: 'document-review-comment-upsert',
|
|
812
|
+
comment: normalized
|
|
2159
813
|
}
|
|
2160
|
-
|
|
814
|
+
: null;
|
|
2161
815
|
})
|
|
2162
|
-
.filter((change) => change.comment.id.length > 0 && change.comment.sessionId.length > 0),
|
|
816
|
+
.filter((change) => Boolean(change && change.comment.id.length > 0 && change.comment.sessionId.length > 0)),
|
|
2163
817
|
...incomingAnnotatedAttachmentSessions
|
|
818
|
+
.map((session) => normalizeAnnotatedAttachmentSessionSyncPayload(session, cloudWorkspaceId))
|
|
819
|
+
.filter((session) => Boolean(session && session.id.length > 0 && session.workspaceId.length > 0 && session.baseImageAssetId.length > 0))
|
|
2164
820
|
.map((session) => ({
|
|
2165
821
|
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)),
|
|
822
|
+
session
|
|
823
|
+
})),
|
|
2184
824
|
...tasks.map((task) => ({ op: 'upsert', archived: false, task })),
|
|
2185
825
|
...archived.map((task) => ({ op: 'upsert', archived: true, task }))
|
|
2186
826
|
];
|
|
@@ -2235,7 +875,7 @@ export function registerSyncRoutes(deps) {
|
|
|
2235
875
|
}
|
|
2236
876
|
const workspaceId = requestWorkspaceId(req) || 'default';
|
|
2237
877
|
const basePath = core.getPaths().basePath;
|
|
2238
|
-
const documents = await listWorkspaceSyncDocumentsSnapshot(basePath, workspaceId);
|
|
878
|
+
const documents = await contentSyncState.listWorkspaceSyncDocumentsSnapshot(basePath, workspaceId);
|
|
2239
879
|
const fingerprint = JSON.stringify(documents.map((doc) => [
|
|
2240
880
|
doc.path,
|
|
2241
881
|
doc.updatedAt,
|
|
@@ -2293,8 +933,8 @@ export function registerSyncRoutes(deps) {
|
|
|
2293
933
|
}
|
|
2294
934
|
}
|
|
2295
935
|
const workspaceId = requestWorkspaceId(req) || 'default';
|
|
2296
|
-
const sessions = listWorkspaceSyncDocumentReviewSessionsSnapshot(workspaceId);
|
|
2297
|
-
const comments = listWorkspaceSyncDocumentReviewCommentsSnapshot(workspaceId);
|
|
936
|
+
const sessions = collaborationSyncState.listWorkspaceSyncDocumentReviewSessionsSnapshot(workspaceId);
|
|
937
|
+
const comments = collaborationSyncState.listWorkspaceSyncDocumentReviewCommentsSnapshot(workspaceId);
|
|
2298
938
|
const fingerprint = JSON.stringify([
|
|
2299
939
|
...sessions.map((session) => [
|
|
2300
940
|
'session',
|
|
@@ -2331,7 +971,7 @@ export function registerSyncRoutes(deps) {
|
|
|
2331
971
|
}
|
|
2332
972
|
}
|
|
2333
973
|
const workspaceId = requestWorkspaceId(req) || 'default';
|
|
2334
|
-
const sessions = listWorkspaceSyncAnnotatedAttachmentSessionsSnapshot(workspaceId);
|
|
974
|
+
const sessions = collaborationSyncState.listWorkspaceSyncAnnotatedAttachmentSessionsSnapshot(workspaceId);
|
|
2335
975
|
const fingerprint = JSON.stringify(sessions.map((session) => [
|
|
2336
976
|
session.id,
|
|
2337
977
|
session.updatedAt,
|
|
@@ -2361,7 +1001,7 @@ export function registerSyncRoutes(deps) {
|
|
|
2361
1001
|
}
|
|
2362
1002
|
const workspaceId = requestWorkspaceId(req) || 'default';
|
|
2363
1003
|
const basePath = core.getPaths().basePath;
|
|
2364
|
-
const assets = await listWorkspaceSyncBinaryAssetsSnapshot(basePath, workspaceId);
|
|
1004
|
+
const assets = await contentSyncState.listWorkspaceSyncBinaryAssetsSnapshot(basePath, workspaceId);
|
|
2365
1005
|
const fingerprint = JSON.stringify(assets.map((asset) => [
|
|
2366
1006
|
asset.path,
|
|
2367
1007
|
asset.updatedAt,
|
|
@@ -2428,379 +1068,30 @@ export function registerSyncRoutes(deps) {
|
|
|
2428
1068
|
const pageSize = Number.isFinite(requestedLimit) && requestedLimit > 0
|
|
2429
1069
|
? Math.max(1, Math.min(500, Math.floor(requestedLimit)))
|
|
2430
1070
|
: 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,
|
|
1071
|
+
const { workspaceMembers, changes, nextCursor, hasMore, diagnostics } = await buildWorkspacePullFeed({
|
|
1072
|
+
core,
|
|
2645
1073
|
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
|
-
}));
|
|
1074
|
+
cursor,
|
|
1075
|
+
pageSize,
|
|
1076
|
+
encodeWorkspaceSyncCursor,
|
|
1077
|
+
contentSyncState,
|
|
1078
|
+
collaborationSyncState,
|
|
1079
|
+
});
|
|
2657
1080
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2658
1081
|
res.end(JSON.stringify({
|
|
2659
1082
|
success: true,
|
|
2660
1083
|
workspaceId: targetWorkspaceId,
|
|
2661
1084
|
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
|
-
})),
|
|
1085
|
+
changes,
|
|
2797
1086
|
nextCursor,
|
|
2798
|
-
hasMore
|
|
1087
|
+
hasMore,
|
|
1088
|
+
diagnostics
|
|
2799
1089
|
}));
|
|
2800
1090
|
auditAuthAction(req, 'sync-workspace-pull-success', {
|
|
2801
1091
|
workspaceId: targetWorkspaceId,
|
|
2802
|
-
returnedChanges:
|
|
2803
|
-
hasMore
|
|
1092
|
+
returnedChanges: changes.length,
|
|
1093
|
+
hasMore,
|
|
1094
|
+
diagnostics
|
|
2804
1095
|
});
|
|
2805
1096
|
});
|
|
2806
1097
|
// POST /api/taskforce/sync/workspace/apply-local - Apply pulled cloud deltas into local workspace store
|
|
@@ -2995,6 +1286,27 @@ export function registerSyncRoutes(deps) {
|
|
|
2995
1286
|
}
|
|
2996
1287
|
incomingChanges = decryptedChanges;
|
|
2997
1288
|
}
|
|
1289
|
+
const receivedAtBaseMs = Date.now();
|
|
1290
|
+
incomingChanges = incomingChanges.map((change, index) => {
|
|
1291
|
+
if (change.op !== 'upsert')
|
|
1292
|
+
return change;
|
|
1293
|
+
const task = change.task;
|
|
1294
|
+
const rawReferenceNumber = task.referenceNumber;
|
|
1295
|
+
const incomingReferenceNumber = rawReferenceNumber === undefined || rawReferenceNumber === null
|
|
1296
|
+
? null
|
|
1297
|
+
: (Number.isFinite(Number(rawReferenceNumber))
|
|
1298
|
+
? Math.max(1, Math.floor(Number(rawReferenceNumber)))
|
|
1299
|
+
: null);
|
|
1300
|
+
if (incomingReferenceNumber)
|
|
1301
|
+
return change;
|
|
1302
|
+
return {
|
|
1303
|
+
...change,
|
|
1304
|
+
task: {
|
|
1305
|
+
...task,
|
|
1306
|
+
updatedAt: new Date(receivedAtBaseMs + index).toISOString()
|
|
1307
|
+
}
|
|
1308
|
+
};
|
|
1309
|
+
});
|
|
2998
1310
|
try {
|
|
2999
1311
|
const { result, deleteCount, emittedEventIds } = await dispatchWorkspaceSyncChanges(targetWorkspaceId, incomingChanges, {
|
|
3000
1312
|
allowReferenceNumberReassignment
|