@critical-path/core
v0.15.2
Published
Core headless domain engine, plugin architecture, and data stores for Critical Path
Readme
@critical-path/core
The foundational domain engine, Domain-Driven Design (DDD) aggregate roots, domain event bus, graph cycle detection, and storage adapters for Critical Path.
📦 Features
- Domain-Driven Design (DDD) Engine: Rich aggregates (
TaskEntity,ProjectEntity) encapsulating lifecycle invariants, state transitions, time tracking, and uncommitted domain events. - Typed Domain Event Bus: In-memory pub/sub
DomainEventBusenabling reactive subscriptions to granular domain events (task.created,task.status_changed,time.logged,dependency.added, etc.). - DAG Graph Cycle Invariant: Built-in topological validation (
detectDependencyCycle,CircularDependencyError) to ensure strict acyclic task dependency graphs. - Custom Field Value Object Validation: Strict type-checking (
validateCustomFieldValues,CustomFieldValidationError) against configured project field schemas. - Interface Segregated Repositories: Focused repository contracts (
ProjectRepository,TaskRepository,WorkflowRepository, etc.) composed intoStorageAdapter. - Plugin Lifecycle Architecture: Extensible hooks (
beforeTaskCreate,afterTaskUpdate,beforeTaskDelete, etc.). - Multiple Built-in Storage Adapters:
InMemoryStore: Fast, zero-config in-memory storage for local dev and testing.SQLiteStore: Embedded relational database storage powered by native Node.js SQLite (node:sqlite).FirebaseStore: Firestore collection mapping for cloud-native web and mobile backends.
- File Storage Adapters for Attachments:
InMemoryFileStore: Lightweight in-memory binary asset storage with presigned URL simulation.S3StorageAdapter: Zero-dependency S3 client adapter compatible with AWS SDK v3, v2, MinIO, and Cloudflare R2.FirebaseStorageAdapter: Google Cloud Storage & Firebase Storage bucket adapter.
- Threaded Conversations, Discussions & Emoji Reactions:
- Nested replies (
parentId), multi-author taxonomy (user,agent,system), emoji reactions (addCommentReaction,removeCommentReaction), and real-time domain event streaming (comment.created,comment.updated,comment.deleted,comment.reaction.added,comment.reaction.removed).
- Nested replies (
- Multi-Assignee Support & Agent Collaboration:
- First-class
TaskAssigneetaxonomy supporting co-assignments across users, autonomous AI agents, and teams with role metadata and custom avatar URLs.
- First-class
- Bidirectional Workflow Transitions:
- Symmetrical transition helpers (
getAllowedNextStatuses,getAllowedPreviousStatuses) and engine methods for moving tasks backwards and forwards through customized workflow states.
- Symmetrical transition helpers (
- Universal Semantic Status & Implied Status Framework:
- Clean 3-tier status architecture: Universal Semantic Statuses (
not_started,in_progress,completed,canceled), customizable workflow-defined statuses mapped bycategory, and automatic system-derived implied statuses (isReady,isBlocked,blockingTaskIds,isOverdue,isUpcoming,isUnplanned,isUnassigned,isStalled,isOverEstimate,isPaceWarning).
- Clean 3-tier status architecture: Universal Semantic Statuses (
- Creative Workflows & First-Class Deliverables:
DeliverableEntityaggregate with automatic delivery timestamps (deliveredAt) and URL registry (outputUrls).DEFAULT_CREATIVE_WORKFLOWtemplate tailored for creative agencies and content production pipelines.- Rollup calculations via
getDeliverableSummary()delivering progress percentages, completed tasks, and total estimated/logged hours across assigned tasks.
🚀 Usage Examples
1. Initializing the Engine & Subscribing to Domain Events
import { CriticalPathEngine, SQLiteStore, type TaskStatusChangedEvent } from '@critical-path/core';
const store = new SQLiteStore({ filename: 'critical-path.db' });
const engine = new CriticalPathEngine({ store });
// Subscribe to specific typed domain events
engine.events.subscribe<TaskStatusChangedEvent>('task.status_changed', (event) => {
console.log(`Task ${event.aggregateId} moved from ${event.payload.previousStatus} to ${event.payload.newStatus}`);
});
// Or subscribe to all domain events with wildcard
engine.events.subscribe('*', (event) => {
console.log(`[Domain Event] ${event.name}`, event);
});2. Rich Entities & Invariant Enforcement
import { TaskEntity, DEFAULT_SOFTWARE_WORKFLOW } from '@critical-path/core';
// Create a rich Task Aggregate Root
const task = TaskEntity.create({
projectId: 'proj_123',
title: 'Implement Payment Gateway',
status: 'todo',
priority: 'high'
});
// Perform valid state transitions with workflow enforcement
task.transitionTo('in_progress', DEFAULT_SOFTWARE_WORKFLOW);
// Log time on aggregate
task.logTime({ hours: 3.5, isBillable: true });
// Read and dispatch uncommitted events
const events = task.getUncommittedEvents();
task.clearEvents();3. DAG Dependency Cycle Prevention
import { CircularDependencyError } from '@critical-path/core';
try {
await engine.addDependency({
taskId: 'task_C',
dependsOnTaskId: 'task_A',
type: 'blocking'
});
} catch (error) {
if (error instanceof CircularDependencyError) {
console.error(`Blocked cyclic dependency! Cycle path: ${error.cyclePath.join(' -> ')}`);
}
}4. Tracking Creative Deliverables & Rollup Metrics
import { CriticalPathEngine, DEFAULT_CREATIVE_WORKFLOW } from '@critical-path/core';
const engine = new CriticalPathEngine();
// 1. Create deliverable
const deliverable = await engine.createDeliverable({
projectId: 'proj_1',
title: 'Brand Hero Video 30s',
format: 'ProRes 422HQ',
specs: { resolution: '3840x2160', fps: 24 }
});
// 2. Attach tasks to deliverable
await engine.createTask({
projectId: 'proj_1',
deliverableId: deliverable.id,
title: 'Storyboard & Animatic',
status: 'approved',
estimatedHours: 12,
loggedHours: 12,
progress: 100
});
// 3. Rollup metrics
const summary = await engine.getDeliverableSummary(deliverable.id);
console.log(`Progress: ${summary?.progressPercentage}%, Total tasks: ${summary?.totalTasks}`);5. Evaluating Implied Statuses & Dependency Readiness
// Evaluates task status category, upstream dependency states, schedules, assignees, and pace
const state = await engine.getTaskLifecycleState('task_123');
if (state?.isBlocked) {
console.warn(`Task blocked by upstream tasks: ${state.blockingTaskIds.join(', ')}`);
} else if (state?.isReady) {
console.log('All dependencies satisfied! Task is ready to start.');
}
if (state?.isOverEstimate) {
console.warn('Task logged hours have exceeded estimated hours!');
}
if (state?.isPaceWarning) {
console.warn('Task has been active longer than its estimated duration!');
}🔌 Creating a Custom Plugin
import type { CriticalPathPlugin } from '@critical-path/core';
export const auditPlugin: CriticalPathPlugin = {
id: 'audit-logger',
name: 'Audit Logger',
version: '1.0.0',
hooks: {
beforeTaskCreate: (task) => {
return { ...task, tags: [...(task.tags || []), 'AUDITED'] };
}
}
};