@taskforcehq/taskforce 0.3.301 → 0.3.302

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/dist/Taskforce.module.css +26 -2
  2. package/dist/TaskforceCore.filter-persistence.test.js +1 -1
  3. package/dist/TaskforceCore.js +13 -24
  4. package/dist/TaskforceCore.test.js +167 -152
  5. package/dist/components/features/TaskSettings.js +14 -82
  6. package/dist/components/features/TaskSettings.test.js +33 -70
  7. package/dist/components/features/settings/TaxonomyLibraryManager.js +8 -2
  8. package/dist/components/features/settings/TaxonomyLibraryManager.test.js +1 -1
  9. package/dist/components/task/TaskActionHeader.js +6 -3
  10. package/dist/components/task/TaskCard.js +6 -3
  11. package/dist/components/task/TaskForm.js +2 -2
  12. package/dist/components/task/TaskForm.test.js +41 -2
  13. package/dist/components/ui/ReferenceBadgeButton.js +1 -1
  14. package/dist/components/views/PlansPage.js +9 -8
  15. package/dist/components/views/StandaloneLayout.js +466 -204
  16. package/dist/components/views/panels/PlanningDrawer.js +18 -1
  17. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +43 -0
  18. package/dist/components/views/standalone/modals/AccountHubModal.js +22 -0
  19. package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.d.ts +9 -0
  20. package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.js +8 -0
  21. package/dist/components/views/standalone/modals/EditProfileModal.d.ts +26 -0
  22. package/dist/components/views/standalone/modals/EditProfileModal.js +10 -0
  23. package/dist/components/views/standalone/modals/HelpModal.d.ts +9 -0
  24. package/dist/components/views/standalone/modals/HelpModal.js +8 -0
  25. package/dist/components/views/standalone/modals/ScheduleWarningModal.d.ts +13 -0
  26. package/dist/components/views/standalone/modals/ScheduleWarningModal.js +6 -0
  27. package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +61 -0
  28. package/dist/components/views/standalone/modals/SyncStatusModal.js +38 -0
  29. package/dist/components/views/standalone/modals/TeamManagementModal.d.ts +64 -0
  30. package/dist/components/views/standalone/modals/TeamManagementModal.js +10 -0
  31. package/dist/components/views/standalone/modals/types.d.ts +2 -0
  32. package/dist/components/views/standalone/modals/types.js +1 -0
  33. package/dist/components/views/teamManagementAccess.d.ts +1 -1
  34. package/dist/components/views/teamManagementAccess.js +2 -2
  35. package/dist/components/views/teamManagementAccess.test.js +3 -3
  36. package/dist/config/deployment.js +2 -2
  37. package/dist/config/deployment.test.js +5 -3
  38. package/dist/core/AiProfileService.js +7 -2
  39. package/dist/core/AiProfiles.test.js +19 -0
  40. package/dist/core/SyncReconciliationService.d.ts +2 -0
  41. package/dist/core/SyncReconciliationService.js +3 -0
  42. package/dist/core/TaskAuditTimeline.test.js +40 -0
  43. package/dist/core/Taskforce.d.ts +49 -0
  44. package/dist/core/Taskforce.ids.test.js +133 -0
  45. package/dist/core/Taskforce.js +241 -78
  46. package/dist/core/UserProfileAvatarDrafts.test.d.ts +1 -0
  47. package/dist/core/UserProfileAvatarDrafts.test.js +102 -0
  48. package/dist/core/taskReferencePolicy.d.ts +28 -0
  49. package/dist/core/taskReferencePolicy.js +189 -0
  50. package/dist/core/types.d.ts +2 -0
  51. package/dist/hooks/tasks/useTaskFormActions.js +3 -0
  52. package/dist/hooks/useTaskData.js +3 -2
  53. package/dist/hooks/useTaskData.test.js +66 -0
  54. package/dist/hooks/useTaskforce.d.ts +5 -0
  55. package/dist/hooks/useTaskforce.js +63 -15
  56. package/dist/hooks/useTaskforce.sync-behavior.test.js +185 -6
  57. package/dist/mcp/aiProfileBootstrap.d.ts +29 -0
  58. package/dist/mcp/aiProfileBootstrap.js +80 -0
  59. package/dist/mcp/aiProfileBootstrap.test.d.ts +1 -0
  60. package/dist/mcp/aiProfileBootstrap.test.js +68 -0
  61. package/dist/mcp/clientIntegrations.d.ts +2 -8
  62. package/dist/mcp/clientIntegrations.js +21 -40
  63. package/dist/mcp/clientIntegrations.test.js +28 -2
  64. package/dist/mcp/clientProfileDefaults.d.ts +12 -0
  65. package/dist/mcp/clientProfileDefaults.js +37 -0
  66. package/dist/mcp/runtime.d.ts +10 -7
  67. package/dist/mcp/runtime.js +41 -19
  68. package/dist/mcp/runtime.test.js +49 -2
  69. package/dist/migrations/taskSchemaMigrations.js +53 -1
  70. package/dist/qaOpen.test.d.ts +1 -0
  71. package/dist/qaOpen.test.js +56 -0
  72. package/dist/server/index.cookieProxy.test.js +1 -0
  73. package/dist/server/index.d.ts +1 -0
  74. package/dist/server/index.js +15 -1
  75. package/dist/server/index.test.js +14 -1
  76. package/dist/server/routes/admin.js +25 -22
  77. package/dist/server/routes/auth.d.ts +5 -0
  78. package/dist/server/routes/auth.js +350 -16
  79. package/dist/server/routes/billing.js +11 -1
  80. package/dist/server/routes/billing.test.js +32 -2
  81. package/dist/server/routes/documents.js +6 -1
  82. package/dist/server/routes/shared.d.ts +1 -1
  83. package/dist/server/routes/shared.js +11 -3
  84. package/dist/server/routes/sync.integration.test.js +130 -0
  85. package/dist/server/routes/sync.js +226 -1924
  86. package/dist/server/routes/tasks.js +18 -4
  87. package/dist/server/routes.js +26 -19
  88. package/dist/server/routes.test.js +352 -6
  89. package/dist/shared/runtimeContract.d.ts +1 -1
  90. package/dist/sync/collaborationSyncPayload.d.ts +17 -0
  91. package/dist/sync/collaborationSyncPayload.js +131 -0
  92. package/dist/sync/collaborationSyncPayload.test.d.ts +1 -0
  93. package/dist/sync/collaborationSyncPayload.test.js +104 -0
  94. package/dist/sync/collaborationSyncState.d.ts +15 -0
  95. package/dist/sync/collaborationSyncState.js +60 -0
  96. package/dist/sync/contentSyncPayload.d.ts +45 -0
  97. package/dist/sync/contentSyncPayload.js +104 -0
  98. package/dist/sync/contentSyncPayload.test.d.ts +1 -0
  99. package/dist/sync/contentSyncPayload.test.js +104 -0
  100. package/dist/sync/contentSyncState.d.ts +88 -0
  101. package/dist/sync/contentSyncState.js +743 -0
  102. package/dist/sync/planningSyncPayload.d.ts +9 -0
  103. package/dist/sync/planningSyncPayload.js +82 -0
  104. package/dist/sync/planningSyncPayload.test.d.ts +1 -0
  105. package/dist/sync/planningSyncPayload.test.js +68 -0
  106. package/dist/sync/syncApplyHandlers.d.ts +83 -0
  107. package/dist/sync/syncApplyHandlers.js +445 -0
  108. package/dist/sync/taskSyncPayload.d.ts +33 -0
  109. package/dist/sync/taskSyncPayload.js +126 -0
  110. package/dist/sync/taskSyncPayload.test.d.ts +1 -0
  111. package/dist/sync/taskSyncPayload.test.js +54 -0
  112. package/dist/sync/workspacePullFeed.d.ts +28 -0
  113. package/dist/sync/workspacePullFeed.js +301 -0
  114. package/dist/sync/workspacePullFeed.test.d.ts +1 -0
  115. package/dist/sync/workspacePullFeed.test.js +166 -0
  116. package/dist/sync/workspaceSyncModel.d.ts +4 -1
  117. package/dist/sync/workspaceSyncModel.js +9 -2
  118. package/dist/sync/workspaceSyncModel.test.js +29 -0
  119. package/dist/test/setup.js +1 -1
  120. package/dist/types.d.ts +1 -0
  121. package/dist/ui/assets/{AgentsModule-Bdq3T1Ts.js → AgentsModule-JG5jc3he.js} +1 -1
  122. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-jKUVo-PJ.js → AnnotatedAttachmentWorkspace-Dvi02Y7q.js} +1 -1
  123. package/dist/ui/assets/{DocumentWorkspace-DuQa-uIC.js → DocumentWorkspace-iys3zfca.js} +2 -2
  124. package/dist/ui/assets/{InitiativesModule-C8Pi2VTk.js → InitiativesModule-DJvLJI3A.js} +1 -1
  125. package/dist/ui/assets/PlansPage-BMPlpFpw.js +1 -0
  126. package/dist/ui/assets/TaskSettings-Bqd4cCzW.js +8 -0
  127. package/dist/ui/assets/{TaskSettings-BwB9NtmJ.css → TaskSettings-CnIBL_Eb.css} +1 -1
  128. package/dist/ui/assets/{WorkflowsModule-b6TLuHwJ.js → WorkflowsModule-BguHeQaV.js} +1 -1
  129. package/dist/ui/assets/index-MXeWoebC.css +1 -0
  130. package/dist/ui/assets/index-YJ4k5QNa.js +6 -0
  131. package/dist/ui/assets/{vendor-icons-QZyhEwgT.js → vendor-icons-C8EED3Oh.js} +1 -1
  132. package/dist/ui/index.html +3 -3
  133. package/dist/utils/billingStatusCache.d.ts +21 -0
  134. package/dist/utils/billingStatusCache.js +70 -0
  135. package/dist/utils/taskActivity.js +17 -4
  136. package/dist/utils/taskActivity.test.js +27 -0
  137. package/dist/utils/taskNormalization.js +10 -2
  138. package/dist/utils/taskReferences.d.ts +11 -1
  139. package/dist/utils/taskReferences.js +33 -1
  140. package/package.json +2 -1
  141. package/scripts/qa-open-lib.mjs +175 -0
  142. package/scripts/qa-open-lib.mjs.d.ts +49 -0
  143. package/scripts/qa-open.mjs +352 -0
  144. package/dist/ui/assets/PlansPage-B6BDECSi.js +0 -1
  145. package/dist/ui/assets/TaskSettings-DUJgRTPU.js +0 -8
  146. package/dist/ui/assets/index-BiiF1cLQ.css +0 -1
  147. package/dist/ui/assets/index-xBWLPOEb.js +0 -6
