@critical-path/core 0.2.1 → 0.4.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/README.md +1 -1
- package/dist/engine/index.d.ts +8 -1
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +90 -2
- package/dist/engine/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.test.js +44 -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 +40 -1
- package/dist/store/firebase.js.map +1 -1
- package/dist/store/firebase.test.js +16 -0
- package/dist/store/firebase.test.js.map +1 -1
- package/dist/store/index.d.ts +12 -1
- 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 +84 -11
- package/dist/store/sqlite.js.map +1 -1
- package/dist/store/sqlite.test.js +21 -0
- package/dist/store/sqlite.test.js.map +1 -1
- package/dist/types/index.d.ts +34 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +4 -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/workflow.d.ts +13 -0
- package/dist/utils/workflow.d.ts.map +1 -0
- package/dist/utils/workflow.js +101 -0
- package/dist/utils/workflow.js.map +1 -0
- package/package.json +1 -1
- package/src/engine/index.ts +103 -3
- package/src/index.test.ts +54 -0
- package/src/index.ts +2 -1
- package/src/store/firebase.test.ts +21 -0
- package/src/store/firebase.ts +45 -2
- package/src/store/index.ts +51 -1
- package/src/store/sqlite.test.ts +28 -0
- package/src/store/sqlite.ts +119 -10
- package/src/types/index.ts +40 -2
- package/src/utils/index.ts +4 -0
- package/src/utils/key.test.ts +55 -0
- package/src/utils/key.ts +51 -0
- package/src/utils/workflow.ts +128 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,eAAe,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,eAAe,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
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 declare function generateProjectKey(name: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* Validates whether a given key meets project key format rules (2-6 uppercase alphanumeric characters).
|
|
8
|
+
*/
|
|
9
|
+
export declare function validateProjectKey(key: string): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Formats a task key using a project key and task index (e.g. "MOB-12").
|
|
12
|
+
*/
|
|
13
|
+
export declare function formatTaskKey(projectKey: string, taskNumber: number): string;
|
|
14
|
+
//# sourceMappingURL=key.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"key.d.ts","sourceRoot":"","sources":["../../src/utils/key.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA8BvD;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAGvD;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAG5E"}
|
|
@@ -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,13 @@
|
|
|
1
|
+
import type { Workflow, TaskTypeDefinition } from '../types/index.js';
|
|
2
|
+
export declare class WorkflowValidationError extends Error {
|
|
3
|
+
readonly fromStatus: string;
|
|
4
|
+
readonly toStatus: string;
|
|
5
|
+
readonly workflowId?: string;
|
|
6
|
+
constructor(fromStatus: string, toStatus: string, workflowId?: string, message?: string);
|
|
7
|
+
}
|
|
8
|
+
export declare const DEFAULT_TASK_TYPES: TaskTypeDefinition[];
|
|
9
|
+
export declare const DEFAULT_SOFTWARE_WORKFLOW: Workflow;
|
|
10
|
+
export declare const DEFAULT_SIMPLE_WORKFLOW: Workflow;
|
|
11
|
+
export declare function validateTransition(workflow: Workflow | undefined, fromStatus: string, toStatus: string): boolean;
|
|
12
|
+
export declare function getAllowedNextStatuses(workflow: Workflow | undefined, currentStatus: string): string[];
|
|
13
|
+
//# sourceMappingURL=workflow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workflow.d.ts","sourceRoot":"","sources":["../../src/utils/workflow.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,QAAQ,EAER,kBAAkB,EACnB,MAAM,mBAAmB,CAAC;AAG3B,qBAAa,uBAAwB,SAAQ,KAAK;IAChD,SAAgB,UAAU,EAAE,MAAM,CAAC;IACnC,SAAgB,QAAQ,EAAE,MAAM,CAAC;IACjC,SAAgB,UAAU,CAAC,EAAE,MAAM,CAAC;gBAExB,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM;CAQxF;AAED,eAAO,MAAM,kBAAkB,EAAE,kBAAkB,EAMlD,CAAC;AAEF,eAAO,MAAM,yBAAyB,EAAE,QA6BvC,CAAC;AAEF,eAAO,MAAM,uBAAuB,EAAE,QAoBrC,CAAC;AAEF,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,QAAQ,GAAG,SAAS,EAC9B,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GACf,OAAO,CAST;AAED,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,QAAQ,GAAG,SAAS,EAC9B,aAAa,EAAE,MAAM,GACpB,MAAM,EAAE,CA0BV"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { DEFAULT_STATUS_DEFINITIONS } from './status.js';
|
|
2
|
+
export class WorkflowValidationError extends Error {
|
|
3
|
+
fromStatus;
|
|
4
|
+
toStatus;
|
|
5
|
+
workflowId;
|
|
6
|
+
constructor(fromStatus, toStatus, workflowId, message) {
|
|
7
|
+
const msg = message || `Illegal status transition from "${fromStatus}" to "${toStatus}"${workflowId ? ` in workflow "${workflowId}"` : ''}.`;
|
|
8
|
+
super(msg);
|
|
9
|
+
this.name = 'WorkflowValidationError';
|
|
10
|
+
this.fromStatus = fromStatus;
|
|
11
|
+
this.toStatus = toStatus;
|
|
12
|
+
this.workflowId = workflowId;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export const DEFAULT_TASK_TYPES = [
|
|
16
|
+
{ key: 'task', label: 'Task', description: 'A standard work item', icon: 'check-square' },
|
|
17
|
+
{ key: 'bug', label: 'Bug', description: 'A problem or defect that needs fixing', icon: 'bug' },
|
|
18
|
+
{ key: 'feature', label: 'Feature', description: 'A new capability or enhancement', icon: 'sparkles' },
|
|
19
|
+
{ key: 'epic', label: 'Epic', description: 'A large body of work that spans multiple tasks', icon: 'layers' },
|
|
20
|
+
{ key: 'subtask', label: 'Subtask', description: 'A piece of work required to complete a parent task', icon: 'corner-down-right' }
|
|
21
|
+
];
|
|
22
|
+
export const DEFAULT_SOFTWARE_WORKFLOW = {
|
|
23
|
+
id: 'wf_software_default',
|
|
24
|
+
name: 'Software Development Workflow',
|
|
25
|
+
description: 'Standard software development lifecycle with backlog, review, and completion states.',
|
|
26
|
+
statuses: [
|
|
27
|
+
DEFAULT_STATUS_DEFINITIONS.backlog,
|
|
28
|
+
DEFAULT_STATUS_DEFINITIONS.todo,
|
|
29
|
+
DEFAULT_STATUS_DEFINITIONS.in_progress,
|
|
30
|
+
DEFAULT_STATUS_DEFINITIONS.in_review,
|
|
31
|
+
DEFAULT_STATUS_DEFINITIONS.done,
|
|
32
|
+
DEFAULT_STATUS_DEFINITIONS.canceled
|
|
33
|
+
],
|
|
34
|
+
transitions: [
|
|
35
|
+
{ name: 'Start Work', fromStatusKey: 'backlog', toStatusKey: 'todo' },
|
|
36
|
+
{ name: 'Start Implementation', fromStatusKey: 'todo', toStatusKey: 'in_progress' },
|
|
37
|
+
{ name: 'Complete Task', fromStatusKey: 'todo', toStatusKey: 'done' },
|
|
38
|
+
{ name: 'Submit for Review', fromStatusKey: 'in_progress', toStatusKey: 'in_review' },
|
|
39
|
+
{ name: 'Complete Implementation', fromStatusKey: 'in_progress', toStatusKey: 'done' },
|
|
40
|
+
{ name: 'Approve & Complete', fromStatusKey: 'in_review', toStatusKey: 'done' },
|
|
41
|
+
{ name: 'Request Changes', fromStatusKey: 'in_review', toStatusKey: 'in_progress' },
|
|
42
|
+
{ name: 'Reopen', fromStatusKey: 'done', toStatusKey: 'todo' },
|
|
43
|
+
{ name: 'Cancel', fromStatusKey: '*', toStatusKey: 'canceled' },
|
|
44
|
+
{ name: 'Restore', fromStatusKey: 'canceled', toStatusKey: 'todo' }
|
|
45
|
+
],
|
|
46
|
+
taskTypes: DEFAULT_TASK_TYPES,
|
|
47
|
+
defaultStatusKey: 'todo',
|
|
48
|
+
isDefault: true,
|
|
49
|
+
createdAt: '2026-01-01T00:00:00.000Z',
|
|
50
|
+
updatedAt: '2026-01-01T00:00:00.000Z'
|
|
51
|
+
};
|
|
52
|
+
export const DEFAULT_SIMPLE_WORKFLOW = {
|
|
53
|
+
id: 'wf_simple_default',
|
|
54
|
+
name: 'Simple Task Workflow',
|
|
55
|
+
description: 'Lightweight workflow with To Do, In Progress, and Done states.',
|
|
56
|
+
statuses: [
|
|
57
|
+
DEFAULT_STATUS_DEFINITIONS.todo,
|
|
58
|
+
DEFAULT_STATUS_DEFINITIONS.in_progress,
|
|
59
|
+
DEFAULT_STATUS_DEFINITIONS.done
|
|
60
|
+
],
|
|
61
|
+
transitions: [
|
|
62
|
+
{ name: 'Start', fromStatusKey: 'todo', toStatusKey: 'in_progress' },
|
|
63
|
+
{ name: 'Complete', fromStatusKey: 'todo', toStatusKey: 'done' },
|
|
64
|
+
{ name: 'Complete', fromStatusKey: 'in_progress', toStatusKey: 'done' },
|
|
65
|
+
{ name: 'Reopen', fromStatusKey: 'done', toStatusKey: 'todo' }
|
|
66
|
+
],
|
|
67
|
+
taskTypes: DEFAULT_TASK_TYPES.filter((t) => ['task', 'subtask'].includes(t.key)),
|
|
68
|
+
defaultStatusKey: 'todo',
|
|
69
|
+
isDefault: true,
|
|
70
|
+
createdAt: '2026-01-01T00:00:00.000Z',
|
|
71
|
+
updatedAt: '2026-01-01T00:00:00.000Z'
|
|
72
|
+
};
|
|
73
|
+
export function validateTransition(workflow, fromStatus, toStatus) {
|
|
74
|
+
if (fromStatus === toStatus)
|
|
75
|
+
return true;
|
|
76
|
+
if (!workflow || !workflow.transitions || workflow.transitions.length === 0)
|
|
77
|
+
return true;
|
|
78
|
+
return workflow.transitions.some((t) => (t.fromStatusKey === '*' || t.fromStatusKey === fromStatus) &&
|
|
79
|
+
(t.toStatusKey === '*' || t.toStatusKey === toStatus));
|
|
80
|
+
}
|
|
81
|
+
export function getAllowedNextStatuses(workflow, currentStatus) {
|
|
82
|
+
if (!workflow || !workflow.statuses || workflow.statuses.length === 0) {
|
|
83
|
+
return Object.keys(DEFAULT_STATUS_DEFINITIONS);
|
|
84
|
+
}
|
|
85
|
+
const allStatusKeys = workflow.statuses.map((s) => s.key);
|
|
86
|
+
if (!workflow.transitions || workflow.transitions.length === 0) {
|
|
87
|
+
return allStatusKeys;
|
|
88
|
+
}
|
|
89
|
+
const matches = workflow.transitions.filter((t) => t.fromStatusKey === '*' || t.fromStatusKey === currentStatus);
|
|
90
|
+
if (matches.some((t) => t.toStatusKey === '*')) {
|
|
91
|
+
return allStatusKeys;
|
|
92
|
+
}
|
|
93
|
+
const allowed = new Set();
|
|
94
|
+
for (const t of matches) {
|
|
95
|
+
if (allStatusKeys.includes(t.toStatusKey)) {
|
|
96
|
+
allowed.add(t.toStatusKey);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return Array.from(allowed);
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=workflow.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workflow.js","sourceRoot":"","sources":["../../src/utils/workflow.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/engine/index.ts
CHANGED
|
@@ -6,11 +6,18 @@ import type {
|
|
|
6
6
|
Team,
|
|
7
7
|
TaskContainer,
|
|
8
8
|
Iteration,
|
|
9
|
-
WebhookEvent
|
|
9
|
+
WebhookEvent,
|
|
10
|
+
Workflow
|
|
10
11
|
} from '../types/index.js';
|
|
11
12
|
import { StorageAdapter, InMemoryStore } from '../store/index.js';
|
|
12
13
|
import { PluginRegistry } from '../plugins/index.js';
|
|
13
14
|
import { deriveTaskLifecycleState, type TaskLifecycleState } from '../utils/status.js';
|
|
15
|
+
import {
|
|
16
|
+
validateTransition,
|
|
17
|
+
getAllowedNextStatuses,
|
|
18
|
+
WorkflowValidationError,
|
|
19
|
+
DEFAULT_SOFTWARE_WORKFLOW
|
|
20
|
+
} from '../utils/workflow.js';
|
|
14
21
|
|
|
15
22
|
export class CriticalPathEngine {
|
|
16
23
|
public readonly store: StorageAdapter;
|
|
@@ -35,6 +42,11 @@ export class CriticalPathEngine {
|
|
|
35
42
|
}
|
|
36
43
|
|
|
37
44
|
private async seedInitialData(data: NonNullable<CriticalPathConfig['initialData']>): Promise<void> {
|
|
45
|
+
if (data.workflows) {
|
|
46
|
+
for (const wf of data.workflows) {
|
|
47
|
+
await this.store.createWorkflow(wf);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
38
50
|
if (data.projects) {
|
|
39
51
|
for (const p of data.projects) {
|
|
40
52
|
await this.store.createProject(p);
|
|
@@ -62,6 +74,81 @@ export class CriticalPathEngine {
|
|
|
62
74
|
}
|
|
63
75
|
}
|
|
64
76
|
|
|
77
|
+
// --- Workflows ---
|
|
78
|
+
async getWorkflows(): Promise<Workflow[]> {
|
|
79
|
+
return this.store.getWorkflows();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async getWorkflow(id: string): Promise<Workflow | null> {
|
|
83
|
+
return this.store.getWorkflow(id);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async createWorkflow(workflow: Omit<Workflow, 'id' | 'createdAt' | 'updatedAt'>): Promise<Workflow> {
|
|
87
|
+
const created = await this.store.createWorkflow(workflow);
|
|
88
|
+
await this.store.logActivity({
|
|
89
|
+
actorId: 'system',
|
|
90
|
+
action: 'workflow.created',
|
|
91
|
+
details: { name: created.name }
|
|
92
|
+
});
|
|
93
|
+
this.dispatchWebhook('workflow.created', { workflow: created });
|
|
94
|
+
return created;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async updateWorkflow(id: string, updates: Partial<Workflow>): Promise<Workflow | null> {
|
|
98
|
+
const existing = await this.store.getWorkflow(id);
|
|
99
|
+
if (!existing) return null;
|
|
100
|
+
|
|
101
|
+
const updated = await this.store.updateWorkflow(id, updates);
|
|
102
|
+
if (updated) {
|
|
103
|
+
await this.store.logActivity({
|
|
104
|
+
actorId: 'system',
|
|
105
|
+
action: 'workflow.updated',
|
|
106
|
+
details: { name: updated.name }
|
|
107
|
+
});
|
|
108
|
+
this.dispatchWebhook('workflow.updated', { workflow: updated, previous: existing });
|
|
109
|
+
}
|
|
110
|
+
return updated;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async deleteWorkflow(id: string): Promise<boolean> {
|
|
114
|
+
const existing = await this.store.getWorkflow(id);
|
|
115
|
+
if (!existing) return false;
|
|
116
|
+
|
|
117
|
+
const deleted = await this.store.deleteWorkflow(id);
|
|
118
|
+
if (deleted) {
|
|
119
|
+
await this.store.logActivity({
|
|
120
|
+
actorId: 'system',
|
|
121
|
+
action: 'workflow.deleted',
|
|
122
|
+
details: { name: existing.name }
|
|
123
|
+
});
|
|
124
|
+
this.dispatchWebhook('workflow.deleted', { workflowId: id, name: existing.name });
|
|
125
|
+
}
|
|
126
|
+
return deleted;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async resolveProjectWorkflow(projectId: string): Promise<Workflow | null> {
|
|
130
|
+
const project = await this.store.getProject(projectId);
|
|
131
|
+
if (project?.workflow) return project.workflow;
|
|
132
|
+
if (project?.workflowId) {
|
|
133
|
+
const wf = await this.store.getWorkflow(project.workflowId);
|
|
134
|
+
if (wf) return wf;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const workflows = await this.store.getWorkflows();
|
|
138
|
+
const defaultWf = workflows.find((w) => w.isDefault);
|
|
139
|
+
if (defaultWf) return defaultWf;
|
|
140
|
+
if (workflows.length > 0) return workflows[0];
|
|
141
|
+
|
|
142
|
+
return DEFAULT_SOFTWARE_WORKFLOW;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async getAllowedTaskTransitions(taskId: string): Promise<string[]> {
|
|
146
|
+
const task = await this.store.getTask(taskId);
|
|
147
|
+
if (!task) return [];
|
|
148
|
+
const workflow = await this.resolveProjectWorkflow(task.projectId);
|
|
149
|
+
return getAllowedNextStatuses(workflow || undefined, task.status);
|
|
150
|
+
}
|
|
151
|
+
|
|
65
152
|
// --- Projects ---
|
|
66
153
|
async getProjects(): Promise<Project[]> {
|
|
67
154
|
return this.store.getProjects();
|
|
@@ -94,13 +181,18 @@ export class CriticalPathEngine {
|
|
|
94
181
|
|
|
95
182
|
async createTask(taskInput: Omit<Task, 'id' | 'createdAt' | 'updatedAt'>): Promise<Task> {
|
|
96
183
|
const processedInput = await this.plugins.runBeforeTaskCreate(taskInput);
|
|
184
|
+
const projectId = processedInput.projectId || taskInput.projectId;
|
|
185
|
+
const workflow = await this.resolveProjectWorkflow(projectId);
|
|
186
|
+
|
|
187
|
+
const defaultStatus = workflow?.defaultStatusKey || 'todo';
|
|
97
188
|
|
|
98
189
|
const created = await this.store.createTask({
|
|
99
|
-
projectId
|
|
190
|
+
projectId,
|
|
100
191
|
title: processedInput.title || taskInput.title,
|
|
101
192
|
description: processedInput.description ?? taskInput.description,
|
|
102
|
-
status: processedInput.status || taskInput.status ||
|
|
193
|
+
status: processedInput.status || taskInput.status || defaultStatus,
|
|
103
194
|
priority: processedInput.priority || taskInput.priority || 'medium',
|
|
195
|
+
taskType: processedInput.taskType || taskInput.taskType || 'task',
|
|
104
196
|
assigneeId: processedInput.assigneeId ?? taskInput.assigneeId,
|
|
105
197
|
reporterId: processedInput.reporterId ?? taskInput.reporterId,
|
|
106
198
|
reviewerId: processedInput.reviewerId ?? taskInput.reviewerId,
|
|
@@ -142,6 +234,14 @@ export class CriticalPathEngine {
|
|
|
142
234
|
const existing = await this.store.getTask(id);
|
|
143
235
|
if (!existing) return null;
|
|
144
236
|
|
|
237
|
+
if (updates.status && updates.status !== existing.status) {
|
|
238
|
+
const workflow = await this.resolveProjectWorkflow(existing.projectId);
|
|
239
|
+
const isValid = validateTransition(workflow || undefined, existing.status, updates.status);
|
|
240
|
+
if (!isValid) {
|
|
241
|
+
throw new WorkflowValidationError(existing.status, updates.status, workflow?.id);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
145
245
|
const processedUpdates = await this.plugins.runBeforeTaskUpdate(id, updates);
|
|
146
246
|
const updated = await this.store.updateTask(id, processedUpdates);
|
|
147
247
|
if (!updated) return null;
|
package/src/index.test.ts
CHANGED
|
@@ -172,4 +172,58 @@ describe('CriticalPathEngine Core Tests', () => {
|
|
|
172
172
|
const graphB = await engine.getTaskDependencyGraph(taskB.id);
|
|
173
173
|
expect(graphB.upstreamTasks.map((t) => t.id)).toContain(taskA.id);
|
|
174
174
|
});
|
|
175
|
+
|
|
176
|
+
it('manages workflows and enforces status transitions', async () => {
|
|
177
|
+
const engine = new CriticalPathEngine();
|
|
178
|
+
|
|
179
|
+
// Create a strict workflow
|
|
180
|
+
const workflow = await engine.createWorkflow({
|
|
181
|
+
name: 'Strict Software Workflow',
|
|
182
|
+
defaultStatusKey: 'backlog',
|
|
183
|
+
statuses: [
|
|
184
|
+
{ key: 'backlog', label: 'Backlog', completionState: 'not_done', executionState: 'inactive' },
|
|
185
|
+
{ key: 'in_progress', label: 'In Progress', completionState: 'not_done', executionState: 'active' },
|
|
186
|
+
{ key: 'done', label: 'Done', completionState: 'done', executionState: 'inactive' }
|
|
187
|
+
],
|
|
188
|
+
transitions: [
|
|
189
|
+
{ id: 't1', fromStatusKey: 'backlog', toStatusKey: 'in_progress', name: 'Start Work' },
|
|
190
|
+
{ id: 't2', fromStatusKey: 'in_progress', toStatusKey: 'done', name: 'Complete Work' }
|
|
191
|
+
]
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
expect(workflow.id).toBeDefined();
|
|
195
|
+
|
|
196
|
+
// Create project tied to workflow
|
|
197
|
+
const proj = await engine.createProject({
|
|
198
|
+
key: 'WF',
|
|
199
|
+
name: 'Workflow Project',
|
|
200
|
+
workflowId: workflow.id
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Create task
|
|
204
|
+
const task = await engine.createTask({
|
|
205
|
+
projectId: proj.id,
|
|
206
|
+
title: 'Workflow Task',
|
|
207
|
+
status: 'backlog',
|
|
208
|
+
priority: 'high'
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
expect(task.status).toBe('backlog');
|
|
212
|
+
expect(task.taskType).toBe('task');
|
|
213
|
+
|
|
214
|
+
// Check allowed transitions
|
|
215
|
+
const allowed = await engine.getAllowedTaskTransitions(task.id);
|
|
216
|
+
expect(allowed).toEqual(['in_progress']);
|
|
217
|
+
|
|
218
|
+
// Valid transition: backlog -> in_progress
|
|
219
|
+
const updated = await engine.updateTask(task.id, { status: 'in_progress' });
|
|
220
|
+
expect(updated?.status).toBe('in_progress');
|
|
221
|
+
|
|
222
|
+
// Invalid transition: in_progress -> backlog (not allowed in strict workflow)
|
|
223
|
+
await expect(engine.updateTask(task.id, { status: 'backlog' })).rejects.toThrow();
|
|
224
|
+
|
|
225
|
+
// Valid transition: in_progress -> done
|
|
226
|
+
const doneTask = await engine.updateTask(task.id, { status: 'done' });
|
|
227
|
+
expect(doneTask?.status).toBe('done');
|
|
228
|
+
});
|
|
175
229
|
});
|
package/src/index.ts
CHANGED
|
@@ -56,4 +56,25 @@ describe('FirebaseStore', () => {
|
|
|
56
56
|
const remaining = await store.getTasks(project.id);
|
|
57
57
|
expect(remaining).toHaveLength(0);
|
|
58
58
|
});
|
|
59
|
+
|
|
60
|
+
it('should create, update, and delete workflows in FirebaseStore', async () => {
|
|
61
|
+
const workflow = await store.createWorkflow({
|
|
62
|
+
name: 'Firebase Workflow',
|
|
63
|
+
isDefault: false,
|
|
64
|
+
defaultStatusKey: 'backlog',
|
|
65
|
+
statuses: [{ key: 'backlog', label: 'Backlog', completionState: 'not_done', executionState: 'inactive' }],
|
|
66
|
+
transitions: []
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(workflow.id).toBeDefined();
|
|
70
|
+
|
|
71
|
+
const fetched = await store.getWorkflow(workflow.id);
|
|
72
|
+
expect(fetched?.name).toBe('Firebase Workflow');
|
|
73
|
+
|
|
74
|
+
const updated = await store.updateWorkflow(workflow.id, { isDefault: true });
|
|
75
|
+
expect(updated?.isDefault).toBe(true);
|
|
76
|
+
|
|
77
|
+
const deleted = await store.deleteWorkflow(workflow.id);
|
|
78
|
+
expect(deleted).toBe(true);
|
|
79
|
+
});
|
|
59
80
|
});
|
package/src/store/firebase.ts
CHANGED
|
@@ -9,8 +9,10 @@ import type {
|
|
|
9
9
|
TimeEntry,
|
|
10
10
|
Activity,
|
|
11
11
|
Webhook,
|
|
12
|
-
TaskDependency
|
|
12
|
+
TaskDependency,
|
|
13
|
+
Workflow
|
|
13
14
|
} from '../types/index.js';
|
|
15
|
+
import { generateProjectKey } from '../utils/key.js';
|
|
14
16
|
|
|
15
17
|
export interface FirestoreDBInterface {
|
|
16
18
|
collection(name: string): {
|
|
@@ -126,7 +128,8 @@ export class FirebaseStore implements StorageAdapter {
|
|
|
126
128
|
async createProject(project: Omit<Project, 'id' | 'createdAt' | 'updatedAt'>): Promise<Project> {
|
|
127
129
|
const docRef = this.db.collection('projects').doc();
|
|
128
130
|
const now = new Date().toISOString();
|
|
129
|
-
const
|
|
131
|
+
const key = project.key || generateProjectKey(project.name);
|
|
132
|
+
const newProj: Project = { ...project, key, id: docRef.id, createdAt: now, updatedAt: now };
|
|
130
133
|
await docRef.set(newProj);
|
|
131
134
|
return newProj;
|
|
132
135
|
}
|
|
@@ -151,6 +154,46 @@ export class FirebaseStore implements StorageAdapter {
|
|
|
151
154
|
return true;
|
|
152
155
|
}
|
|
153
156
|
|
|
157
|
+
// --- Workflows ---
|
|
158
|
+
async getWorkflows(): Promise<Workflow[]> {
|
|
159
|
+
const snap = await this.db.collection('workflows').get();
|
|
160
|
+
return snap.docs.map((doc) => ({ ...doc.data(), id: doc.id }));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async getWorkflow(id: string): Promise<Workflow | null> {
|
|
164
|
+
const snap = await this.db.collection('workflows').doc(id).get();
|
|
165
|
+
if (!snap.exists) return null;
|
|
166
|
+
return { ...snap.data(), id: snap.id };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async createWorkflow(workflow: Omit<Workflow, 'id' | 'createdAt' | 'updatedAt'>): Promise<Workflow> {
|
|
170
|
+
const docRef = this.db.collection('workflows').doc();
|
|
171
|
+
const now = new Date().toISOString();
|
|
172
|
+
const newWf: Workflow = { ...workflow, id: docRef.id, createdAt: now, updatedAt: now };
|
|
173
|
+
await docRef.set(newWf);
|
|
174
|
+
return newWf;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async updateWorkflow(id: string, updates: Partial<Workflow>): Promise<Workflow | null> {
|
|
178
|
+
const existing = await this.getWorkflow(id);
|
|
179
|
+
if (!existing) return null;
|
|
180
|
+
|
|
181
|
+
const updated: Workflow = {
|
|
182
|
+
...existing,
|
|
183
|
+
...updates,
|
|
184
|
+
updatedAt: new Date().toISOString()
|
|
185
|
+
};
|
|
186
|
+
await this.db.collection('workflows').doc(id).set(updated, { merge: true });
|
|
187
|
+
return updated;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async deleteWorkflow(id: string): Promise<boolean> {
|
|
191
|
+
const existing = await this.getWorkflow(id);
|
|
192
|
+
if (!existing) return false;
|
|
193
|
+
await this.db.collection('workflows').doc(id).delete();
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
|
|
154
197
|
// --- Teams ---
|
|
155
198
|
async getTeams(): Promise<Team[]> {
|
|
156
199
|
const snap = await this.db.collection('teams').get();
|