@critical-path/core 0.2.0 → 0.3.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.
Files changed (57) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/engine/index.d.ts +19 -1
  3. package/dist/engine/index.d.ts.map +1 -1
  4. package/dist/engine/index.js +129 -6
  5. package/dist/engine/index.js.map +1 -1
  6. package/dist/index.d.ts +1 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +1 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/index.test.js +96 -1
  11. package/dist/index.test.js.map +1 -1
  12. package/dist/store/firebase.d.ts +19 -7
  13. package/dist/store/firebase.d.ts.map +1 -1
  14. package/dist/store/firebase.js +98 -14
  15. package/dist/store/firebase.js.map +1 -1
  16. package/dist/store/firebase.test.js +5 -2
  17. package/dist/store/firebase.test.js.map +1 -1
  18. package/dist/store/index.d.ts +34 -8
  19. package/dist/store/index.d.ts.map +1 -1
  20. package/dist/store/index.js +79 -12
  21. package/dist/store/index.js.map +1 -1
  22. package/dist/store/sqlite.d.ts +17 -4
  23. package/dist/store/sqlite.d.ts.map +1 -1
  24. package/dist/store/sqlite.js +179 -39
  25. package/dist/store/sqlite.js.map +1 -1
  26. package/dist/types/index.d.ts +70 -9
  27. package/dist/types/index.d.ts.map +1 -1
  28. package/dist/utils/index.d.ts +3 -0
  29. package/dist/utils/index.d.ts.map +1 -0
  30. package/dist/utils/index.js +3 -0
  31. package/dist/utils/index.js.map +1 -0
  32. package/dist/utils/key.d.ts +14 -0
  33. package/dist/utils/key.d.ts.map +1 -0
  34. package/dist/utils/key.js +49 -0
  35. package/dist/utils/key.js.map +1 -0
  36. package/dist/utils/key.test.d.ts +2 -0
  37. package/dist/utils/key.test.d.ts.map +1 -0
  38. package/dist/utils/key.test.js +48 -0
  39. package/dist/utils/key.test.js.map +1 -0
  40. package/dist/utils/status.d.ts +14 -0
  41. package/dist/utils/status.d.ts.map +1 -0
  42. package/dist/utils/status.js +58 -0
  43. package/dist/utils/status.js.map +1 -0
  44. package/package.json +5 -1
  45. package/src/engine/index.ts +161 -8
  46. package/src/index.test.ts +113 -1
  47. package/src/index.ts +2 -0
  48. package/src/store/firebase.test.ts +8 -2
  49. package/src/store/firebase.ts +112 -18
  50. package/src/store/index.ts +113 -18
  51. package/src/store/sqlite.ts +277 -47
  52. package/src/types/index.ts +76 -10
  53. package/src/utils/index.ts +2 -0
  54. package/src/utils/key.test.ts +55 -0
  55. package/src/utils/key.ts +51 -0
  56. package/src/utils/status.ts +85 -0
  57. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Generates a 2-6 character uppercase project short code/key from a project name.