@@ -0,0 +1,28 @@
1
+ import type { TaskforceCore, TaskforceSyncCapable } from '../core/Taskforce.js';
2
+ import type { WorkspaceSyncChange, WorkspaceSyncWorkspaceMemberSnapshot } from './workspaceSyncModel.js';
3
+ import { createCollaborationSyncState } from './collaborationSyncState.js';
4
+ import { createContentSyncState } from './contentSyncState.js';
5
+ type SyncCore = TaskforceCore & TaskforceSyncCapable;
6
+ export interface WorkspaceSyncCursorValue {
7
+ u: string;
8
+ id: string;
9
+ }
10
+ type EncodedWorkspaceSyncChange = WorkspaceSyncChange & {
11
+ watermark: string;
12
+ };
13
+ export interface WorkspacePullFeedResult {
14
+ workspaceMembers: WorkspaceSyncWorkspaceMemberSnapshot[];
15
+ changes: EncodedWorkspaceSyncChange[];
16
+ nextCursor: string | null;
17
+ hasMore: boolean;
18
+ }
19
+ export declare function buildWorkspacePullFeed(input: {
20
+ core: SyncCore;
21
+ workspaceId: string;
22
+ cursor: WorkspaceSyncCursorValue | null;
23
+ pageSize: number;
24
+ encodeWorkspaceSyncCursor: (cursor: WorkspaceSyncCursorValue | null) => string | null;
25
+ contentSyncState: ReturnType<typeof createContentSyncState>;
26
+ collaborationSyncState: ReturnType<typeof createCollaborationSyncState>;
27
+ }): Promise<WorkspacePullFeedResult>;
28
+ export {};
@@ -0,0 +1,301 @@
1
+ import { serializeTaskForSync } from './taskSyncPayload.js';
2
+ export async function buildWorkspacePullFeed(input) {
3
+ const { core, workspaceId, cursor, pageSize, encodeWorkspaceSyncCursor, contentSyncState, collaborationSyncState } = input;
4
+ const syncCursor = cursor ? { watermark: cursor.u, id: cursor.id } : null;
5
+ const dbFetchLimit = pageSize * 2 + 10;
6
+ const taskRows = core.listTasksForSync(workspaceId, syncCursor, dbFetchLimit);
7
+ const upsertEntries = taskRows
8
+ .map((entry) => {
9
+ const watermark = String(entry.task?.updatedAt || entry.task?.createdAt || '').trim();
10
+ const taskId = String(entry.task?.id || '').trim();
11
+ return {
12
+ kind: 'upsert',
13
+ watermark,
14
+ sortId: taskId,
15
+ task: serializeTaskForSync(entry.task),
16
+ archived: Boolean(entry.archived)
17
+ };
18
+ })
19
+ .filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.sortId.length > 0);
20
+ const rawDeleteEntries = core.listWorkspaceSyncDeletesSince(workspaceId, syncCursor, dbFetchLimit);
21
+ const deleteEntries = rawDeleteEntries
22
+ .map((entry) => ({
23
+ kind: 'delete',
24
+ watermark: String(entry.deletedAt || '').trim(),
25
+ sortId: String(entry.taskId || '').trim(),
26
+ taskId: String(entry.taskId || '').trim()
27
+ }))
28
+ .filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.sortId.length > 0);
29
+ const taxonomySnapshot = core.getTaxonomiesSyncSnapshot(workspaceId);
30
+ const taxonomyEntries = (taxonomySnapshot
31
+ && Array.isArray(taxonomySnapshot.taxonomies)
32
+ && Number.isFinite(Date.parse(String(taxonomySnapshot.updatedAt || '').trim())))
33
+ ? [{
34
+ kind: 'taxonomy',
35
+ watermark: String(taxonomySnapshot.updatedAt || '').trim(),
36
+ sortId: '__taxonomy_config__',
37
+ taxonomies: taxonomySnapshot.taxonomies,
38
+ ...(taxonomySnapshot.taxonomyState ? { taxonomyState: taxonomySnapshot.taxonomyState } : {})
39
+ }]
40
+ : [];
41
+ const initiativeEntries = core.listInitiatives(workspaceId)
42
+ .map((initiative) => ({
43
+ kind: 'initiative',
44
+ watermark: String(initiative.updatedAt || initiative.createdAt || '').trim(),
45
+ sortId: `initiative:${String(initiative.id || '').trim()}`,
46
+ initiative
47
+ }))
48
+ .filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.initiative.id.length > 0);
49
+ const workstreamEntries = core.listWorkstreams(workspaceId)
50
+ .map((workstream) => ({
51
+ kind: 'workstream',
52
+ watermark: String(workstream.updatedAt || workstream.createdAt || '').trim(),
53
+ sortId: `workstream:${String(workstream.id || '').trim()}`,
54
+ workstream
55
+ }))
56
+ .filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.workstream.id.length > 0);
57
+ const aiProfileEntries = core.listAiProfiles(workspaceId)
58
+ .map((profile) => ({
59
+ kind: 'ai-profile',
60
+ watermark: String(profile.updatedAt || profile.createdAt || '').trim(),
61
+ sortId: `ai-profile:${String(profile.id || '').trim()}`,
62
+ profile
63
+ }))
64
+ .filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.sortId.length > 0);
65
+ const documentEntries = (await contentSyncState.listWorkspaceSyncDocumentsSnapshot(core.getPaths().basePath, workspaceId))
66
+ .map((doc) => ({
67
+ kind: 'document',
68
+ watermark: String(doc.updatedAt || '').trim(),
69
+ sortId: `doc:${String(doc.path || '').trim()}`,
70
+ path: String(doc.path || '').trim(),
71
+ content: String(doc.content || ''),
72
+ assetId: doc.assetId,
73
+ documentId: doc.documentId,
74
+ referenceNumber: doc.referenceNumber,
75
+ version: doc.version,
76
+ taskId: doc.taskId,
77
+ logicalName: doc.logicalName,
78
+ caption: doc.caption,
79
+ originalFilename: doc.originalFilename,
80
+ linkRole: (doc.linkRole === 'reference' ? 'reference' : 'attachment')
81
+ }))
82
+ .filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.path.length > 0);
83
+ const documentDeleteEntries = contentSyncState.listWorkspaceSyncDocumentDeletesSnapshot(workspaceId)
84
+ .map((doc) => ({
85
+ kind: 'document-delete',
86
+ watermark: String(doc.deletedAt || '').trim(),
87
+ sortId: `doc-delete:${String(doc.path || '').trim()}:${String(doc.assetId || '').trim()}`,
88
+ path: String(doc.path || '').trim(),
89
+ assetId: typeof doc.assetId === 'string' && doc.assetId.trim().length > 0 ? doc.assetId.trim() : undefined
90
+ }))
91
+ .filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.path.length > 0);
92
+ const documentReviewSessions = collaborationSyncState.listWorkspaceSyncDocumentReviewSessionsSnapshot(workspaceId);
93
+ const documentReviewSessionEntries = documentReviewSessions
94
+ .filter((session) => !session.deletedAt)
95
+ .map((session) => ({
96
+ kind: 'document-review-session',
97
+ watermark: String(session.updatedAt || session.createdAt || '').trim(),
98
+ sortId: `document-review-session:${String(session.id || '').trim()}`,
99
+ session
100
+ }))
101
+ .filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.session.id.length > 0 && entry.session.assetId.length > 0);
102
+ const documentReviewSessionDeleteEntries = documentReviewSessions
103
+ .filter((session) => typeof session.deletedAt === 'string' && session.deletedAt.trim().length > 0)
104
+ .map((session) => ({
105
+ kind: 'document-review-session-delete',
106
+ watermark: String(session.deletedAt || session.updatedAt || '').trim(),
107
+ sortId: `document-review-session-delete:${String(session.id || '').trim()}`,
108
+ sessionId: String(session.id || '').trim()
109
+ }))
110
+ .filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.sessionId.length > 0);
111
+ const documentReviewComments = collaborationSyncState.listWorkspaceSyncDocumentReviewCommentsSnapshot(workspaceId);
112
+ const documentReviewCommentEntries = documentReviewComments
113
+ .filter((comment) => !comment.deletedAt)
114
+ .map((comment) => ({
115
+ kind: 'document-review-comment',
116
+ watermark: String(comment.updatedAt || comment.createdAt || '').trim(),
117
+ sortId: `document-review-comment:${String(comment.id || '').trim()}`,
118
+ comment
119
+ }))
120
+ .filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.comment.id.length > 0 && entry.comment.sessionId.length > 0);
121
+ const documentReviewCommentDeleteEntries = documentReviewComments
122
+ .filter((comment) => typeof comment.deletedAt === 'string' && comment.deletedAt.trim().length > 0)
123
+ .map((comment) => ({
124
+ kind: 'document-review-comment-delete',
125
+ watermark: String(comment.deletedAt || comment.updatedAt || '').trim(),
126
+ sortId: `document-review-comment-delete:${String(comment.id || '').trim()}`,
127
+ commentId: String(comment.id || '').trim()
128
+ }))
129
+ .filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.commentId.length > 0);
130
+ const annotatedAttachmentSessions = collaborationSyncState.listWorkspaceSyncAnnotatedAttachmentSessionsSnapshot(workspaceId);
131
+ const annotatedAttachmentSessionEntries = annotatedAttachmentSessions
132
+ .filter((session) => !session.deletedAt)
133
+ .map((session) => ({
134
+ kind: 'annotated-attachment-session',
135
+ watermark: String(session.updatedAt || session.createdAt || '').trim(),
136
+ sortId: `annotated-attachment-session:${String(session.id || '').trim()}`,
137
+ session
138
+ }))
139
+ .filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.session.id.length > 0 && entry.session.baseImageAssetId.length > 0);
140
+ const annotatedAttachmentSessionDeleteEntries = annotatedAttachmentSessions
141
+ .filter((session) => typeof session.deletedAt === 'string' && session.deletedAt.trim().length > 0)
142
+ .map((session) => ({
143
+ kind: 'annotated-attachment-session-delete',
144
+ watermark: String(session.deletedAt || session.updatedAt || '').trim(),
145
+ sortId: `annotated-attachment-session-delete:${String(session.id || '').trim()}`,
146
+ sessionId: String(session.id || '').trim()
147
+ }))
148
+ .filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.sessionId.length > 0);
149
+ const assetEntries = (await contentSyncState.listWorkspaceSyncBinaryAssetsSnapshot(core.getPaths().basePath, workspaceId))
150
+ .map((asset) => ({
151
+ kind: 'asset',
152
+ watermark: String(asset.updatedAt || '').trim(),
153
+ sortId: `asset:${String(asset.path || '').trim()}`,
154
+ path: String(asset.path || '').trim(),
155
+ contentBase64: String(asset.contentBase64 || ''),
156
+ assetId: asset.assetId,
157
+ kindValue: asset.kind,
158
+ mimeType: asset.mimeType,
159
+ referenceNumber: asset.referenceNumber,
160
+ taskId: asset.taskId,
161
+ logicalName: asset.logicalName,
162
+ caption: asset.caption,
163
+ originalFilename: asset.originalFilename,
164
+ linkRole: asset.linkRole
165
+ }))
166
+ .filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.path.length > 0 && entry.contentBase64.length > 0);
167
+ const assetDeleteEntries = contentSyncState.listWorkspaceSyncBinaryAssetDeletesSnapshot(workspaceId)
168
+ .map((asset) => ({
169
+ kind: 'asset-delete',
170
+ watermark: String(asset.deletedAt || '').trim(),
171
+ sortId: `asset-delete:${String(asset.path || '').trim()}:${String(asset.assetId || '').trim()}`,
172
+ path: String(asset.path || '').trim(),
173
+ assetId: typeof asset.assetId === 'string' && asset.assetId.trim().length > 0 ? asset.assetId.trim() : undefined
174
+ }))
175
+ .filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.path.length > 0);
176
+ const all = [
177
+ ...upsertEntries,
178
+ ...deleteEntries,
179
+ ...taxonomyEntries,
180
+ ...initiativeEntries,
181
+ ...workstreamEntries,
182
+ ...aiProfileEntries,
183
+ ...documentEntries,
184
+ ...documentDeleteEntries,
185
+ ...documentReviewSessionEntries,
186
+ ...documentReviewSessionDeleteEntries,
187
+ ...documentReviewCommentEntries,
188
+ ...documentReviewCommentDeleteEntries,
189
+ ...annotatedAttachmentSessionEntries,
190
+ ...annotatedAttachmentSessionDeleteEntries,
191
+ ...assetEntries,
192
+ ...assetDeleteEntries
193
+ ];
194
+ const afterCursor = all.filter((entry) => {
195
+ if (!cursor)
196
+ return true;
197
+ if (entry.watermark > cursor.u)
198
+ return true;
199
+ if (entry.watermark < cursor.u)
200
+ return false;
201
+ return entry.sortId > cursor.id;
202
+ });
203
+ afterCursor.sort((a, b) => {
204
+ if (a.watermark !== b.watermark)
205
+ return a.watermark.localeCompare(b.watermark);
206
+ return String(a.sortId || '').localeCompare(String(b.sortId || ''));
207
+ });
208
+ const page = afterCursor.slice(0, pageSize);
209
+ const hasMore = afterCursor.length > pageSize;
210
+ const last = page.length > 0 ? page[page.length - 1] : null;
211
+ const nextCursor = encodeWorkspaceSyncCursor(last
212
+ ? { u: last.watermark, id: String(last.sortId || '') }
213
+ : cursor);
214
+ const workspaceMembers = core.listWorkspaceUsers(workspaceId).map((member) => ({
215
+ userId: member.userId,
216
+ workspaceId,
217
+ email: member.email,
218
+ displayName: member.displayName || null,
219
+ role: member.role,
220
+ permissionMode: member.permissionMode,
221
+ betaAccess: member.betaAccess,
222
+ status: member.status,
223
+ disabled: member.disabled,
224
+ createdAt: member.createdAt || null,
225
+ updatedAt: member.updatedAt || null,
226
+ emailVerifiedAt: member.emailVerifiedAt || null
227
+ }));
228
+ const changes = page.map((entry) => (entry.kind === 'delete'
229
+ ? { op: 'delete', taskId: entry.taskId, deletedAt: entry.watermark, watermark: entry.watermark }
230
+ : entry.kind === 'taxonomy'
231
+ ? {
232
+ op: 'taxonomy-upsert',
233
+ taxonomies: entry.taxonomies,
234
+ ...(entry.taxonomyState ? { taxonomyState: entry.taxonomyState } : {}),
235
+ updatedAt: entry.watermark,
236
+ watermark: entry.watermark
237
+ }
238
+ : entry.kind === 'initiative'
239
+ ? { op: 'initiative-upsert', initiative: entry.initiative, updatedAt: entry.watermark, watermark: entry.watermark }
240
+ : entry.kind === 'workstream'
241
+ ? { op: 'workstream-upsert', workstream: entry.workstream, updatedAt: entry.watermark, watermark: entry.watermark }
242
+ : entry.kind === 'ai-profile'
243
+ ? { op: 'ai-profile-upsert', profile: entry.profile, updatedAt: entry.watermark, watermark: entry.watermark }
244
+ : entry.kind === 'document-review-session'
245
+ ? { op: 'document-review-session-upsert', session: entry.session, updatedAt: entry.watermark, watermark: entry.watermark }
246
+ : entry.kind === 'document-review-session-delete'
247
+ ? { op: 'document-review-session-delete', sessionId: entry.sessionId, deletedAt: entry.watermark, watermark: entry.watermark }
248
+ : entry.kind === 'document-review-comment'
249
+ ? { op: 'document-review-comment-upsert', comment: entry.comment, updatedAt: entry.watermark, watermark: entry.watermark }
250
+ : entry.kind === 'document-review-comment-delete'
251
+ ? { op: 'document-review-comment-delete', commentId: entry.commentId, deletedAt: entry.watermark, watermark: entry.watermark }
252
+ : entry.kind === 'annotated-attachment-session'
253
+ ? { op: 'annotated-attachment-session-upsert', session: entry.session, updatedAt: entry.watermark, watermark: entry.watermark }
254
+ : entry.kind === 'annotated-attachment-session-delete'
255
+ ? { op: 'annotated-attachment-session-delete', sessionId: entry.sessionId, deletedAt: entry.watermark, watermark: entry.watermark }
256
+ : entry.kind === 'document'
257
+ ? {
258
+ op: 'document-upsert',
259
+ path: entry.path,
260
+ updatedAt: entry.watermark,
261
+ content: entry.content,
262
+ assetId: entry.assetId,
263
+ documentId: entry.documentId,
264
+ referenceNumber: entry.referenceNumber,
265
+ version: entry.version,
266
+ taskId: entry.taskId,
267
+ logicalName: entry.logicalName,
268
+ caption: entry.caption,
269
+ originalFilename: entry.originalFilename,
270
+ linkRole: entry.linkRole,
271
+ watermark: entry.watermark
272
+ }
273
+ : entry.kind === 'document-delete'
274
+ ? { op: 'document-delete', path: entry.path, assetId: entry.assetId, deletedAt: entry.watermark, watermark: entry.watermark }
275
+ : entry.kind === 'asset'
276
+ ? {
277
+ op: 'asset-upsert',
278
+ path: entry.path,
279
+ updatedAt: entry.watermark,
280
+ contentBase64: entry.contentBase64,
281
+ assetId: entry.assetId,
282
+ kind: entry.kindValue,
283
+ mimeType: entry.mimeType,
284
+ referenceNumber: entry.referenceNumber,
285
+ taskId: entry.taskId,
286
+ logicalName: entry.logicalName,
287
+ caption: entry.caption,
288
+ originalFilename: entry.originalFilename,
289
+ linkRole: entry.linkRole,
290
+ watermark: entry.watermark
291
+ }
292
+ : entry.kind === 'asset-delete'
293
+ ? { op: 'asset-delete', path: entry.path, assetId: entry.assetId, deletedAt: entry.watermark, watermark: entry.watermark }
294
+ : { op: 'upsert', task: entry.task, archived: Boolean(entry.archived), watermark: entry.watermark }));
295
+ return {
296
+ workspaceMembers,
297
+ changes,
298
+ nextCursor,
299
+ hasMore
300
+ };
301
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,166 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { buildWorkspacePullFeed } from './workspacePullFeed';
3
+ function createCoreMock() {
4
+ return {
5
+ getPaths: () => ({ basePath: '/tmp/taskforce-test' }),
6
+ listTasksForSync: () => [],
7
+ listWorkspaceSyncDeletesSince: () => [],
8
+ getTaxonomiesSyncSnapshot: () => null,
9
+ listInitiatives: () => [],
10
+ listWorkstreams: () => [],
11
+ listAiProfiles: () => [],
12
+ listWorkspaceUsers: () => [],
13
+ };
14
+ }
15
+ function createContentSyncStateMock() {
16
+ return {
17
+ listWorkspaceSyncDocumentsSnapshot: async () => [],
18
+ listWorkspaceSyncDocumentDeletesSnapshot: () => [],
19
+ listWorkspaceSyncBinaryAssetsSnapshot: async () => [],
20
+ listWorkspaceSyncBinaryAssetDeletesSnapshot: () => [],
21
+ };
22
+ }
23
+ function createCollaborationSyncStateMock() {
24
+ return {
25
+ listWorkspaceSyncDocumentReviewSessionsSnapshot: () => [],
26
+ listWorkspaceSyncDocumentReviewCommentsSnapshot: () => [],
27
+ listWorkspaceSyncAnnotatedAttachmentSessionsSnapshot: () => [],
28
+ };
29
+ }
30
+ function encodeCursor(cursor) {
31
+ return cursor ? `${cursor.u}|${cursor.id}` : null;
32
+ }
33
+ describe('workspacePullFeed', () => {
34
+ it('sorts entries by watermark and sort id, then paginates deterministically', async () => {
35
+ const core = createCoreMock();
36
+ core.listTasksForSync = () => [
37
+ {
38
+ archived: false,
39
+ task: {
40
+ id: 'b-task',
41
+ title: 'Task B',
42
+ status: 'task',
43
+ category: 'general',
44
+ type: 'task',
45
+ createdAt: '2026-04-10T10:00:00.000Z',
46
+ }
47
+ },
48
+ {
49
+ archived: false,
50
+ task: {
51
+ id: 'a-task',
52
+ title: 'Task A',
53
+ status: 'task',
54
+ category: 'general',
55
+ type: 'task',
56
+ createdAt: '2026-04-10T10:00:00.000Z',
57
+ }
58
+ },
59
+ {
60
+ archived: false,
61
+ task: {
62
+ id: 'c-task',
63
+ title: 'Task C',
64
+ status: 'task',
65
+ category: 'general',
66
+ type: 'task',
67
+ createdAt: '2026-04-11T10:00:00.000Z',
68
+ }
69
+ },
70
+ ];
71
+ const result = await buildWorkspacePullFeed({
72
+ core,
73
+ workspaceId: 'workspace-1',
74
+ cursor: null,
75
+ pageSize: 2,
76
+ encodeWorkspaceSyncCursor: encodeCursor,
77
+ contentSyncState: createContentSyncStateMock(),
78
+ collaborationSyncState: createCollaborationSyncStateMock(),
79
+ });
80
+ expect(result.changes).toHaveLength(2);
81
+ expect(result.changes[0]).toEqual(expect.objectContaining({
82
+ op: 'upsert',
83
+ watermark: '2026-04-10T10:00:00.000Z',
84
+ task: expect.objectContaining({ id: 'a-task' }),
85
+ }));
86
+ expect(result.changes[1]).toEqual(expect.objectContaining({
87
+ op: 'upsert',
88
+ watermark: '2026-04-10T10:00:00.000Z',
89
+ task: expect.objectContaining({ id: 'b-task' }),
90
+ }));
91
+ expect(result.nextCursor).toBe('2026-04-10T10:00:00.000Z|b-task');
92
+ expect(result.hasMore).toBe(true);
93
+ });
94
+ it('resumes strictly after the cursor and preserves workspace member snapshots', async () => {
95
+ const core = createCoreMock();
96
+ core.listTasksForSync = () => [
97
+ {
98
+ archived: false,
99
+ task: {
100
+ id: 'z-task',
101
+ title: 'Carry task',
102
+ status: 'review',
103
+ category: 'general',
104
+ type: 'task',
105
+ createdAt: '2026-04-10T10:00:00.000Z',
106
+ }
107
+ }
108
+ ];
109
+ core.listWorkspaceSyncDeletesSince = () => [
110
+ {
111
+ taskId: 'delete-task',
112
+ deletedAt: '2026-04-10T10:00:00.000Z',
113
+ },
114
+ {
115
+ taskId: 'later-delete',
116
+ deletedAt: '2026-04-11T09:00:00.000Z',
117
+ }
118
+ ];
119
+ core.listWorkspaceUsers = () => [
120
+ {
121
+ userId: 'user-1',
122
+ email: 'person@example.com',
123
+ displayName: 'Person',
124
+ role: 'owner',
125
+ permissionMode: 'read-write',
126
+ betaAccess: true,
127
+ status: 'active',
128
+ disabled: false,
129
+ createdAt: '2026-04-01T00:00:00.000Z',
130
+ updatedAt: '2026-04-02T00:00:00.000Z',
131
+ emailVerifiedAt: '2026-04-03T00:00:00.000Z',
132
+ }
133
+ ];
134
+ const result = await buildWorkspacePullFeed({
135
+ core,
136
+ workspaceId: 'workspace-2',
137
+ cursor: { u: '2026-04-10T10:00:00.000Z', id: 'delete-task' },
138
+ pageSize: 10,
139
+ encodeWorkspaceSyncCursor: encodeCursor,
140
+ contentSyncState: createContentSyncStateMock(),
141
+ collaborationSyncState: createCollaborationSyncStateMock(),
142
+ });
143
+ expect(result.changes).toEqual([
144
+ expect.objectContaining({
145
+ op: 'upsert',
146
+ watermark: '2026-04-10T10:00:00.000Z',
147
+ task: expect.objectContaining({ id: 'z-task' }),
148
+ }),
149
+ expect.objectContaining({
150
+ op: 'delete',
151
+ taskId: 'later-delete',
152
+ watermark: '2026-04-11T09:00:00.000Z',
153
+ }),
154
+ ]);
155
+ expect(result.hasMore).toBe(false);
156
+ expect(result.nextCursor).toBe('2026-04-11T09:00:00.000Z|later-delete');
157
+ expect(result.workspaceMembers).toEqual([
158
+ expect.objectContaining({
159
+ userId: 'user-1',
160
+ workspaceId: 'workspace-2',
161
+ email: 'person@example.com',
162
+ displayName: 'Person',
163
+ })
164
+ ]);
165
+ });
166
+ });
@@ -3,6 +3,8 @@ import type { DocumentReviewCommentRecord, DocumentReviewSessionStatus } from '.
3
3
  import type { AnnotatedAttachmentAnnotation } from '../annotatedAttachments/types.js';
