@critical-path/core 0.1.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.
@@ -0,0 +1,130 @@
1
+ export type Priority = 'urgent' | 'high' | 'medium' | 'low' | 'none';
2
+ export type TaskStatus = 'backlog' | 'todo' | 'in_progress' | 'in_review' | 'done' | 'canceled';
3
+ export type Role = 'admin' | 'project_manager' | 'contributor' | 'viewer';
4
+ export interface User {
5
+ id: string;
6
+ name: string;
7
+ email: string;
8
+ avatarUrl?: string;
9
+ role: Role;
10
+ createdAt: string;
11
+ }
12
+ export interface CustomFieldDefinition {
13
+ id: string;
14
+ key: string;
15
+ label: string;
16
+ type: 'text' | 'number' | 'date' | 'boolean' | 'single_select' | 'multi_select' | 'user';
17
+ options?: string[];
18
+ required?: boolean;
19
+ defaultValue?: unknown;
20
+ }
21
+ export interface Project {
22
+ id: string;
23
+ key: string;
24
+ name: string;
25
+ description?: string;
26
+ ownerId?: string;
27
+ members?: string[];
28
+ customFieldDefinitions?: CustomFieldDefinition[];
29
+ createdAt: string;
30
+ updatedAt: string;
31
+ }
32
+ export interface TaskDependency {
33
+ id: string;
34
+ taskId: string;
35
+ dependsOnTaskId: string;
36
+ type: 'blocking' | 'blocked_by' | 'relates_to';
37
+ }
38
+ export interface Task {
39
+ id: string;
40
+ projectId: string;
41
+ title: string;
42
+ description?: string;
43
+ status: TaskStatus;
44
+ priority: Priority;
45
+ assigneeId?: string;
46
+ reporterId?: string;
47
+ sprintId?: string;
48
+ dueDate?: string;
49
+ estimatedHours?: number;
50
+ loggedHours?: number;
51
+ tags?: string[];
52
+ customFields?: Record<string, unknown>;
53
+ parentId?: string;
54
+ createdAt: string;
55
+ updatedAt: string;
56
+ }
57
+ export interface Sprint {
58
+ id: string;
59
+ projectId: string;
60
+ name: string;
61
+ goal?: string;
62
+ startDate: string;
63
+ endDate: string;
64
+ status: 'planning' | 'active' | 'completed';
65
+ createdAt: string;
66
+ }
67
+ export interface Comment {
68
+ id: string;
69
+ taskId: string;
70
+ authorId: string;
71
+ content: string;
72
+ createdAt: string;
73
+ updatedAt: string;
74
+ }
75
+ export interface TimeEntry {
76
+ id: string;
77
+ taskId: string;
78
+ userId: string;
79
+ hours: number;
80
+ description?: string;
81
+ loggedAt: string;
82
+ }
83
+ export interface Activity {
84
+ id: string;
85
+ projectId?: string;
86
+ taskId?: string;
87
+ actorId: string;
88
+ action: string;
89
+ details: Record<string, unknown>;
90
+ createdAt: string;
91
+ }
92
+ export interface Webhook {
93
+ id: string;
94
+ name: string;
95
+ url: string;
96
+ secret?: string;
97
+ events: WebhookEvent[];
98
+ active: boolean;
99
+ createdAt: string;
100
+ }
101
+ export type WebhookEvent = 'project.created' | 'project.updated' | 'task.created' | 'task.updated' | 'task.deleted' | 'task.status_changed' | 'comment.created' | 'sprint.started' | 'sprint.completed';
102
+ export interface PluginHooks {
103
+ beforeTaskCreate?: (task: Partial<Task>) => Promise<Partial<Task>> | Partial<Task>;
104
+ afterTaskCreate?: (task: Task) => Promise<void> | void;
105
+ beforeTaskUpdate?: (id: string, updates: Partial<Task>) => Promise<Partial<Task>> | Partial<Task>;
106
+ afterTaskUpdate?: (task: Task, previousState: Task) => Promise<void> | void;
107
+ beforeTaskDelete?: (id: string) => Promise<void> | void;
108
+ afterTaskDelete?: (id: string) => Promise<void> | void;
109
+ }
110
+ export interface CriticalPathPlugin {
111
+ id: string;
112
+ name: string;
113
+ version: string;
114
+ description?: string;
115
+ hooks?: PluginHooks;
116
+ customFieldTypes?: CustomFieldDefinition[];
117
+ init?: (engine: unknown) => Promise<void> | void;
118
+ }
119
+ export interface CriticalPathConfig {
120
+ store?: 'memory' | 'sqlite' | unknown;
121
+ plugins?: CriticalPathPlugin[];
122
+ webhooks?: Omit<Webhook, 'id' | 'createdAt'>[];
123
+ initialData?: {
124
+ projects?: Project[];
125
+ tasks?: Task[];
126
+ users?: User[];
127
+ sprints?: Sprint[];
128
+ };
129
+ }
130
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@critical-path/core",
3
+ "version": "0.1.0",
4
+ "description": "Core headless domain engine, plugin architecture, and data stores for Critical Path",
5
+ "author": "Jack James",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "import": "./dist/index.js",
13
+ "types": "./dist/index.d.ts"
14
+ }
15
+ },
16
+ "devDependencies": {
17
+ "@types/node": "^24.0.0",
18
+ "typescript": "^5.7.0",
19
+ "vitest": "^3.0.0"
20
+ },
21
+ "engines": {
22
+ "node": ">=24.0.0"
23
+ },
24
+ "scripts": {
25
+ "build": "tsc --build",
26
+ "test": "vitest run",
27
+ "clean": "rm -rf dist *.tsbuildinfo"
28
+ }
29
+ }
@@ -0,0 +1,169 @@
1
+ import type { CriticalPathConfig, Project, Task, WebhookEvent } from '../types/index.js';
2
+ import { StorageAdapter, InMemoryStore } from '../store/index.js';
3
+ import { PluginRegistry } from '../plugins/index.js';
4
+
5
+ export class CriticalPathEngine {
6
+ public readonly store: StorageAdapter;
7
+ public readonly plugins: PluginRegistry;
8
+
9
+ constructor(config: CriticalPathConfig = {}) {
10
+ this.store = typeof config.store === 'object' && config.store !== null
11
+ ? (config.store as StorageAdapter)
12
+ : new InMemoryStore();
13
+
14
+ this.plugins = new PluginRegistry();
15
+
16
+ if (config.plugins) {
17
+ for (const plugin of config.plugins) {
18
+ this.plugins.register(plugin);
19
+ }
20
+ }
21
+
22
+ if (config.initialData) {
23
+ this.seedInitialData(config.initialData);
24
+ }
25
+ }
26
+
27
+ private async seedInitialData(data: NonNullable<CriticalPathConfig['initialData']>): Promise<void> {
28
+ if (data.projects) {
29
+ for (const p of data.projects) {
30
+ await this.store.createProject(p);
31
+ }
32
+ }
33
+ if (data.tasks) {
34
+ for (const t of data.tasks) {
35
+ await this.store.createTask(t);
36
+ }
37
+ }
38
+ }
39
+
40
+ // --- Domain Methods with Plugin Hooks & Audit Logging ---
41
+
42
+ async getProjects(): Promise<Project[]> {
43
+ return this.store.getProjects();
44
+ }
45
+
46
+ async getProject(id: string): Promise<Project | null> {
47
+ return this.store.getProject(id);
48
+ }
49
+
50
+ async createProject(project: Omit<Project, 'id' | 'createdAt' | 'updatedAt'>): Promise<Project> {
51
+ const created = await this.store.createProject(project);
52
+ await this.store.logActivity({
53
+ projectId: created.id,
54
+ actorId: project.ownerId || 'system',
55
+ action: 'project.created',
56
+ details: { name: created.name, key: created.key }
57
+ });
58
+ this.dispatchWebhook('project.created', { project: created });
59
+ return created;
60
+ }
61
+
62
+ async getTasks(projectId?: string): Promise<Task[]> {
63
+ return this.store.getTasks(projectId);
64
+ }
65
+
66
+ async getTask(id: string): Promise<Task | null> {
67
+ return this.store.getTask(id);
68
+ }
69
+
70
+ async createTask(taskInput: Omit<Task, 'id' | 'createdAt' | 'updatedAt'>): Promise<Task> {
71
+ // Run plugin beforeTaskCreate hooks
72
+ const processedInput = await this.plugins.runBeforeTaskCreate(taskInput);
73
+
74
+ const created = await this.store.createTask({
75
+ projectId: processedInput.projectId || taskInput.projectId,
76
+ title: processedInput.title || taskInput.title,
77
+ description: processedInput.description ?? taskInput.description,
78
+ status: processedInput.status || taskInput.status || 'todo',
79
+ priority: processedInput.priority || taskInput.priority || 'medium',
80
+ assigneeId: processedInput.assigneeId ?? taskInput.assigneeId,
81
+ reporterId: processedInput.reporterId ?? taskInput.reporterId,
82
+ sprintId: processedInput.sprintId ?? taskInput.sprintId,
83
+ dueDate: processedInput.dueDate ?? taskInput.dueDate,
84
+ estimatedHours: processedInput.estimatedHours ?? taskInput.estimatedHours,
85
+ loggedHours: processedInput.loggedHours ?? taskInput.loggedHours ?? 0,
86
+ tags: processedInput.tags ?? taskInput.tags ?? [],
87
+ customFields: processedInput.customFields ?? taskInput.customFields ?? {},
88
+ parentId: processedInput.parentId ?? taskInput.parentId
89
+ });
90
+
91
+ // Run plugin afterTaskCreate hooks
92
+ await this.plugins.runAfterTaskCreate(created);
93
+
94
+ await this.store.logActivity({
95
+ projectId: created.projectId,
96
+ taskId: created.id,
97
+ actorId: created.reporterId || 'system',
98
+ action: 'task.created',
99
+ details: { title: created.title, status: created.status }
100
+ });
101
+
102
+ this.dispatchWebhook('task.created', { task: created });
103
+ return created;
104
+ }
105
+
106
+ async updateTask(id: string, updates: Partial<Task>): Promise<Task | null> {
107
+ const existing = await this.store.getTask(id);
108
+ if (!existing) return null;
109
+
110
+ const processedUpdates = await this.plugins.runBeforeTaskUpdate(id, updates);
111
+ const updated = await this.store.updateTask(id, processedUpdates);
112
+ if (!updated) return null;
113
+
114
+ await this.plugins.runAfterTaskUpdate(updated, existing);
115
+
116
+ const isStatusChange = existing.status !== updated.status;
117
+ await this.store.logActivity({
118
+ projectId: updated.projectId,
119
+ taskId: updated.id,
120
+ actorId: updated.assigneeId || 'system',
121
+ action: isStatusChange ? 'task.status_changed' : 'task.updated',
122
+ details: { fromStatus: existing.status, toStatus: updated.status }
123
+ });
124
+
125
+ this.dispatchWebhook(isStatusChange ? 'task.status_changed' : 'task.updated', {
126
+ task: updated,
127
+ previous: existing
128
+ });
129
+
130
+ return updated;
131
+ }
132
+
133
+ async deleteTask(id: string): Promise<boolean> {
134
+ const existing = await this.store.getTask(id);
135
+ if (!existing) return false;
136
+
137
+ await this.plugins.runBeforeTaskDelete(id);
138
+ const deleted = await this.store.deleteTask(id);
139
+
140
+ if (deleted) {
141
+ await this.plugins.runAfterTaskDelete(id);
142
+ await this.store.logActivity({
143
+ projectId: existing.projectId,
144
+ taskId: id,
145
+ actorId: 'system',
146
+ action: 'task.deleted',
147
+ details: { title: existing.title }
148
+ });
149
+ this.dispatchWebhook('task.deleted', { taskId: id, title: existing.title });
150
+ }
151
+ return deleted;
152
+ }
153
+
154
+ private async dispatchWebhook(event: WebhookEvent, payload: Record<string, unknown>): Promise<void> {
155
+ const webhooks = await this.store.getWebhooks();
156
+ const active = webhooks.filter((w) => w.active && w.events.includes(event));
157
+
158
+ for (const wh of active) {
159
+ // Fire-and-forget webhook post
160
+ fetch(wh.url, {
161
+ method: 'POST',
162
+ headers: { 'Content-Type': 'application/json' },
163
+ body: JSON.stringify({ event, timestamp: new Date().toISOString(), data: payload })
164
+ }).catch(() => {
165
+ // Silent catch for webhook delivery errors in dev
166
+ });
167
+ }
168
+ }
169
+ }
@@ -0,0 +1,63 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { CriticalPathEngine } from './engine/index.js';
3
+ import type { CriticalPathPlugin } from './types/index.js';
4
+
5
+ describe('CriticalPathEngine Core Tests', () => {
6
+ it('creates projects and tasks with activity logs', async () => {
7
+ const engine = new CriticalPathEngine();
8
+
9
+ const proj = await engine.createProject({
10
+ key: 'TEST',
11
+ name: 'Test Project',
12
+ description: 'A test project'
13
+ });
14
+
15
+ expect(proj.id).toBeDefined();
16
+ expect(proj.key).toBe('TEST');
17
+
18
+ const task = await engine.createTask({
19
+ projectId: proj.id,
20
+ title: 'First Task',
21
+ status: 'todo',
22
+ priority: 'high'
23
+ });
24
+
25
+ expect(task.id).toBeDefined();
26
+ expect(task.title).toBe('First Task');
27
+
28
+ const activities = await engine.store.getActivities({ projectId: proj.id });
29
+ expect(activities.length).toBeGreaterThanOrEqual(2);
30
+ });
31
+
32
+ it('runs plugin hooks on task lifecycle', async () => {
33
+ let hookExecuted = false;
34
+
35
+ const testPlugin: CriticalPathPlugin = {
36
+ id: 'auto-tag-plugin',
37
+ name: 'Auto Tag Plugin',
38
+ version: '1.0.0',
39
+ hooks: {
40
+ beforeTaskCreate: (task) => {
41
+ hookExecuted = true;
42
+ return {
43
+ ...task,
44
+ tags: [...(task.tags || []), 'auto-tagged']
45
+ };
46
+ }
47
+ }
48
+ };
49
+
50
+ const engine = new CriticalPathEngine({ plugins: [testPlugin] });
51
+
52
+ const proj = await engine.createProject({ key: 'PLUG', name: 'Plugin Proj' });
53
+ const task = await engine.createTask({
54
+ projectId: proj.id,
55
+ title: 'Plugin Task',
56
+ status: 'todo',
57
+ priority: 'medium'
58
+ });
59
+
60
+ expect(hookExecuted).toBe(true);
61
+ expect(task.tags).toContain('auto-tagged');
62
+ });
63
+ });
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from './types/index.js';
2
+ export * from './store/index.js';
3
+ export * from './plugins/index.js';
4
+ export * from './engine/index.js';
@@ -0,0 +1,68 @@
1
+ import type { CriticalPathPlugin, Task } from '../types/index.js';
2
+
3
+ export class PluginRegistry {
4
+ private plugins = new Map<string, CriticalPathPlugin>();
5
+
6
+ register(plugin: CriticalPathPlugin): void {
7
+ if (this.plugins.has(plugin.id)) {
8
+ throw new Error(`Plugin with ID "${plugin.id}" is already registered.`);
9
+ }
10
+ this.plugins.set(plugin.id, plugin);
11
+ }
12
+
13
+ getPlugins(): CriticalPathPlugin[] {
14
+ return Array.from(this.plugins.values());
15
+ }
16
+
17
+ async runBeforeTaskCreate(task: Partial<Task>): Promise<Partial<Task>> {
18
+ let currentTask = { ...task };
19
+ for (const plugin of this.plugins.values()) {
20
+ if (plugin.hooks?.beforeTaskCreate) {
21
+ currentTask = await plugin.hooks.beforeTaskCreate(currentTask);
22
+ }
23
+ }
24
+ return currentTask;
25
+ }
26
+
27
+ async runAfterTaskCreate(task: Task): Promise<void> {
28
+ for (const plugin of this.plugins.values()) {
29
+ if (plugin.hooks?.afterTaskCreate) {
30
+ await plugin.hooks.afterTaskCreate(task);
31
+ }
32
+ }
33
+ }
34
+
35
+ async runBeforeTaskUpdate(id: string, updates: Partial<Task>): Promise<Partial<Task>> {
36
+ let currentUpdates = { ...updates };
37
+ for (const plugin of this.plugins.values()) {
38
+ if (plugin.hooks?.beforeTaskUpdate) {
39
+ currentUpdates = await plugin.hooks.beforeTaskUpdate(id, currentUpdates);
40
+ }
41
+ }
42
+ return currentUpdates;
43
+ }
44
+
45
+ async runAfterTaskUpdate(task: Task, previousState: Task): Promise<void> {
46
+ for (const plugin of this.plugins.values()) {
47
+ if (plugin.hooks?.afterTaskUpdate) {
48
+ await plugin.hooks.afterTaskUpdate(task, previousState);
49
+ }
50
+ }
51
+ }
52
+
53
+ async runBeforeTaskDelete(id: string): Promise<void> {
54
+ for (const plugin of this.plugins.values()) {
55
+ if (plugin.hooks?.beforeTaskDelete) {
56
+ await plugin.hooks.beforeTaskDelete(id);
57
+ }
58
+ }
59
+ }
60
+
61
+ async runAfterTaskDelete(id: string): Promise<void> {
62
+ for (const plugin of this.plugins.values()) {
63
+ if (plugin.hooks?.afterTaskDelete) {
64
+ await plugin.hooks.afterTaskDelete(id);
65
+ }
66
+ }
67
+ }
68
+ }