3
+ * e.g., "Mobile App" -> "MA", "Frontend Core" -> "FC", "Operative" -> "OPE".
4
+ */
5
+ export function generateProjectKey(name) {
6
+ if (!name || !name.trim()) {
7
+ return 'PRJ';
8
+ }
9
+ const words = name.trim().split(/\s+/).filter(Boolean);
10
+ let rawKey = '';
11
+ if (words.length === 1) {
12
+ const word = words[0].replace(/[^a-zA-Z0-9]/g, '');
13
+ if (word.length <= 3) {
14
+ rawKey = word.toUpperCase();
15
+ }
16
+ else {
17
+ rawKey = word.substring(0, 3).toUpperCase();
18
+ }
19
+ }
20
+ else {
21
+ rawKey = words
22
+ .map((w) => w.replace(/[^a-zA-Z0-9]/g, '')[0] || '')
23
+ .join('')
24
+ .substring(0, 4)
25
+ .toUpperCase();
26
+ }
27
+ // Ensure key meets basic 2-6 char alphanumeric requirement
28
+ const cleanKey = rawKey.replace(/[^A-Z0-9]/g, '');
29
+ if (cleanKey.length >= 2 && cleanKey.length <= 6) {
30
+ return cleanKey;
31
+ }
32
+ return 'PRJ';
33
+ }
34
+ /**
35
+ * Validates whether a given key meets project key format rules (2-6 uppercase alphanumeric characters).
36
+ */
37
+ export function validateProjectKey(key) {
38
+ if (!key)
39
+ return false;
40
+ return /^[A-Z0-9]{2,6}$/.test(key);
41
+ }
42
+ /**
43
+ * Formats a task key using a project key and task index (e.g. "MOB-12").
44
+ */
45
+ export function formatTaskKey(projectKey, taskNumber) {
46
+ const prefix = projectKey ? projectKey.toUpperCase() : 'TASK';
47
+ return `${prefix}-${taskNumber}`;
48
+ }
49
+ //# sourceMappingURL=key.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"key.js","sourceRoot":"","sources":["../../src/utils/key.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;QAC1B,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACvD,IAAI,MAAM,GAAG,EAAE,CAAC;IAEhB,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC,CAAC;QACnD,IAAI,IAAI,CAAC,MAAM,IAAI,CAAC,EAAE,CAAC;YACrB,MAAM,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;QAC9B,CAAC;aAAM,CAAC;YACN,MAAM,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;QAC9C,CAAC;IACH,CAAC;SAAM,CAAC;QACN,MAAM,GAAG,KAAK;aACX,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;aACnD,IAAI,CAAC,EAAE,CAAC;aACR,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC;aACf,WAAW,EAAE,CAAC;IACnB,CAAC;IAED,2DAA2D;IAC3D,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC;IAClD,IAAI,QAAQ,CAAC,MAAM,IAAI,CAAC,IAAI,QAAQ,CAAC,MAAM,IAAI,CAAC,EAAE,CAAC;QACjD,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,kBAAkB,CAAC,GAAW;IAC5C,IAAI,CAAC,GAAG;QAAE,OAAO,KAAK,CAAC;IACvB,OAAO,iBAAiB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACrC,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,aAAa,CAAC,UAAkB,EAAE,UAAkB;IAClE,MAAM,MAAM,GAAG,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC;IAC9D,OAAO,GAAG,MAAM,IAAI,UAAU,EAAE,CAAC;AACnC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=key.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"key.test.d.ts","sourceRoot":"","sources":["../../src/utils/key.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,48 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { generateProjectKey, validateProjectKey, formatTaskKey } from './key.js';
3
+ describe('Project Key Utilities', () => {
4
+ describe('generateProjectKey', () => {
5
+ it('generates a key for a single word name', () => {
6
+ expect(generateProjectKey('Operative')).toBe('OPE');
7
+ expect(generateProjectKey('App')).toBe('APP');
8
+ expect(generateProjectKey('Go')).toBe('GO');
9
+ });
10
+ it('generates a key for multi-word names', () => {
11
+ expect(generateProjectKey('Mobile App')).toBe('MA');
12
+ expect(generateProjectKey('Frontend Core Platform')).toBe('FCP');
13
+ expect(generateProjectKey('Very Long Project Name Here')).toBe('VLPN');
14
+ });
15
+ it('handles special characters and whitespace', () => {
16
+ expect(generateProjectKey(' v2.0 ')).toBe('V20');
17
+ expect(generateProjectKey('Project #1')).toBe('P1');
18
+ });
19
+ it('returns default PRJ fallback for empty input', () => {
20
+ expect(generateProjectKey('')).toBe('PRJ');
21
+ expect(generateProjectKey(' ')).toBe('PRJ');
22
+ });
23
+ });
24
+ describe('validateProjectKey', () => {
25
+ it('returns true for valid keys', () => {
26
+ expect(validateProjectKey('CP')).toBe(true);
27
+ expect(validateProjectKey('PROJ')).toBe(true);
28
+ expect(validateProjectKey('APP123')).toBe(true);
29
+ });
30
+ it('returns false for invalid keys', () => {
31
+ expect(validateProjectKey('A')).toBe(false); // too short
32
+ expect(validateProjectKey('TOOLONGKEY')).toBe(false); // too long
33
+ expect(validateProjectKey('proj')).toBe(false); // lowercase
34
+ expect(validateProjectKey('P-1')).toBe(false); // special char
35
+ expect(validateProjectKey('')).toBe(false);
36
+ });
37
+ });
38
+ describe('formatTaskKey', () => {
39
+ it('formats project key and task number', () => {
40
+ expect(formatTaskKey('MOB', 42)).toBe('MOB-42');
41
+ expect(formatTaskKey('op', 1)).toBe('OP-1');
42
+ });
43
+ it('uses fallback for missing project key', () => {
44
+ expect(formatTaskKey('', 10)).toBe('TASK-10');
45
+ });
46
+ });
47
+ });
48
+ //# sourceMappingURL=key.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"key.test.js","sourceRoot":"","sources":["../../src/utils/key.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import type { Task, StatusDefinition, CompletionState, ExecutionState } from '../types/index.js';
2
+ export declare const DEFAULT_STATUS_DEFINITIONS: Record<string, StatusDefinition>;
3
+ export interface TaskLifecycleState {
4
+ completionState: CompletionState;
5
+ executionState: ExecutionState;
6
+ isDone: boolean;
7
+ isActive: boolean;
8
+ isCancelled: boolean;
9
+ isOverdue: boolean;
10
+ isUpcoming: boolean;
11
+ }
12
+ export declare function resolveStatusDefinition(statusKey: string, customDefinitions?: StatusDefinition[]): StatusDefinition;
13
+ export declare function deriveTaskLifecycleState(task: Task, customDefinitions?: StatusDefinition[], referenceDate?: Date): TaskLifecycleState;
14
+ //# sourceMappingURL=status.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/utils/status.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,IAAI,EACJ,gBAAgB,EAChB,eAAe,EACf,cAAc,EACf,MAAM,mBAAmB,CAAC;AAE3B,eAAO,MAAM,0BAA0B,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAOvE,CAAC;AAEF,MAAM,WAAW,kBAAkB;IACjC,eAAe,EAAE,eAAe,CAAC;IACjC,cAAc,EAAE,cAAc,CAAC;IAC/B,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,OAAO,CAAC;IAClB,WAAW,EAAE,OAAO,CAAC;IACrB,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,MAAM,EACjB,iBAAiB,CAAC,EAAE,gBAAgB,EAAE,GACrC,gBAAgB,CAkBlB;AAED,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,IAAI,EACV,iBAAiB,CAAC,EAAE,gBAAgB,EAAE,EACtC,aAAa,GAAE,IAAiB,GAC/B,kBAAkB,CA+BpB"}
@@ -0,0 +1,58 @@
1
+ export const DEFAULT_STATUS_DEFINITIONS = {
2
+ backlog: { key: 'backlog', label: 'Backlog', completionState: 'not_done', executionState: 'inactive' },
3
+ todo: { key: 'todo', label: 'To Do', completionState: 'not_done', executionState: 'inactive' },
4
+ in_progress: { key: 'in_progress', label: 'In Progress', completionState: 'not_done', executionState: 'active' },
5
+ in_review: { key: 'in_review', label: 'In Review', completionState: 'not_done', executionState: 'active' },
6
+ done: { key: 'done', label: 'Done', completionState: 'done', executionState: 'inactive' },
7
+ canceled: { key: 'canceled', label: 'Canceled', completionState: 'done', executionState: 'inactive', isCancelled: true }
8
+ };
9
+ export function resolveStatusDefinition(statusKey, customDefinitions) {
10
+ if (customDefinitions) {
11
+ const found = customDefinitions.find((def) => def.key === statusKey);
12
+ if (found)
13
+ return found;
14
+ }
15
+ if (DEFAULT_STATUS_DEFINITIONS[statusKey]) {
16
+ return DEFAULT_STATUS_DEFINITIONS[statusKey];
17
+ }
18
+ // Fallback heuristic for unknown custom statuses:
19
+ const isDoneKey = ['done', 'completed', 'finished', 'closed', 'canceled', 'cancelled'].includes(statusKey.toLowerCase());
20
+ const isActiveKey = ['in_progress', 'active', 'working', 'in_review', 'review'].includes(statusKey.toLowerCase());
21
+ return {
22
+ key: statusKey,
23
+ label: statusKey,
24
+ completionState: isDoneKey ? 'done' : 'not_done',
25
+ executionState: isActiveKey ? 'active' : 'inactive',
26
+ isCancelled: statusKey.toLowerCase().includes('cancel')
27
+ };
28
+ }
29
+ export function deriveTaskLifecycleState(task, customDefinitions, referenceDate = new Date()) {
30
+ const def = resolveStatusDefinition(task.status, customDefinitions);
31
+ const isDone = def.completionState === 'done';
32
+ const isActive = def.executionState === 'active';
33
+ const isCancelled = Boolean(def.isCancelled);
34
+ let isOverdue = false;
35
+ if (!isDone && task.dueDate) {
36
+ const due = new Date(task.dueDate);
37
+ if (!isNaN(due.getTime()) && due < referenceDate) {
38
+ isOverdue = true;
39
+ }
40
+ }
41
+ let isUpcoming = false;
42
+ if (!isDone && !isActive && task.plannedStartDate) {
43
+ const plannedStart = new Date(task.plannedStartDate);
44
+ if (!isNaN(plannedStart.getTime()) && plannedStart > referenceDate) {
45
+ isUpcoming = true;
46
+ }
47
+ }
48
+ return {
49
+ completionState: def.completionState,
50
+ executionState: def.executionState,
51
+ isDone,
52
+ isActive,
53
+ isCancelled,
54
+ isOverdue,
55
+ isUpcoming
56
+ };
57
+ }
58
+ //# sourceMappingURL=status.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status.js","sourceRoot":"","sources":["../../src/utils/status.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,9 +1,13 @@
1
1
  {
2
2
  "name": "@critical-path/core",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Core headless domain engine, plugin architecture, and data stores for Critical Path",
5
5
  "author": "Jack James",
6
6
  "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/Pixerate/critical-path.git"
10
+ },
7
11
  "type": "module",