4
4
  import type { WorkspaceTaxonomyState } from '../shared/taxonomyState.js';
5
5
  import { type AiProfileSurfaceType } from '../shared/aiProfileSurfaceType.js';
6
+ import { type TaskSyncPayload } from './taskSyncPayload.js';
7
+ export type WorkspaceSyncTaskSnapshot = TaskSyncPayload;
6
8
  export interface WorkspaceSyncAiProfileSnapshot {
7
9
  id: string;
8
10
  workspaceId: string;
@@ -98,7 +100,7 @@ export interface WorkspaceSyncWorkstreamSnapshot {
98
100
  export type WorkspaceSyncChange = {
99
101
  op: 'upsert';
100
102
  archived: boolean;
101
- task: TaskItem;
103
+ task: WorkspaceSyncTaskSnapshot;
102
104
  } | {
103
105
  op: 'delete';
104
106
  taskId: string;
@@ -204,6 +206,7 @@ export interface WorkspaceSyncPayload {
204
206
  /** Watermarks for every task upsert included in this payload (id → updatedAt|createdAt). */
205
207
  pushedWatermarks: Map<string, string>;
206
208
  }
209
+ export declare function toWorkspaceSyncTaskSnapshot(task: TaskItem): WorkspaceSyncTaskSnapshot;
207
210
  export interface WorkspaceSyncDocumentSnapshot {
208
211
  path: string;
209
212
  updatedAt: string;
@@ -1,4 +1,8 @@
1
1
  import { parseAiProfileSurfaceType } from '../shared/aiProfileSurfaceType.js';
2
+ import { serializeTaskForSync } from './taskSyncPayload.js';
3
+ export function toWorkspaceSyncTaskSnapshot(task) {
4
+ return serializeTaskForSync(task);
5
+ }
2
6
  function sortTasksForSync(tasks) {
3
7
  return [...tasks].sort((left, right) => {
4
8
  const leftId = String(left?.id || '').trim();
@@ -13,6 +17,9 @@ export function buildWorkspaceSyncPayloadModel(input) {
13
17
  .filter((taskId) => taskId.length > 0));
14
18
  const lastWatermarks = input.lastPushedWatermarks;
15
19
  const isDirty = (task) => {
20
+ if ((task?.referenceNumber === undefined || task?.referenceNumber === null) && Number.isFinite(Number(task?.localReferenceNumber))) {
21
+ return true;
22
+ }
16
23
  if (!lastWatermarks || lastWatermarks.size === 0)
17
24
  return true;
18
25
  const taskId = String(task?.id || '').trim();
@@ -31,7 +38,7 @@ export function buildWorkspaceSyncPayloadModel(input) {
31
38
  const wm = String(task?.updatedAt || task?.createdAt || '').trim();
32
39
  if (taskId)
33
40
  pushedWatermarks.set(taskId, wm);
34
- return { op: 'upsert', archived: false, task };
41
+ return { op: 'upsert', archived: false, task: toWorkspaceSyncTaskSnapshot(task) };
35
42
  });
36
43
  const archivedChanges = archivedTasksOrdered
37
44
  .filter((task) => !pendingDeleteIds.has(String(task?.id || '').trim()) && isDirty(task))
@@ -40,7 +47,7 @@ export function buildWorkspaceSyncPayloadModel(input) {
40
47
  const wm = String(task?.updatedAt || task?.createdAt || '').trim();
41
48
  if (taskId)
42
49
  pushedWatermarks.set(taskId, wm);
43
- return { op: 'upsert', archived: true, task };
50
+ return { op: 'upsert', archived: true, task: toWorkspaceSyncTaskSnapshot(task) };
44
51
  });
45
52
  const currentTaskIds = new Set();
46
53
  for (const task of input.tasks || []) {
@@ -711,5 +711,34 @@ describe('workspaceSyncModel', () => {
711
711
  ]));
712
712
  expect(payload.pushedWatermarks.get('task-1')).toBe('2026-03-16T10:05:00.000Z');
713
713
  });
714
+ it('keeps provisional local-reference tasks dirty until a canonical reference is assigned', () => {
715
+ const payload = buildWorkspaceSyncPayloadModel({
716
+ tasks: [{
717
+ id: 'task-provisional-1',
718
+ title: 'Offline draft',
719
+ status: 'task',
720
+ createdAt: '2026-03-16T10:00:00.000Z',
721
+ updatedAt: '2026-03-16T10:05:00.000Z',
722
+ referenceNumber: null,
723
+ localReferenceNumber: 3
724
+ }],
725
+ archivedTasks: [],
726
+ lastPushedTaskIds: new Set(['task-provisional-1']),
727
+ pendingDeletedTaskIds: new Set(),
728
+ lastPushedWatermarks: new Map([
729
+ ['task-provisional-1', '2026-03-16T10:05:00.000Z']
730
+ ])
731
+ });
732
+ expect(payload.changes).toEqual(expect.arrayContaining([
733
+ expect.objectContaining({
734
+ op: 'upsert',
735
+ archived: false,
736
+ task: expect.objectContaining({
737
+ id: 'task-provisional-1',
738
+ referenceNumber: null
739
+ })
740
+ })
741
+ ]));
742
+ });
714
743
  });
715
744
  });
@@ -6,7 +6,7 @@ import os from 'os';
6
6
  import path from 'path';
7
7
  // Ensure all tests write global settings to a writable temp home.
8
8
  const testHome = path.join(os.tmpdir(), 'taskforce-vitest-home');
9
- fs.mkdirSync(path.join(testHome, '.tasknexus'), { recursive: true });
9
+ fs.mkdirSync(path.join(testHome, '.taskforce'), { recursive: true });
10
10
  process.env.HOME = testHome;
11
11
  process.env.USERPROFILE = testHome;
12
12
  // Cleanup after each test case (e.g. clearing jsdom)
package/dist/types.d.ts CHANGED
@@ -163,6 +163,7 @@ export interface ChecklistItem {
163
163
  export interface TaskItem {
164
164
  id: string;
165
165
  referenceNumber?: number | null;
166
+ localReferenceNumber?: number | null;
166
167
  referenceLabel?: string;
167
168
  title: string;
168
169
  description: string | null;