@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.
- package/CHANGELOG.md +12 -0
- package/dist/engine/index.d.ts +19 -1
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +129 -6
- package/dist/engine/index.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/index.test.js +96 -1
- package/dist/index.test.js.map +1 -1
- package/dist/store/firebase.d.ts +19 -7
- package/dist/store/firebase.d.ts.map +1 -1
- package/dist/store/firebase.js +98 -14
- package/dist/store/firebase.js.map +1 -1
- package/dist/store/firebase.test.js +5 -2
- package/dist/store/firebase.test.js.map +1 -1
- package/dist/store/index.d.ts +34 -8
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +79 -12
- package/dist/store/index.js.map +1 -1
- package/dist/store/sqlite.d.ts +17 -4
- package/dist/store/sqlite.d.ts.map +1 -1
- package/dist/store/sqlite.js +179 -39
- package/dist/store/sqlite.js.map +1 -1
- package/dist/types/index.d.ts +70 -9
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +3 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/key.d.ts +14 -0
- package/dist/utils/key.d.ts.map +1 -0
- package/dist/utils/key.js +49 -0
- package/dist/utils/key.js.map +1 -0
- package/dist/utils/key.test.d.ts +2 -0
- package/dist/utils/key.test.d.ts.map +1 -0
- package/dist/utils/key.test.js +48 -0
- package/dist/utils/key.test.js.map +1 -0
- package/dist/utils/status.d.ts +14 -0
- package/dist/utils/status.d.ts.map +1 -0
- package/dist/utils/status.js +58 -0
- package/dist/utils/status.js.map +1 -0
- package/package.json +5 -1
- package/src/engine/index.ts +161 -8
- package/src/index.test.ts +113 -1
- package/src/index.ts +2 -0
- package/src/store/firebase.test.ts +8 -2
- package/src/store/firebase.ts +112 -18
- package/src/store/index.ts +113 -18
- package/src/store/sqlite.ts +277 -47
- package/src/types/index.ts +76 -10
- package/src/utils/index.ts +2 -0
- package/src/utils/key.test.ts +55 -0
- package/src/utils/key.ts +51 -0
- package/src/utils/status.ts +85 -0
- 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 @@
|
|
|
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.
|
|
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",
|
package/src/engine/index.ts
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
|
-
import type {
|
|
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
|
-
// ---
|
|
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
|
-
|
|
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
|
|
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 './
|
|
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,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 () => {
|