8
12
  "main": "./dist/index.js",
9
13
  "types": "./dist/index.d.ts",
@@ -1,6 +1,16 @@
1
- import type { CriticalPathConfig, Project, Task, WebhookEvent } from '../types/index.js';
1
+ import type {
2
+ CriticalPathConfig,
3
+ Project,
4
+ Task,
5
+ TaskDependencyGraph,
6
+ Team,
7
+ TaskContainer,
8
+ Iteration,
9
+ WebhookEvent
10
+ } from '../types/index.js';
2
11
  import { StorageAdapter, InMemoryStore } from '../store/index.js';
3
12
  import { PluginRegistry } from '../plugins/index.js';
13
+ import { deriveTaskLifecycleState, type TaskLifecycleState } from '../utils/status.js';
4
14
 
5
15
  export class CriticalPathEngine {
6
16
  public readonly store: StorageAdapter;
@@ -30,6 +40,21 @@ export class CriticalPathEngine {
30
40
  await this.store.createProject(p);
31
41
  }
32
42
  }
43
+ if (data.teams) {
44
+ for (const tm of data.teams) {
45
+ await this.store.createTeam(tm);
46
+ }
47
+ }
48
+ if (data.containers) {
49
+ for (const c of data.containers) {
50
+ await this.store.createContainer(c);
51
+ }
52
+ }
53
+ if (data.iterations) {
54
+ for (const it of data.iterations) {
55
+ await this.store.createIteration(it);
56
+ }
57
+ }
33
58
  if (data.tasks) {
34
59
  for (const t of data.tasks) {
35
60
  await this.store.createTask(t);
@@ -37,8 +62,7 @@ export class CriticalPathEngine {
37
62
  }
38
63
  }
39
64
 
40
- // --- Domain Methods with Plugin Hooks & Audit Logging ---
41
-
65
+ // --- Projects ---
42
66
  async getProjects(): Promise<Project[]> {
43
67
  return this.store.getProjects();
44
68
  }
@@ -59,6 +83,7 @@ export class CriticalPathEngine {
59
83
  return created;
60
84
  }
61
85
 
86
+ // --- Tasks ---
62
87
  async getTasks(projectId?: string): Promise<Task[]> {
63
88
  return this.store.getTasks(projectId);
64
89
  }
@@ -68,7 +93,6 @@ export class CriticalPathEngine {
68
93
  }
69
94
 
70
95
  async createTask(taskInput: Omit<Task, 'id' | 'createdAt' | 'updatedAt'>): Promise<Task> {
71
- // Run plugin beforeTaskCreate hooks
72
96
  const processedInput = await this.plugins.runBeforeTaskCreate(taskInput);
73
97
 
74
98
  const created = await this.store.createTask({
@@ -79,16 +103,27 @@ export class CriticalPathEngine {
79
103
  priority: processedInput.priority || taskInput.priority || 'medium',
80
104
  assigneeId: processedInput.assigneeId ?? taskInput.assigneeId,
81
105
  reporterId: processedInput.reporterId ?? taskInput.reporterId,
82
- sprintId: processedInput.sprintId ?? taskInput.sprintId,
106
+ reviewerId: processedInput.reviewerId ?? taskInput.reviewerId,
107
+ iterationId: processedInput.iterationId ?? taskInput.iterationId,
108
+ teamId: processedInput.teamId ?? taskInput.teamId,
109
+ containerId: processedInput.containerId ?? taskInput.containerId,
110
+ plannedStartDate: processedInput.plannedStartDate ?? taskInput.plannedStartDate,
111
+ actualStartDate: processedInput.actualStartDate ?? taskInput.actualStartDate,
112
+ actualEndDate: processedInput.actualEndDate ?? taskInput.actualEndDate,
83
113
  dueDate: processedInput.dueDate ?? taskInput.dueDate,
84
114
  estimatedHours: processedInput.estimatedHours ?? taskInput.estimatedHours,
85
115
  loggedHours: processedInput.loggedHours ?? taskInput.loggedHours ?? 0,
116
+ actualHours: processedInput.actualHours ?? taskInput.actualHours,
117
+ billableHours: processedInput.billableHours ?? taskInput.billableHours,
118
+ estimatedDurationMinutes: processedInput.estimatedDurationMinutes ?? taskInput.estimatedDurationMinutes,
119
+ actualDurationMinutes: processedInput.actualDurationMinutes ?? taskInput.actualDurationMinutes,
120
+ billableDurationMinutes: processedInput.billableDurationMinutes ?? taskInput.billableDurationMinutes,
121
+ progress: processedInput.progress ?? taskInput.progress ?? 0,
86
122
  tags: processedInput.tags ?? taskInput.tags ?? [],
87
123
  customFields: processedInput.customFields ?? taskInput.customFields ?? {},
88
124
  parentId: processedInput.parentId ?? taskInput.parentId
89
125
  });
90
126
 
91
- // Run plugin afterTaskCreate hooks
92
127
  await this.plugins.runAfterTaskCreate(created);
93
128
 
94
129
  await this.store.logActivity({
@@ -151,18 +186,136 @@ export class CriticalPathEngine {
151
186
  return deleted;
152
187
  }
153
188
 
189
+ // --- Task Dependencies & Graph ---
190
+ async getTaskDependencyGraph(taskId: string): Promise<TaskDependencyGraph> {
191
+ const dependencies = await this.store.getDependencies(taskId);
192
+ const upstreamTaskIds = new Set<string>();
193
+ const downstreamTaskIds = new Set<string>();
194
+
195
+ for (const dep of dependencies) {
196
+ if (dep.taskId === taskId) {
197
+ upstreamTaskIds.add(dep.dependsOnTaskId);
198
+ } else if (dep.dependsOnTaskId === taskId) {
199
+ downstreamTaskIds.add(dep.taskId);
200
+ }
201
+ }
202
+
203
+ const upstreamTasks = (
204
+ await Promise.all(Array.from(upstreamTaskIds).map((id) => this.store.getTask(id)))
205
+ ).filter((t): t is Task => t !== null);
206
+
207
+ const downstreamTasks = (
208
+ await Promise.all(Array.from(downstreamTaskIds).map((id) => this.store.getTask(id)))
209
+ ).filter((t): t is Task => t !== null);
210
+
211
+ return {
212
+ taskId,
213
+ upstreamTasks,
214
+ downstreamTasks,
215
+ dependencies
216
+ };
217
+ }
218
+
219
+ async getTaskLifecycleState(taskId: string): Promise<TaskLifecycleState | null> {
220
+ const task = await this.getTask(taskId);
221
+ if (!task) return null;
222
+ const project = await this.getProject(task.projectId);
223
+ return deriveTaskLifecycleState(task, project?.statusDefinitions);
224
+ }
225
+
226
+ // --- Teams ---
227
+ async getTeams(): Promise<Team[]> {
228
+ return this.store.getTeams();
229
+ }
230
+
231
+ async getTeam(id: string): Promise<Team | null> {
232
+ return this.store.getTeam(id);
233
+ }
234
+
235
+ async createTeam(team: Omit<Team, 'id' | 'createdAt' | 'updatedAt'>): Promise<Team> {
236
+ const created = await this.store.createTeam(team);
237
+ this.dispatchWebhook('team.created', { team: created });
238
+ return created;
239
+ }
240
+
241
+ async updateTeam(id: string, updates: Partial<Team>): Promise<Team | null> {
242
+ return this.store.updateTeam(id, updates);
243
+ }
244
+
245
+ async deleteTeam(id: string): Promise<boolean> {
246
+ return this.store.deleteTeam(id);
247
+ }
248
+
249
+ // --- Containers ---
250
+ async getContainers(projectId: string): Promise<TaskContainer[]> {
251
+ return this.store.getContainers(projectId);
252
+ }
253
+
254
+ async getContainer(id: string): Promise<TaskContainer | null> {
255
+ return this.store.getContainer(id);
256
+ }
257
+
258
+ async createContainer(container: Omit<TaskContainer, 'id' | 'createdAt' | 'updatedAt'>): Promise<TaskContainer> {
259
+ const created = await this.store.createContainer(container);
260
+ this.dispatchWebhook('container.created', { container: created });
261
+ return created;
262
+ }
263
+
264
+ async updateContainer(id: string, updates: Partial<TaskContainer>): Promise<TaskContainer | null> {
265
+ return this.store.updateContainer(id, updates);
266
+ }
267
+
268
+ async deleteContainer(id: string): Promise<boolean> {
269
+ return this.store.deleteContainer(id);
270
+ }
271
+
272
+ // --- Iterations ---
273
+ async getIterations(projectId: string): Promise<Iteration[]> {
274
+ return this.store.getIterations(projectId);
275
+ }
276
+
277
+ async getIteration(id: string): Promise<Iteration | null> {
278
+ return this.store.getIteration(id);
279
+ }
280
+
281
+ async createIteration(iteration: Omit<Iteration, 'id' | 'createdAt'>): Promise<Iteration> {
282
+ const created = await this.store.createIteration(iteration);
283
+ if (created.status === 'active') {
284
+ this.dispatchWebhook('iteration.started', { iteration: created });
285
+ }
286
+ return created;
287
+ }
288
+
289
+ async updateIteration(id: string, updates: Partial<Iteration>): Promise<Iteration | null> {
290
+ const existing = await this.store.getIteration(id);
291
+ if (!existing) return null;
292
+
293
+ const updated = await this.store.updateIteration(id, updates);
294
+ if (updated) {
295
+ if (existing.status !== 'active' && updated.status === 'active') {
296
+ this.dispatchWebhook('iteration.started', { iteration: updated });
297
+ } else if (existing.status !== 'completed' && updated.status === 'completed') {
298
+ this.dispatchWebhook('iteration.completed', { iteration: updated });
299
+ }
300
+ }
301
+ return updated;
302
+ }
303
+
304
+ async deleteIteration(id: string): Promise<boolean> {
305
+ return this.store.deleteIteration(id);
306
+ }
307
+
154
308
  private async dispatchWebhook(event: WebhookEvent, payload: Record<string, unknown>): Promise<void> {
155
309
  const webhooks = await this.store.getWebhooks();
156
310
  const active = webhooks.filter((w) => w.active && w.events.includes(event));
157
311
 
158
312
  for (const wh of active) {
159
- // Fire-and-forget webhook post
160
313
  fetch(wh.url, {
161
314
  method: 'POST',
162
315
  headers: { 'Content-Type': 'application/json' },
163
316
  body: JSON.stringify({ event, timestamp: new Date().toISOString(), data: payload })
164
317
  }).catch(() => {
165
- // Silent catch for webhook delivery errors in dev
318
+ // Silent catch for webhook errors in dev
166
319
  });
167
320
  }
168
321
  }
package/src/index.test.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { CriticalPathEngine } from './engine/index.js';
2
+ import { CriticalPathEngine, deriveTaskLifecycleState } from './index.js';
3
3
  import type { CriticalPathPlugin } from './types/index.js';
4
4
 
5
5
  describe('CriticalPathEngine Core Tests', () => {
@@ -60,4 +60,116 @@ describe('CriticalPathEngine Core Tests', () => {
60
60
  expect(hookExecuted).toBe(true);
61
61
  expect(task.tags).toContain('auto-tagged');
62
62
  });
63
+
64
+ it('handles teams, containers, and iterations', async () => {
65
+ const engine = new CriticalPathEngine();
66
+
67
+ const team = await engine.createTeam({
68
+ name: 'Engineering',
69
+ memberIds: ['user_1', 'user_2']
70
+ });
71
+
72
+ const proj = await engine.createProject({
73
+ key: 'ENG',
74
+ name: 'Engineering Project',
75
+ teamIds: [team.id]
76
+ });
77
+
78
+ const container = await engine.createContainer({
79
+ projectId: proj.id,
80
+ name: 'Core Platform Epic',
81
+ type: 'epic'
82
+ });
83
+
84
+ const iteration = await engine.createIteration({
85
+ projectId: proj.id,
86
+ name: 'Sprint 1',
87
+ type: 'sprint',
88
+ status: 'active'
89
+ });
90
+
91
+ const task = await engine.createTask({
92
+ projectId: proj.id,
93
+ title: 'Setup Database Schema',
94
+ status: 'in_progress',
95
+ priority: 'urgent',
96
+ reviewerId: 'user_2',
97
+ teamId: team.id,
98
+ containerId: container.id,
99
+ iterationId: iteration.id,
100
+ plannedStartDate: '2026-09-01T00:00:00.000Z',
101
+ dueDate: '2026-09-15T00:00:00.000Z',
102
+ estimatedHours: 10,
103
+ actualHours: 8,
104
+ billableHours: 8,
105
+ estimatedDurationMinutes: 600,
106
+ actualDurationMinutes: 480,
107
+ billableDurationMinutes: 480,
108
+ progress: 80
109
+ });
110
+
111
+ expect(task.reviewerId).toBe('user_2');
112
+ expect(task.teamId).toBe(team.id);
113
+ expect(task.containerId).toBe(container.id);
114
+ expect(task.iterationId).toBe(iteration.id);
115
+ expect(task.estimatedHours).toBe(10);
116
+ expect(task.actualHours).toBe(8);
117
+ expect(task.billableHours).toBe(8);
118
+ expect(task.estimatedDurationMinutes).toBe(600);
119
+ expect(task.actualDurationMinutes).toBe(480);
120
+ expect(task.billableDurationMinutes).toBe(480);
121
+ expect(task.progress).toBe(80);
122
+ });
123
+
124
+ it('evaluates status definitions and task lifecycle state', async () => {
125
+ const engine = new CriticalPathEngine();
126
+
127
+ const proj = await engine.createProject({
128
+ key: 'CUST',
129
+ name: 'Custom Workflow Project',
130
+ statusDefinitions: [
131
+ { key: 'draft', label: 'Draft', completionState: 'not_done', executionState: 'inactive' },
132
+ { key: 'active_work', label: 'Active Work', completionState: 'not_done', executionState: 'active' },
133
+ { key: 'finished', label: 'Finished', completionState: 'done', executionState: 'inactive' }
134
+ ]
135
+ });
136
+
137
+ const task = await engine.createTask({
138
+ projectId: proj.id,
139
+ title: 'Workflow Task',
140
+ status: 'active_work',
141
+ priority: 'medium',
142
+ plannedStartDate: '2026-08-01T00:00:00.000Z',
143
+ dueDate: '2026-08-10T00:00:00.000Z'
144
+ });
145
+
146
+ const state = await engine.getTaskLifecycleState(task.id);
147
+ expect(state).not.toBeNull();
148
+ expect(state?.completionState).toBe('not_done');
149
+ expect(state?.executionState).toBe('active');
150
+ expect(state?.isActive).toBe(true);
151
+ expect(state?.isDone).toBe(false);
152
+ });
153
+
154
+ it('builds task dependency graph with upstream and downstream tasks', async () => {
155
+ const engine = new CriticalPathEngine();
156
+
157
+ const proj = await engine.createProject({ key: 'DEP', name: 'Dependency Project' });
158
+
159
+ const taskA = await engine.createTask({ projectId: proj.id, title: 'Task A', status: 'todo', priority: 'medium' });
160
+ const taskB = await engine.createTask({ projectId: proj.id, title: 'Task B', status: 'todo', priority: 'medium' });
161
+
162
+ // Task B depends on Task A
163
+ await engine.store.addDependency({
164
+ taskId: taskB.id,
165
+ dependsOnTaskId: taskA.id,
166
+ type: 'blocking'
167
+ });
168
+
169
+ const graphA = await engine.getTaskDependencyGraph(taskA.id);
170
+ expect(graphA.downstreamTasks.map((t) => t.id)).toContain(taskB.id);
171
+
172
+ const graphB = await engine.getTaskDependencyGraph(taskB.id);
173
+ expect(graphB.upstreamTasks.map((t) => t.id)).toContain(taskA.id);
174
+ });
63
175
  });
package/src/index.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export * from './types/index.js';
2
+ export * from './utils/index.js';
2
3
  export * from './store/index.js';
3
4
  export * from './plugins/index.js';
4
5
  export * from './engine/index.js';
6
+
@@ -1,11 +1,17 @@
1
1
  import { describe, it, expect, beforeEach } from 'vitest';
2
- import { FirebaseStore } from './firebase.js';
2
+ import { FirebaseStore, InMemoryFirestoreMock } from './firebase.js';
3
3
 
4
4
  describe('FirebaseStore', () => {
5
5
  let store: FirebaseStore;
6
6
 
7
7
  beforeEach(() => {
8
- store = new FirebaseStore();
8
+ store = new FirebaseStore({ db: new InMemoryFirestoreMock() });
9
+ });
10
+
11
+ it('should throw an error if db is missing in config', () => {
12
+ expect(() => new FirebaseStore({} as any)).toThrow(
13
+ 'FirebaseStore requires a valid Firestore db instance'
14
+ );
9
15
  });
10
16
 
11
17
  it('should create and retrieve projects in FirebaseStore', async () => {