@critical-path/core 0.10.0 → 0.12.0
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/CHANGELOG.md +23 -0
- package/README.md +35 -0
- package/dist/deliverables.test.d.ts +2 -0
- package/dist/deliverables.test.d.ts.map +1 -0
- package/dist/deliverables.test.js +232 -0
- package/dist/deliverables.test.js.map +1 -0
- package/dist/domain/entities.d.ts +25 -1
- package/dist/domain/entities.d.ts.map +1 -1
- package/dist/domain/entities.js +153 -0
- package/dist/domain/entities.js.map +1 -1
- package/dist/domain/events.d.ts +31 -3
- package/dist/domain/events.d.ts.map +1 -1
- package/dist/domain/events.js.map +1 -1
- package/dist/engine/index.d.ts +8 -1
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +170 -3
- package/dist/engine/index.js.map +1 -1
- package/dist/index.test.js +25 -0
- package/dist/index.test.js.map +1 -1
- package/dist/store/firebase.d.ts +6 -1
- package/dist/store/firebase.d.ts.map +1 -1
- package/dist/store/firebase.js +41 -0
- package/dist/store/firebase.js.map +1 -1
- package/dist/store/index.d.ts +15 -2
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +38 -0
- package/dist/store/index.js.map +1 -1
- package/dist/store/sqlite.d.ts +7 -1
- package/dist/store/sqlite.d.ts.map +1 -1
- package/dist/store/sqlite.js +107 -9
- package/dist/store/sqlite.js.map +1 -1
- package/dist/store/sqlite.test.js +6 -1
- package/dist/store/sqlite.test.js.map +1 -1
- package/dist/types/index.d.ts +34 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/mentions.d.ts +24 -0
- package/dist/utils/mentions.d.ts.map +1 -0
- package/dist/utils/mentions.js +73 -0
- package/dist/utils/mentions.js.map +1 -0
- package/dist/utils/mentions.test.d.ts +2 -0
- package/dist/utils/mentions.test.d.ts.map +1 -0
- package/dist/utils/mentions.test.js +71 -0
- package/dist/utils/mentions.test.js.map +1 -0
- package/dist/utils/workflow.d.ts +1 -0
- package/dist/utils/workflow.d.ts.map +1 -1
- package/dist/utils/workflow.js +37 -0
- package/dist/utils/workflow.js.map +1 -1
- package/package.json +1 -1
- package/src/deliverables.test.ts +284 -0
- package/src/domain/entities.ts +185 -2
- package/src/domain/events.ts +28 -3
- package/src/engine/index.ts +195 -4
- package/src/index.test.ts +30 -0
- package/src/store/firebase.ts +47 -1
- package/src/store/index.ts +54 -1
- package/src/store/sqlite.test.ts +6 -1
- package/src/store/sqlite.ts +148 -6
- package/src/types/index.ts +52 -1
- package/src/utils/index.ts +1 -0
- package/src/utils/mentions.test.ts +82 -0
- package/src/utils/mentions.ts +89 -0
- package/src/utils/workflow.ts +38 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/store/index.ts
CHANGED
|
@@ -13,7 +13,9 @@ import type {
|
|
|
13
13
|
Activity,
|
|
14
14
|
Webhook,
|
|
15
15
|
TaskDependency,
|
|
16
|
-
Workflow
|
|
16
|
+
Workflow,
|
|
17
|
+
Deliverable,
|
|
18
|
+
CreateDeliverableInput
|
|
17
19
|
} from '../types/index.js';
|
|
18
20
|
import { generateProjectKey } from '../utils/key.js';
|
|
19
21
|
|
|
@@ -102,12 +104,21 @@ export interface WebhookRepository {
|
|
|
102
104
|
addWebhook(webhook: Omit<Webhook, 'id' | 'createdAt'>): Promise<Webhook>;
|
|
103
105
|
}
|
|
104
106
|
|
|
107
|
+
export interface DeliverableRepository {
|
|
108
|
+
getDeliverables(projectId: string): Promise<Deliverable[]>;
|
|
109
|
+
getDeliverable(id: string): Promise<Deliverable | null>;
|
|
110
|
+
createDeliverable(deliverable: CreateDeliverableInput): Promise<Deliverable>;
|
|
111
|
+
updateDeliverable(id: string, updates: Partial<Deliverable>): Promise<Deliverable | null>;
|
|
112
|
+
deleteDeliverable(id: string): Promise<boolean>;
|
|
113
|
+
}
|
|
114
|
+
|
|
105
115
|
export interface StorageAdapter
|
|
106
116
|
extends ProjectRepository,
|
|
107
117
|
WorkflowRepository,
|
|
108
118
|
TaskRepository,
|
|
109
119
|
TeamRepository,
|
|
110
120
|
ContainerRepository,
|
|
121
|
+
DeliverableRepository,
|
|
111
122
|
IterationRepository,
|
|
112
123
|
CommentRepository,
|
|
113
124
|
AttachmentRepository,
|
|
@@ -122,6 +133,7 @@ export class InMemoryStore implements StorageAdapter {
|
|
|
122
133
|
private tasks = new Map<string, Task>();
|
|
123
134
|
private teams = new Map<string, Team>();
|
|
124
135
|
private containers = new Map<string, TaskContainer>();
|
|
136
|
+
private deliverables = new Map<string, Deliverable>();
|
|
125
137
|
private iterations = new Map<string, Iteration>();
|
|
126
138
|
private comments = new Map<string, Comment>();
|
|
127
139
|
private attachments = new Map<string, Attachment>();
|
|
@@ -306,6 +318,47 @@ export class InMemoryStore implements StorageAdapter {
|
|
|
306
318
|
return this.containers.delete(id);
|
|
307
319
|
}
|
|
308
320
|
|
|
321
|
+
// Deliverables
|
|
322
|
+
async getDeliverables(projectId: string): Promise<Deliverable[]> {
|
|
323
|
+
return Array.from(this.deliverables.values()).filter((d) => d.projectId === projectId);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async getDeliverable(id: string): Promise<Deliverable | null> {
|
|
327
|
+
return this.deliverables.get(id) || null;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async createDeliverable(deliverable: CreateDeliverableInput): Promise<Deliverable> {
|
|
331
|
+
const id = (deliverable as any).id || `deliv_${Math.random().toString(36).substring(2, 9)}`;
|
|
332
|
+
const now = new Date().toISOString();
|
|
333
|
+
const newDeliverable: Deliverable = {
|
|
334
|
+
...deliverable,
|
|
335
|
+
id,
|
|
336
|
+
status: deliverable.status || 'planned',
|
|
337
|
+
outputUrls: deliverable.outputUrls ? [...deliverable.outputUrls] : [],
|
|
338
|
+
customFields: deliverable.customFields ? { ...deliverable.customFields } : {},
|
|
339
|
+
createdAt: now,
|
|
340
|
+
updatedAt: now
|
|
341
|
+
};
|
|
342
|
+
this.deliverables.set(id, newDeliverable);
|
|
343
|
+
return newDeliverable;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async updateDeliverable(id: string, updates: Partial<Deliverable>): Promise<Deliverable | null> {
|
|
347
|
+
const existing = this.deliverables.get(id);
|
|
348
|
+
if (!existing) return null;
|
|
349
|
+
const updated: Deliverable = {
|
|
350
|
+
...existing,
|
|
351
|
+
...updates,
|
|
352
|
+
updatedAt: new Date().toISOString()
|
|
353
|
+
};
|
|
354
|
+
this.deliverables.set(id, updated);
|
|
355
|
+
return updated;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async deleteDeliverable(id: string): Promise<boolean> {
|
|
359
|
+
return this.deliverables.delete(id);
|
|
360
|
+
}
|
|
361
|
+
|
|
309
362
|
// Iterations
|
|
310
363
|
async getIterations(projectId: string): Promise<Iteration[]> {
|
|
311
364
|
return Array.from(this.iterations.values()).filter((s) => s.projectId === projectId);
|
package/src/store/sqlite.test.ts
CHANGED
|
@@ -77,11 +77,16 @@ describe('SQLiteStore', () => {
|
|
|
77
77
|
const comments = await store.getComments('task_1');
|
|
78
78
|
expect(comments).toHaveLength(2);
|
|
79
79
|
|
|
80
|
-
const updatedComment = await store.updateComment(comment.id, {
|
|
80
|
+
const updatedComment = await store.updateComment(comment.id, {
|
|
81
|
+
content: 'Updated content',
|
|
82
|
+
mentions: ['planner', 'coordinator']
|
|
83
|
+
});
|
|
81
84
|
expect(updatedComment?.content).toBe('Updated content');
|
|
85
|
+
expect(updatedComment?.mentions).toEqual(['planner', 'coordinator']);
|
|
82
86
|
|
|
83
87
|
const fetchedComment = await store.getComment(comment.id);
|
|
84
88
|
expect(fetchedComment?.content).toBe('Updated content');
|
|
89
|
+
expect(fetchedComment?.mentions).toEqual(['planner', 'coordinator']);
|
|
85
90
|
|
|
86
91
|
const timeEntry = await store.logTime({
|
|
87
92
|
taskId: 'task_1',
|
package/src/store/sqlite.ts
CHANGED
|
@@ -12,7 +12,9 @@ import type {
|
|
|
12
12
|
Activity,
|
|
13
13
|
Webhook,
|
|
14
14
|
TaskDependency,
|
|
15
|
-
Workflow
|
|
15
|
+
Workflow,
|
|
16
|
+
Deliverable,
|
|
17
|
+
CreateDeliverableInput
|
|
16
18
|
} from '../types/index.js';
|
|
17
19
|
import { generateProjectKey } from '../utils/key.js';
|
|
18
20
|
|
|
@@ -116,6 +118,7 @@ export class SQLiteStore implements StorageAdapter {
|
|
|
116
118
|
iterationId TEXT,
|
|
117
119
|
teamId TEXT,
|
|
118
120
|
containerId TEXT,
|
|
121
|
+
deliverableId TEXT,
|
|
119
122
|
plannedStartDate TEXT,
|
|
120
123
|
actualStartDate TEXT,
|
|
121
124
|
actualEndDate TEXT,
|
|
@@ -135,6 +138,24 @@ export class SQLiteStore implements StorageAdapter {
|
|
|
135
138
|
updatedAt TEXT NOT NULL
|
|
136
139
|
);
|
|
137
140
|
|
|
141
|
+
CREATE TABLE IF NOT EXISTS deliverables (
|
|
142
|
+
id TEXT PRIMARY KEY,
|
|
143
|
+
projectId TEXT NOT NULL,
|
|
144
|
+
title TEXT NOT NULL,
|
|
145
|
+
description TEXT,
|
|
146
|
+
status TEXT NOT NULL,
|
|
147
|
+
format TEXT,
|
|
148
|
+
specs TEXT,
|
|
149
|
+
leadId TEXT,
|
|
150
|
+
reviewerId TEXT,
|
|
151
|
+
dueDate TEXT,
|
|
152
|
+
deliveredAt TEXT,
|
|
153
|
+
outputUrls TEXT,
|
|
154
|
+
customFields TEXT,
|
|
155
|
+
createdAt TEXT NOT NULL,
|
|
156
|
+
updatedAt TEXT NOT NULL
|
|
157
|
+
);
|
|
158
|
+
|
|
138
159
|
CREATE TABLE IF NOT EXISTS iterations (
|
|
139
160
|
id TEXT PRIMARY KEY,
|
|
140
161
|
projectId TEXT NOT NULL,
|
|
@@ -154,6 +175,7 @@ export class SQLiteStore implements StorageAdapter {
|
|
|
154
175
|
authorType TEXT,
|
|
155
176
|
parentId TEXT,
|
|
156
177
|
content TEXT NOT NULL,
|
|
178
|
+
mentions TEXT,
|
|
157
179
|
reactions TEXT,
|
|
158
180
|
metadata TEXT,
|
|
159
181
|
createdAt TEXT NOT NULL,
|
|
@@ -219,6 +241,18 @@ export class SQLiteStore implements StorageAdapter {
|
|
|
219
241
|
} catch {
|
|
220
242
|
// Column may already exist
|
|
221
243
|
}
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
this.db.exec('ALTER TABLE comments ADD COLUMN mentions TEXT');
|
|
247
|
+
} catch {
|
|
248
|
+
// Column may already exist
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
this.db.exec('ALTER TABLE tasks ADD COLUMN deliverableId TEXT');
|
|
253
|
+
} catch {
|
|
254
|
+
// Column may already exist
|
|
255
|
+
}
|
|
222
256
|
}
|
|
223
257
|
|
|
224
258
|
// --- Projects ---
|
|
@@ -496,6 +530,98 @@ export class SQLiteStore implements StorageAdapter {
|
|
|
496
530
|
return (result.changes ?? 0) > 0;
|
|
497
531
|
}
|
|
498
532
|
|
|
533
|
+
// --- Deliverables ---
|
|
534
|
+
async getDeliverables(projectId: string): Promise<Deliverable[]> {
|
|
535
|
+
const stmt = this.db.prepare('SELECT * FROM deliverables WHERE projectId = ?');
|
|
536
|
+
const rows = stmt.all(projectId) as any[];
|
|
537
|
+
return rows.map((r) => this.mapDeliverable(r));
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
async getDeliverable(id: string): Promise<Deliverable | null> {
|
|
541
|
+
const stmt = this.db.prepare('SELECT * FROM deliverables WHERE id = ?');
|
|
542
|
+
const row = stmt.get(id) as any;
|
|
543
|
+
return row ? this.mapDeliverable(row) : null;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
async createDeliverable(deliverable: CreateDeliverableInput): Promise<Deliverable> {
|
|
547
|
+
const id = `deliv_${Math.random().toString(36).substring(2, 9)}`;
|
|
548
|
+
const now = new Date().toISOString();
|
|
549
|
+
const newDeliverable: Deliverable = {
|
|
550
|
+
...deliverable,
|
|
551
|
+
id,
|
|
552
|
+
status: deliverable.status || 'planned',
|
|
553
|
+
outputUrls: deliverable.outputUrls ? [...deliverable.outputUrls] : [],
|
|
554
|
+
customFields: deliverable.customFields ? { ...deliverable.customFields } : {},
|
|
555
|
+
createdAt: now,
|
|
556
|
+
updatedAt: now
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
const stmt = this.db.prepare(`
|
|
560
|
+
INSERT INTO deliverables (
|
|
561
|
+
id, projectId, title, description, status, format, specs, leadId, reviewerId,
|
|
562
|
+
dueDate, deliveredAt, outputUrls, customFields, createdAt, updatedAt
|
|
563
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
564
|
+
`);
|
|
565
|
+
stmt.run(
|
|
566
|
+
newDeliverable.id,
|
|
567
|
+
newDeliverable.projectId,
|
|
568
|
+
newDeliverable.title,
|
|
569
|
+
newDeliverable.description || null,
|
|
570
|
+
newDeliverable.status,
|
|
571
|
+
newDeliverable.format || null,
|
|
572
|
+
newDeliverable.specs ? JSON.stringify(newDeliverable.specs) : null,
|
|
573
|
+
newDeliverable.leadId || null,
|
|
574
|
+
newDeliverable.reviewerId || null,
|
|
575
|
+
newDeliverable.dueDate || null,
|
|
576
|
+
newDeliverable.deliveredAt || null,
|
|
577
|
+
JSON.stringify(newDeliverable.outputUrls || []),
|
|
578
|
+
JSON.stringify(newDeliverable.customFields || {}),
|
|
579
|
+
newDeliverable.createdAt,
|
|
580
|
+
newDeliverable.updatedAt
|
|
581
|
+
);
|
|
582
|
+
return newDeliverable;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
async updateDeliverable(id: string, updates: Partial<Deliverable>): Promise<Deliverable | null> {
|
|
586
|
+
const existing = await this.getDeliverable(id);
|
|
587
|
+
if (!existing) return null;
|
|
588
|
+
|
|
589
|
+
const updated: Deliverable = {
|
|
590
|
+
...existing,
|
|
591
|
+
...updates,
|
|
592
|
+
updatedAt: new Date().toISOString()
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
const stmt = this.db.prepare(`
|
|
596
|
+
UPDATE deliverables SET
|
|
597
|
+
title = ?, description = ?, status = ?, format = ?, specs = ?, leadId = ?, reviewerId = ?,
|
|
598
|
+
dueDate = ?, deliveredAt = ?, outputUrls = ?, customFields = ?, updatedAt = ?
|
|
599
|
+
WHERE id = ?
|
|
600
|
+
`);
|
|
601
|
+
stmt.run(
|
|
602
|
+
updated.title,
|
|
603
|
+
updated.description || null,
|
|
604
|
+
updated.status,
|
|
605
|
+
updated.format || null,
|
|
606
|
+
updated.specs ? JSON.stringify(updated.specs) : null,
|
|
607
|
+
updated.leadId || null,
|
|
608
|
+
updated.reviewerId || null,
|
|
609
|
+
updated.dueDate || null,
|
|
610
|
+
updated.deliveredAt || null,
|
|
611
|
+
JSON.stringify(updated.outputUrls || []),
|
|
612
|
+
JSON.stringify(updated.customFields || {}),
|
|
613
|
+
updated.updatedAt,
|
|
614
|
+
id
|
|
615
|
+
);
|
|
616
|
+
return updated;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
async deleteDeliverable(id: string): Promise<boolean> {
|
|
620
|
+
const stmt = this.db.prepare('DELETE FROM deliverables WHERE id = ?');
|
|
621
|
+
const result = stmt.run(id);
|
|
622
|
+
return (result.changes ?? 0) > 0;
|
|
623
|
+
}
|
|
624
|
+
|
|
499
625
|
// --- Tasks ---
|
|
500
626
|
async getTasks(projectId?: string): Promise<Task[]> {
|
|
501
627
|
let rows: any[];
|
|
@@ -523,11 +649,11 @@ export class SQLiteStore implements StorageAdapter {
|
|
|
523
649
|
const stmt = this.db.prepare(`
|
|
524
650
|
INSERT INTO tasks (
|
|
525
651
|
id, projectId, title, description, status, priority, taskType, assigneeId, reporterId,
|
|
526
|
-
reviewerId, iterationId, teamId, containerId, plannedStartDate, actualStartDate, actualEndDate,
|
|
652
|
+
reviewerId, iterationId, teamId, containerId, deliverableId, plannedStartDate, actualStartDate, actualEndDate,
|
|
527
653
|
dueDate, estimatedHours, loggedHours, actualHours, billableHours,
|
|
528
654
|
estimatedDurationMinutes, actualDurationMinutes, billableDurationMinutes, progress,
|
|
529
655
|
tags, customFields, parentId, createdAt, updatedAt
|
|
530
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
656
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
531
657
|
`);
|
|
532
658
|
stmt.run(
|
|
533
659
|
newTask.id,
|
|
@@ -543,6 +669,7 @@ export class SQLiteStore implements StorageAdapter {
|
|
|
543
669
|
newTask.iterationId || null,
|
|
544
670
|
newTask.teamId || null,
|
|
545
671
|
newTask.containerId || null,
|
|
672
|
+
newTask.deliverableId || null,
|
|
546
673
|
newTask.plannedStartDate || null,
|
|
547
674
|
newTask.actualStartDate || null,
|
|
548
675
|
newTask.actualEndDate || null,
|
|
@@ -577,7 +704,7 @@ export class SQLiteStore implements StorageAdapter {
|
|
|
577
704
|
const stmt = this.db.prepare(`
|
|
578
705
|
UPDATE tasks SET
|
|
579
706
|
projectId = ?, title = ?, description = ?, status = ?, priority = ?, taskType = ?,
|
|
580
|
-
assigneeId = ?, reporterId = ?, reviewerId = ?, iterationId = ?, teamId = ?, containerId = ?,
|
|
707
|
+
assigneeId = ?, reporterId = ?, reviewerId = ?, iterationId = ?, teamId = ?, containerId = ?, deliverableId = ?,
|
|
581
708
|
plannedStartDate = ?, actualStartDate = ?, actualEndDate = ?, dueDate = ?,
|
|
582
709
|
estimatedHours = ?, loggedHours = ?, actualHours = ?, billableHours = ?,
|
|
583
710
|
estimatedDurationMinutes = ?, actualDurationMinutes = ?, billableDurationMinutes = ?, progress = ?,
|
|
@@ -597,6 +724,7 @@ export class SQLiteStore implements StorageAdapter {
|
|
|
597
724
|
updated.iterationId || null,
|
|
598
725
|
updated.teamId || null,
|
|
599
726
|
updated.containerId || null,
|
|
727
|
+
updated.deliverableId || null,
|
|
600
728
|
updated.plannedStartDate || null,
|
|
601
729
|
updated.actualStartDate || null,
|
|
602
730
|
updated.actualEndDate || null,
|
|
@@ -697,6 +825,7 @@ export class SQLiteStore implements StorageAdapter {
|
|
|
697
825
|
authorType: row.authorType,
|
|
698
826
|
parentId: row.parentId || undefined,
|
|
699
827
|
content: row.content,
|
|
828
|
+
mentions: row.mentions ? JSON.parse(row.mentions) : undefined,
|
|
700
829
|
reactions: row.reactions ? JSON.parse(row.reactions) : undefined,
|
|
701
830
|
metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
|
|
702
831
|
createdAt: row.createdAt,
|
|
@@ -723,8 +852,8 @@ export class SQLiteStore implements StorageAdapter {
|
|
|
723
852
|
const newComment: Comment = { ...comment, id, createdAt: now, updatedAt: now };
|
|
724
853
|
|
|
725
854
|
const stmt = this.db.prepare(`
|
|
726
|
-
INSERT INTO comments (id, taskId, authorId, authorType, parentId, content, reactions, metadata, createdAt, updatedAt)
|
|
727
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
855
|
+
INSERT INTO comments (id, taskId, authorId, authorType, parentId, content, mentions, reactions, metadata, createdAt, updatedAt)
|
|
856
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
728
857
|
`);
|
|
729
858
|
stmt.run(
|
|
730
859
|
newComment.id,
|
|
@@ -733,6 +862,7 @@ export class SQLiteStore implements StorageAdapter {
|
|
|
733
862
|
newComment.authorType || 'user',
|
|
734
863
|
newComment.parentId || null,
|
|
735
864
|
newComment.content,
|
|
865
|
+
newComment.mentions ? JSON.stringify(newComment.mentions) : null,
|
|
736
866
|
newComment.reactions ? JSON.stringify(newComment.reactions) : null,
|
|
737
867
|
newComment.metadata ? JSON.stringify(newComment.metadata) : null,
|
|
738
868
|
newComment.createdAt,
|
|
@@ -757,6 +887,7 @@ export class SQLiteStore implements StorageAdapter {
|
|
|
757
887
|
authorType = ?,
|
|
758
888
|
parentId = ?,
|
|
759
889
|
content = ?,
|
|
890
|
+
mentions = ?,
|
|
760
891
|
reactions = ?,
|
|
761
892
|
metadata = ?,
|
|
762
893
|
updatedAt = ?
|
|
@@ -767,6 +898,7 @@ export class SQLiteStore implements StorageAdapter {
|
|
|
767
898
|
updated.authorType || 'user',
|
|
768
899
|
updated.parentId || null,
|
|
769
900
|
updated.content,
|
|
901
|
+
updated.mentions ? JSON.stringify(updated.mentions) : null,
|
|
770
902
|
updated.reactions ? JSON.stringify(updated.reactions) : null,
|
|
771
903
|
updated.metadata ? JSON.stringify(updated.metadata) : null,
|
|
772
904
|
updated.updatedAt,
|
|
@@ -1048,8 +1180,18 @@ export class SQLiteStore implements StorageAdapter {
|
|
|
1048
1180
|
return {
|
|
1049
1181
|
...row,
|
|
1050
1182
|
taskType: row.taskType || undefined,
|
|
1183
|
+
deliverableId: row.deliverableId || undefined,
|
|
1051
1184
|
tags: row.tags ? JSON.parse(row.tags) : [],
|
|
1052
1185
|
customFields: row.customFields ? JSON.parse(row.customFields) : {}
|
|
1053
1186
|
};
|
|
1054
1187
|
}
|
|
1188
|
+
|
|
1189
|
+
private mapDeliverable(row: any): Deliverable {
|
|
1190
|
+
return {
|
|
1191
|
+
...row,
|
|
1192
|
+
specs: row.specs ? JSON.parse(row.specs) : undefined,
|
|
1193
|
+
outputUrls: row.outputUrls ? JSON.parse(row.outputUrls) : [],
|
|
1194
|
+
customFields: row.customFields ? JSON.parse(row.customFields) : {}
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1055
1197
|
}
|
package/src/types/index.ts
CHANGED
|
@@ -142,6 +142,7 @@ export interface Task {
|
|
|
142
142
|
iterationId?: string;
|
|
143
143
|
teamId?: string;
|
|
144
144
|
containerId?: string;
|
|
145
|
+
deliverableId?: string; // Direct link to parent deliverable
|
|
145
146
|
plannedStartDate?: string;
|
|
146
147
|
actualStartDate?: string;
|
|
147
148
|
actualEndDate?: string;
|
|
@@ -169,6 +170,50 @@ export type CreateTaskInput = Omit<Task, 'id' | 'createdAt' | 'updatedAt' | 'sta
|
|
|
169
170
|
priority?: Priority;
|
|
170
171
|
};
|
|
171
172
|
|
|
173
|
+
export type DeliverableStatus =
|
|
174
|
+
| 'draft'
|
|
175
|
+
| 'planned'
|
|
176
|
+
| 'in_production'
|
|
177
|
+
| 'internal_review'
|
|
178
|
+
| 'client_review'
|
|
179
|
+
| 'revision_requested'
|
|
180
|
+
| 'approved'
|
|
181
|
+
| 'delivered'
|
|
182
|
+
| 'canceled'
|
|
183
|
+
| (string & {});
|
|
184
|
+
|
|
185
|
+
export interface Deliverable {
|
|
186
|
+
id: string;
|
|
187
|
+
projectId: string;
|
|
188
|
+
title: string;
|
|
189
|
+
description?: string;
|
|
190
|
+
status: DeliverableStatus;
|
|
191
|
+
format?: string; // e.g., '16:9 4K ProRes', 'Figma', 'PDF'
|
|
192
|
+
specs?: Record<string, unknown>; // Technical specifications (resolution, frame rate, etc.)
|
|
193
|
+
leadId?: string; // Creative director or lead creator
|
|
194
|
+
reviewerId?: string; // Approver or client reviewer
|
|
195
|
+
dueDate?: string;
|
|
196
|
+
deliveredAt?: string;
|
|
197
|
+
outputUrls?: string[]; // Links to final rendered assets, exports, or storage keys
|
|
198
|
+
customFields?: Record<string, unknown>;
|
|
199
|
+
createdAt: string;
|
|
200
|
+
updatedAt: string;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export interface DeliverableSummary {
|
|
204
|
+
deliverable: Deliverable;
|
|
205
|
+
totalTasks: number;
|
|
206
|
+
completedTasks: number;
|
|
207
|
+
activeTasks: number;
|
|
208
|
+
progressPercentage: number;
|
|
209
|
+
estimatedHours: number;
|
|
210
|
+
loggedHours: number;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export type CreateDeliverableInput = Omit<Deliverable, 'id' | 'createdAt' | 'updatedAt' | 'status'> & {
|
|
214
|
+
status?: DeliverableStatus;
|
|
215
|
+
};
|
|
216
|
+
|
|
172
217
|
export type CreateProjectInput = Omit<Project, 'id' | 'createdAt' | 'updatedAt'>;
|
|
173
218
|
|
|
174
219
|
export interface Iteration {
|
|
@@ -198,6 +243,7 @@ export interface Comment {
|
|
|
198
243
|
authorType?: AuthorType;
|
|
199
244
|
parentId?: string; // Threaded reply support
|
|
200
245
|
content: string;
|
|
246
|
+
mentions?: string[];
|
|
201
247
|
reactions?: CommentReaction[];
|
|
202
248
|
metadata?: Record<string, unknown>;
|
|
203
249
|
createdAt: string;
|
|
@@ -308,7 +354,11 @@ export type WebhookEvent =
|
|
|
308
354
|
| 'container.created'
|
|
309
355
|
| 'workflow.created'
|
|
310
356
|
| 'workflow.updated'
|
|
311
|
-
| 'workflow.deleted'
|
|
357
|
+
| 'workflow.deleted'
|
|
358
|
+
| 'deliverable.created'
|
|
359
|
+
| 'deliverable.updated'
|
|
360
|
+
| 'deliverable.deleted'
|
|
361
|
+
| 'deliverable.status_changed';
|
|
312
362
|
|
|
313
363
|
export interface PluginHooks {
|
|
314
364
|
beforeTaskCreate?: (task: Partial<Task>) => Promise<Partial<Task>> | Partial<Task>;
|
|
@@ -342,5 +392,6 @@ export interface CriticalPathConfig {
|
|
|
342
392
|
teams?: Team[];
|
|
343
393
|
containers?: TaskContainer[];
|
|
344
394
|
workflows?: Workflow[];
|
|
395
|
+
deliverables?: Deliverable[];
|
|
345
396
|
};
|
|
346
397
|
}
|
package/src/utils/index.ts
CHANGED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { extractMentions, parseMentionSegments } from './mentions.js';
|
|
3
|
+
|
|
4
|
+
describe('mentions utility', () => {
|
|
5
|
+
describe('extractMentions', () => {
|
|
6
|
+
it('returns empty array for empty or non-string input', () => {
|
|
7
|
+
expect(extractMentions('')).toEqual([]);
|
|
8
|
+
expect(extractMentions(null as any)).toEqual([]);
|
|
9
|
+
expect(extractMentions(undefined as any)).toEqual([]);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('extracts single mention', () => {
|
|
13
|
+
expect(extractMentions('Hello @planner, please review.')).toEqual(['planner']);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('extracts multiple mentions and deduplicates them', () => {
|
|
17
|
+
const text = 'Hey @jane and @planner, can @jane check with @coordinator?';
|
|
18
|
+
expect(extractMentions(text)).toEqual(['jane', 'planner', 'coordinator']);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('supports quoted handles with spaces', () => {
|
|
22
|
+
const text = 'Assigning to @"Agent Planner" and @"Jane Doe"';
|
|
23
|
+
expect(extractMentions(text)).toEqual(['Agent Planner', 'Jane Doe']);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('ignores email addresses without leading space or boundary', () => {
|
|
27
|
+
const text = 'Send mail to user@example.com or ping @alex';
|
|
28
|
+
expect(extractMentions(text)).toEqual(['alex']);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('handles mentions with dashes, underscores, and periods', () => {
|
|
32
|
+
const text = 'Call @agent-1, @super_user, and @john.doe';
|
|
33
|
+
expect(extractMentions(text)).toEqual(['agent-1', 'super_user', 'john.doe']);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('parseMentionSegments', () => {
|
|
38
|
+
it('returns empty array for empty string', () => {
|
|
39
|
+
expect(parseMentionSegments('')).toEqual([]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('returns single text segment if no mentions', () => {
|
|
43
|
+
expect(parseMentionSegments('Just a normal comment without tags.')).toEqual([
|
|
44
|
+
{ type: 'text', value: 'Just a normal comment without tags.' }
|
|
45
|
+
]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('correctly segments text with single mention in the middle', () => {
|
|
49
|
+
const result = parseMentionSegments('Hello @planner, please check.');
|
|
50
|
+
expect(result).toEqual([
|
|
51
|
+
{ type: 'text', value: 'Hello ' },
|
|
52
|
+
{ type: 'mention', value: '@planner', handle: 'planner' },
|
|
53
|
+
{ type: 'text', value: ', please check.' }
|
|
54
|
+
]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('correctly segments text starting with a mention', () => {
|
|
58
|
+
const result = parseMentionSegments('@planner can you take a look?');
|
|
59
|
+
expect(result).toEqual([
|
|
60
|
+
{ type: 'mention', value: '@planner', handle: 'planner' },
|
|
61
|
+
{ type: 'text', value: ' can you take a look?' }
|
|
62
|
+
]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('correctly segments text ending with a mention', () => {
|
|
66
|
+
const result = parseMentionSegments('Assigned to @coordinator');
|
|
67
|
+
expect(result).toEqual([
|
|
68
|
+
{ type: 'text', value: 'Assigned to ' },
|
|
69
|
+
{ type: 'mention', value: '@coordinator', handle: 'coordinator' }
|
|
70
|
+
]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('correctly segments quoted mentions', () => {
|
|
74
|
+
const result = parseMentionSegments('Ping @"Agent Supervisor" ASAP');
|
|
75
|
+
expect(result).toEqual([
|
|
76
|
+
{ type: 'text', value: 'Ping ' },
|
|
77
|
+
{ type: 'mention', value: '@"Agent Supervisor"', handle: 'Agent Supervisor' },
|
|
78
|
+
{ type: 'text', value: ' ASAP' }
|
|
79
|
+
]);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
export interface MentionSegment {
|
|
2
|
+
type: 'text' | 'mention';
|
|
3
|
+
value: string;
|
|
4
|
+
handle?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Regex matching mentions:
|
|
9
|
+
* - Quoted mentions: @"Jane Doe"
|
|
10
|
+
* - Standard mentions: @jane_doe, @planner, @user-1, @john.smith
|
|
11
|
+
* Note: Does not match emails like foo@bar.com (requires word boundary or start of string before @).
|
|
12
|
+
*/
|
|
13
|
+
export const MENTION_REGEX = /(?:^|\s)@(?:"([^"]+)"|([a-zA-Z0-9_.-]+))/g;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Extracts unique mention handles from a text string.
|
|
17
|
+
* Strips quotes and the leading '@' symbol.
|
|
18
|
+
* Preserves the order of their first appearance.
|
|
19
|
+
*/
|
|
20
|
+
export function extractMentions(content: string): string[] {
|
|
21
|
+
if (!content || typeof content !== 'string') return [];
|
|
22
|
+
|
|
23
|
+
const mentions: string[] = [];
|
|
24
|
+
const seen = new Set<string>();
|
|
25
|
+
|
|
26
|
+
// Reset regex lastIndex
|
|
27
|
+
const regex = new RegExp(MENTION_REGEX.source, 'g');
|
|
28
|
+
let match: RegExpExecArray | null;
|
|
29
|
+
|
|
30
|
+
while ((match = regex.exec(content)) !== null) {
|
|
31
|
+
const handle = match[1] ?? match[2];
|
|
32
|
+
if (handle && !seen.has(handle)) {
|
|
33
|
+
seen.add(handle);
|
|
34
|
+
mentions.push(handle);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return mentions;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Segments a text string into plain text parts and mention tokens.
|
|
43
|
+
* Useful for rendering highlighted mention badges or links in UI components.
|
|
44
|
+
*/
|
|
45
|
+
export function parseMentionSegments(content: string): MentionSegment[] {
|
|
46
|
+
if (!content) return [];
|
|
47
|
+
|
|
48
|
+
const segments: MentionSegment[] = [];
|
|
49
|
+
const regex = new RegExp(MENTION_REGEX.source, 'g');
|
|
50
|
+
let lastIndex = 0;
|
|
51
|
+
let match: RegExpExecArray | null;
|
|
52
|
+
|
|
53
|
+
while ((match = regex.exec(content)) !== null) {
|
|
54
|
+
const fullMatch = match[0];
|
|
55
|
+
const leadingWhitespace = fullMatch.startsWith(' ') || fullMatch.startsWith('\t') || fullMatch.startsWith('\n')
|
|
56
|
+
? fullMatch[0]
|
|
57
|
+
: '';
|
|
58
|
+
const mentionToken = fullMatch.slice(leadingWhitespace.length);
|
|
59
|
+
const handle = match[1] ?? match[2];
|
|
60
|
+
const matchStart = match.index + leadingWhitespace.length;
|
|
61
|
+
|
|
62
|
+
// Push text before this mention if any
|
|
63
|
+
if (matchStart > lastIndex) {
|
|
64
|
+
segments.push({
|
|
65
|
+
type: 'text',
|
|
66
|
+
value: content.slice(lastIndex, matchStart)
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Push the mention token
|
|
71
|
+
segments.push({
|
|
72
|
+
type: 'mention',
|
|
73
|
+
value: mentionToken,
|
|
74
|
+
handle
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
lastIndex = match.index + fullMatch.length;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Push any remaining text after the last match
|
|
81
|
+
if (lastIndex < content.length) {
|
|
82
|
+
segments.push({
|
|
83
|
+
type: 'text',
|
|
84
|
+
value: content.slice(lastIndex)
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return segments;
|
|
89
|
+
}
|
package/src/utils/workflow.ts
CHANGED
|
@@ -117,6 +117,44 @@ export const DEFAULT_VFX_WORKFLOW: Workflow = {
|
|
|
117
117
|
updatedAt: '2026-01-01T00:00:00.000Z'
|
|
118
118
|
};
|
|
119
119
|
|
|
120
|
+
export const DEFAULT_CREATIVE_WORKFLOW: Workflow = {
|
|
121
|
+
id: 'wf_creative_default',
|
|
122
|
+
name: 'Creative Production Workflow',
|
|
123
|
+
description: 'Production pipeline for creative deliverables from briefing and concepts to internal review, client feedback, and final delivery.',
|
|
124
|
+
statuses: [
|
|
125
|
+
{ key: 'briefing', label: 'Briefing & Scoping', completionState: 'not_done', executionState: 'inactive' },
|
|
126
|
+
{ key: 'concept', label: 'Concept & Storyboarding', completionState: 'not_done', executionState: 'active' },
|
|
127
|
+
{ key: 'in_production', label: 'In Production', completionState: 'not_done', executionState: 'active' },
|
|
128
|
+
{ key: 'internal_review', label: 'Internal Review', completionState: 'not_done', executionState: 'active' },
|
|
129
|
+
{ key: 'client_review', label: 'Client Review', completionState: 'not_done', executionState: 'active' },
|
|
130
|
+
{ key: 'revision_requested', label: 'Revision Requested', completionState: 'not_done', executionState: 'active' },
|
|
131
|
+
{ key: 'approved', label: 'Approved', completionState: 'done', executionState: 'inactive' },
|
|
132
|
+
{ key: 'delivered', label: 'Delivered', completionState: 'done', executionState: 'inactive' },
|
|
133
|
+
DEFAULT_STATUS_DEFINITIONS.canceled
|
|
134
|
+
],
|
|
135
|
+
transitions: [
|
|
136
|
+
{ name: 'Start Concepts', fromStatusKey: 'briefing', toStatusKey: 'concept' },
|
|
137
|
+
{ name: 'Approve Concept for Production', fromStatusKey: 'concept', toStatusKey: 'in_production' },
|
|
138
|
+
{ name: 'Submit for Internal Review', fromStatusKey: 'in_production', toStatusKey: 'internal_review' },
|
|
139
|
+
{ name: 'Submit for Client Review', fromStatusKey: 'internal_review', toStatusKey: 'client_review' },
|
|
140
|
+
{ name: 'Request Revisions (Internal)', fromStatusKey: 'internal_review', toStatusKey: 'revision_requested' },
|
|
141
|
+
{ name: 'Request Revisions (Client)', fromStatusKey: 'client_review', toStatusKey: 'revision_requested' },
|
|
142
|
+
{ name: 'Address Revisions', fromStatusKey: 'revision_requested', toStatusKey: 'in_production' },
|
|
143
|
+
{ name: 'Approve Work', fromStatusKey: 'client_review', toStatusKey: 'approved' },
|
|
144
|
+
{ name: 'Deliver Final Assets', fromStatusKey: 'approved', toStatusKey: 'delivered' },
|
|
145
|
+
{ name: 'Cancel', fromStatusKey: '*', toStatusKey: 'canceled' }
|
|
146
|
+
],
|
|
147
|
+
taskTypes: [
|
|
148
|
+
{ key: 'deliverable', label: 'Deliverable', description: 'Finished asset, cutdown, or packaged export', icon: 'package' },
|
|
149
|
+
{ key: 'creative_task', label: 'Creative Task', description: 'Production task (e.g. Design, Copy, Editing, Color)', icon: 'pen-tool' },
|
|
150
|
+
{ key: 'review', label: 'Review Gate', description: 'Internal or external client sign-off', icon: 'check-circle' }
|
|
151
|
+
],
|
|
152
|
+
defaultStatusKey: 'briefing',
|
|
153
|
+
isDefault: false,
|
|
154
|
+
createdAt: '2026-01-01T00:00:00.000Z',
|
|
155
|
+
updatedAt: '2026-01-01T00:00:00.000Z'
|
|
156
|
+
};
|
|
157
|
+
|
|
120
158
|
export function validateTransition(
|
|
121
159
|
workflow: Workflow | undefined,
|
|
122
160
|
fromStatus: string,
|